mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
Merge branch 'improve-subsurfaceweb' of git://github.com/thiagomacieira/subsurface
This commit is contained in:
commit
421dceba1e
4 changed files with 588 additions and 197 deletions
|
@ -269,7 +269,7 @@ void MainWindow::on_actionDownloadWeb_triggered()
|
||||||
|
|
||||||
void MainWindow::on_actionDivelogs_de_triggered()
|
void MainWindow::on_actionDivelogs_de_triggered()
|
||||||
{
|
{
|
||||||
DivelogsDeWebServices::instance()->exec();
|
DivelogsDeWebServices::instance()->downloadDives();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_actionEditDeviceNames_triggered()
|
void MainWindow::on_actionEditDeviceNames_triggered()
|
||||||
|
|
|
@ -3,205 +3,33 @@
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
|
|
||||||
#include <libxml/parser.h>
|
#include <libxml/parser.h>
|
||||||
|
#include <zip.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QHttpMultiPart>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QXmlStreamReader>
|
||||||
#include <qdesktopservices.h>
|
#include <qdesktopservices.h>
|
||||||
|
|
||||||
#include "../dive.h"
|
#include "../dive.h"
|
||||||
#include "../divelist.h"
|
#include "../divelist.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_UNIX
|
||||||
|
# include <unistd.h> // for dup(2)
|
||||||
|
#endif
|
||||||
|
|
||||||
struct dive_table gps_location_table;
|
struct dive_table gps_location_table;
|
||||||
static bool merge_locations_into_dives(void);
|
static bool merge_locations_into_dives(void);
|
||||||
|
|
||||||
WebServices::WebServices(QWidget* parent, Qt::WindowFlags f): QDialog(parent, f)
|
|
||||||
, reply(0)
|
|
||||||
{
|
|
||||||
ui.setupUi(this);
|
|
||||||
connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*)));
|
|
||||||
connect(ui.download, SIGNAL(clicked(bool)), this, SLOT(startDownload()));
|
|
||||||
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebServices::hidePassword()
|
|
||||||
{
|
|
||||||
ui.password->hide();
|
|
||||||
ui.passLabel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebServices::hideUpload()
|
|
||||||
{
|
|
||||||
ui.upload->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
// #
|
|
||||||
// #
|
|
||||||
// # Subsurface Web Service Implementation.
|
|
||||||
// #
|
|
||||||
// #
|
|
||||||
|
|
||||||
SubsurfaceWebServices* SubsurfaceWebServices::instance()
|
|
||||||
{
|
|
||||||
static SubsurfaceWebServices *self = new SubsurfaceWebServices(mainWindow());
|
|
||||||
self->setAttribute(Qt::WA_QuitOnClose, false);
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
SubsurfaceWebServices::SubsurfaceWebServices(QWidget* parent, Qt::WindowFlags f)
|
|
||||||
{
|
|
||||||
QSettings s;
|
|
||||||
ui.userID->setText(s.value("subsurface_webservice_uid").toString().toUpper());
|
|
||||||
hidePassword();
|
|
||||||
hideUpload();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void clear_table(struct dive_table *table)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < table->nr; i++)
|
|
||||||
free(table->dives[i]);
|
|
||||||
table->nr = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SubsurfaceWebServices::buttonClicked(QAbstractButton* button)
|
|
||||||
{
|
|
||||||
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
|
|
||||||
switch(ui.buttonBox->buttonRole(button)){
|
|
||||||
case QDialogButtonBox::ApplyRole:{
|
|
||||||
clear_table(&gps_location_table);
|
|
||||||
QByteArray url = tr("Webservice").toLocal8Bit();
|
|
||||||
parse_xml_buffer(url.data(), downloadedData.data(), downloadedData.length(), &gps_location_table, NULL, NULL);
|
|
||||||
|
|
||||||
/* now merge the data in the gps_location table into the dive_table */
|
|
||||||
if (merge_locations_into_dives()) {
|
|
||||||
mark_divelist_changed(TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* store last entered uid in config */
|
|
||||||
QSettings s;
|
|
||||||
s.setValue("subsurface_webservice_uid", ui.userID->text().toUpper());
|
|
||||||
s.sync();
|
|
||||||
hide();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case QDialogButtonBox::RejectRole:
|
|
||||||
// we may want to clean up after ourselves, but this
|
|
||||||
// makes Subsurface throw a SIGSEGV...
|
|
||||||
// manager->deleteLater();
|
|
||||||
// reply->deleteLater();
|
|
||||||
ui.progressBar->setMaximum(1);
|
|
||||||
break;
|
|
||||||
case QDialogButtonBox::HelpRole:
|
|
||||||
QDesktopServices::openUrl(QUrl("http://api.hohndel.org"));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SubsurfaceWebServices::startDownload()
|
|
||||||
{
|
|
||||||
QUrl url("http://api.hohndel.org/api/dive/get/");
|
|
||||||
url.setQueryItems( QList<QPair<QString,QString> >() << qMakePair(QString("login"), ui.userID->text().toUpper()));
|
|
||||||
|
|
||||||
manager = new QNetworkAccessManager(this);
|
|
||||||
QNetworkRequest request;
|
|
||||||
request.setUrl(url);
|
|
||||||
request.setRawHeader("Accept", "text/xml");
|
|
||||||
reply = manager->get(request);
|
|
||||||
ui.status->setText(tr("Wait a bit until we have something..."));
|
|
||||||
ui.progressBar->setRange(0,0); // this makes the progressbar do an 'infinite spin'
|
|
||||||
ui.download->setEnabled(false);
|
|
||||||
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
|
|
||||||
|
|
||||||
connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
|
|
||||||
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
|
|
||||||
this, SLOT(downloadError(QNetworkReply::NetworkError)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SubsurfaceWebServices::downloadFinished()
|
|
||||||
{
|
|
||||||
ui.progressBar->setRange(0,1);
|
|
||||||
downloadedData = reply->readAll();
|
|
||||||
|
|
||||||
ui.download->setEnabled(true);
|
|
||||||
ui.status->setText(tr("Download Finished"));
|
|
||||||
|
|
||||||
uint resultCode = download_dialog_parse_response(downloadedData);
|
|
||||||
setStatusText(resultCode);
|
|
||||||
if (resultCode == DD_STATUS_OK){
|
|
||||||
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
|
|
||||||
}
|
|
||||||
manager->deleteLater();
|
|
||||||
reply->deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SubsurfaceWebServices::downloadError(QNetworkReply::NetworkError error)
|
|
||||||
{
|
|
||||||
ui.download->setEnabled(true);
|
|
||||||
ui.progressBar->setRange(0,1);
|
|
||||||
ui.status->setText(QString::number((int)QNetworkRequest::HttpStatusCodeAttribute));
|
|
||||||
manager->deleteLater();
|
|
||||||
reply->deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SubsurfaceWebServices::setStatusText(int status)
|
|
||||||
{
|
|
||||||
QString text;
|
|
||||||
switch (status) {
|
|
||||||
case DD_STATUS_ERROR_CONNECT: text = tr("Connection Error: "); break;
|
|
||||||
case DD_STATUS_ERROR_ID: text = tr("Invalid user identifier!"); break;
|
|
||||||
case DD_STATUS_ERROR_PARSE: text = tr("Cannot parse response!"); break;
|
|
||||||
case DD_STATUS_OK: text = tr("Download Success!"); break;
|
|
||||||
}
|
|
||||||
ui.status->setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* requires that there is a <download> or <error> tag under the <root> tag */
|
|
||||||
void SubsurfaceWebServices::download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status)
|
|
||||||
{
|
|
||||||
xmlNodePtr cur_node;
|
|
||||||
for (cur_node = node; cur_node; cur_node = cur_node->next) {
|
|
||||||
if ((!strcmp((const char *)cur_node->name, (const char *)"download")) &&
|
|
||||||
(!strcmp((const char *)xmlNodeGetContent(cur_node), (const char *)"ok"))) {
|
|
||||||
*download_status = DD_STATUS_OK;
|
|
||||||
return;
|
|
||||||
} else if (!strcmp((const char *)cur_node->name, (const char *)"error")) {
|
|
||||||
*download_status = DD_STATUS_ERROR_ID;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int SubsurfaceWebServices::download_dialog_parse_response(const QByteArray& xml)
|
|
||||||
{
|
|
||||||
xmlNodePtr root;
|
|
||||||
xmlDocPtr doc = xmlParseMemory(xml.data(), xml.length());
|
|
||||||
unsigned int status = DD_STATUS_ERROR_PARSE;
|
|
||||||
|
|
||||||
if (!doc)
|
|
||||||
return DD_STATUS_ERROR_PARSE;
|
|
||||||
root = xmlDocGetRootElement(doc);
|
|
||||||
if (!root) {
|
|
||||||
status = DD_STATUS_ERROR_PARSE;
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
if (root->children)
|
|
||||||
download_dialog_traverse_xml(root->children, &status);
|
|
||||||
end:
|
|
||||||
xmlFreeDoc(doc);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool is_automatic_fix(struct dive *gpsfix)
|
static bool is_automatic_fix(struct dive *gpsfix)
|
||||||
{
|
{
|
||||||
if (gpsfix && gpsfix->location &&
|
if (gpsfix && gpsfix->location &&
|
||||||
(!strcmp(gpsfix->location, "automatic fix") ||
|
(!strcmp(gpsfix->location, "automatic fix") ||
|
||||||
!strcmp(gpsfix->location, "Auto-created dive")))
|
!strcmp(gpsfix->location, "Auto-created dive")))
|
||||||
return TRUE;
|
return TRUE;
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
@ -223,9 +51,9 @@ static bool merge_locations_into_dives(void)
|
||||||
struct tm tm;
|
struct tm tm;
|
||||||
utc_mkdate(gpsfix->when, &tm);
|
utc_mkdate(gpsfix->when, &tm);
|
||||||
printf("found dive named %s @ %04d-%02d-%02d %02d:%02d:%02d\n",
|
printf("found dive named %s @ %04d-%02d-%02d %02d:%02d:%02d\n",
|
||||||
gpsfix->location,
|
gpsfix->location,
|
||||||
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
|
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
|
||||||
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||||
#endif
|
#endif
|
||||||
changed++;
|
changed++;
|
||||||
copy_gps_location(gpsfix, dive);
|
copy_gps_location(gpsfix, dive);
|
||||||
|
@ -252,9 +80,9 @@ static bool merge_locations_into_dives(void)
|
||||||
utc_mkdate(gpsfix->when, &tm);
|
utc_mkdate(gpsfix->when, &tm);
|
||||||
#if DEBUG_WEBSERVICE
|
#if DEBUG_WEBSERVICE
|
||||||
printf("didn't find dive matching gps fix named %s @ %04d-%02d-%02d %02d:%02d:%02d\n",
|
printf("didn't find dive matching gps fix named %s @ %04d-%02d-%02d %02d:%02d:%02d\n",
|
||||||
gpsfix->location,
|
gpsfix->location,
|
||||||
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
|
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
|
||||||
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,12 +90,347 @@ static bool merge_locations_into_dives(void)
|
||||||
return changed > 0;
|
return changed > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void clear_table(struct dive_table *table)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < table->nr; i++)
|
||||||
|
free(table->dives[i]);
|
||||||
|
table->nr = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebServices::WebServices(QWidget* parent, Qt::WindowFlags f): QDialog(parent, f)
|
||||||
|
, reply(0)
|
||||||
|
{
|
||||||
|
ui.setupUi(this);
|
||||||
|
connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*)));
|
||||||
|
connect(ui.download, SIGNAL(clicked(bool)), this, SLOT(startDownload()));
|
||||||
|
connect(ui.upload, SIGNAL(clicked(bool)), this, SLOT(startUpload()));
|
||||||
|
connect(&timeout, SIGNAL(timeout()), this, SLOT(downloadTimedOut()));
|
||||||
|
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
|
||||||
|
timeout.setSingleShot(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServices::hidePassword()
|
||||||
|
{
|
||||||
|
ui.password->hide();
|
||||||
|
ui.passLabel->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServices::hideUpload()
|
||||||
|
{
|
||||||
|
ui.upload->hide();
|
||||||
|
ui.download->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServices::hideDownload()
|
||||||
|
{
|
||||||
|
ui.download->hide();
|
||||||
|
ui.upload->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkAccessManager *WebServices::manager()
|
||||||
|
{
|
||||||
|
static QNetworkAccessManager *manager = new QNetworkAccessManager(qApp);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServices::downloadTimedOut()
|
||||||
|
{
|
||||||
|
if (!reply)
|
||||||
|
return;
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
reply = NULL;
|
||||||
|
resetState();
|
||||||
|
ui.status->setText(tr("Operation timed out"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServices::updateProgress(qint64 current, qint64 total)
|
||||||
|
{
|
||||||
|
if (!reply)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (total >= INT_MAX / 2) {
|
||||||
|
// over a gigabyte!
|
||||||
|
if (total >= Q_INT64_C(1) << 47) {
|
||||||
|
total >>= 16;
|
||||||
|
current >>= 16;
|
||||||
|
}
|
||||||
|
total >>= 16;
|
||||||
|
current >>= 16;
|
||||||
|
}
|
||||||
|
ui.progressBar->setRange(0, total);
|
||||||
|
ui.progressBar->setValue(current);
|
||||||
|
ui.status->setText(tr("Transfering data..."));
|
||||||
|
|
||||||
|
// reset the timer: 30 seconds after we last got any data
|
||||||
|
timeout.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServices::connectSignalsForDownload(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
|
||||||
|
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
|
||||||
|
this, SLOT(downloadError(QNetworkReply::NetworkError)));
|
||||||
|
connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this,
|
||||||
|
SLOT(updateProgress(qint64,qint64)));
|
||||||
|
|
||||||
|
timeout.start(30000); // 30s
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServices::resetState()
|
||||||
|
{
|
||||||
|
ui.download->setEnabled(true);
|
||||||
|
ui.upload->setEnabled(true);
|
||||||
|
ui.userID->setEnabled(true);
|
||||||
|
ui.password->setEnabled(true);
|
||||||
|
ui.progressBar->reset();
|
||||||
|
ui.progressBar->setRange(0,1);
|
||||||
|
ui.status->setText(QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// #
|
||||||
|
// #
|
||||||
|
// # Subsurface Web Service Implementation.
|
||||||
|
// #
|
||||||
|
// #
|
||||||
|
|
||||||
|
SubsurfaceWebServices* SubsurfaceWebServices::instance()
|
||||||
|
{
|
||||||
|
static SubsurfaceWebServices *self = new SubsurfaceWebServices(mainWindow());
|
||||||
|
self->setAttribute(Qt::WA_QuitOnClose, false);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubsurfaceWebServices::SubsurfaceWebServices(QWidget* parent, Qt::WindowFlags f)
|
||||||
|
{
|
||||||
|
QSettings s;
|
||||||
|
ui.userID->setText(s.value("subsurface_webservice_uid").toString().toUpper());
|
||||||
|
hidePassword();
|
||||||
|
hideUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SubsurfaceWebServices::buttonClicked(QAbstractButton* button)
|
||||||
|
{
|
||||||
|
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
|
||||||
|
switch(ui.buttonBox->buttonRole(button)){
|
||||||
|
case QDialogButtonBox::ApplyRole:{
|
||||||
|
clear_table(&gps_location_table);
|
||||||
|
QByteArray url = tr("Webservice").toLocal8Bit();
|
||||||
|
parse_xml_buffer(url.data(), downloadedData.data(), downloadedData.length(), &gps_location_table, NULL, NULL);
|
||||||
|
|
||||||
|
/* now merge the data in the gps_location table into the dive_table */
|
||||||
|
if (merge_locations_into_dives()) {
|
||||||
|
mark_divelist_changed(TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* store last entered uid in config */
|
||||||
|
QSettings s;
|
||||||
|
s.setValue("subsurface_webservice_uid", ui.userID->text().toUpper());
|
||||||
|
s.sync();
|
||||||
|
hide();
|
||||||
|
close();
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QDialogButtonBox::RejectRole:
|
||||||
|
// we may want to clean up after ourselves
|
||||||
|
// reply->deleteLater();
|
||||||
|
reply = NULL;
|
||||||
|
resetState();
|
||||||
|
break;
|
||||||
|
case QDialogButtonBox::HelpRole:
|
||||||
|
QDesktopServices::openUrl(QUrl("http://api.hohndel.org"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SubsurfaceWebServices::startDownload()
|
||||||
|
{
|
||||||
|
QUrl url("http://api.hohndel.org/api/dive/get/");
|
||||||
|
url.addQueryItem("login", ui.userID->text().toUpper());
|
||||||
|
|
||||||
|
QNetworkRequest request;
|
||||||
|
request.setUrl(url);
|
||||||
|
request.setRawHeader("Accept", "text/xml");
|
||||||
|
reply = manager()->get(request);
|
||||||
|
ui.status->setText(tr("Connecting..."));
|
||||||
|
ui.progressBar->setRange(0,0); // this makes the progressbar do an 'infinite spin'
|
||||||
|
ui.download->setEnabled(false);
|
||||||
|
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
|
||||||
|
connectSignalsForDownload(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SubsurfaceWebServices::downloadFinished()
|
||||||
|
{
|
||||||
|
if (!reply)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ui.progressBar->setRange(0,1);
|
||||||
|
downloadedData = reply->readAll();
|
||||||
|
|
||||||
|
ui.download->setEnabled(true);
|
||||||
|
ui.status->setText(tr("Download finished"));
|
||||||
|
|
||||||
|
uint resultCode = download_dialog_parse_response(downloadedData);
|
||||||
|
setStatusText(resultCode);
|
||||||
|
if (resultCode == DD_STATUS_OK){
|
||||||
|
ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
reply = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SubsurfaceWebServices::downloadError(QNetworkReply::NetworkError)
|
||||||
|
{
|
||||||
|
resetState();
|
||||||
|
ui.status->setText(tr("Download error: %1").arg(reply->errorString()));
|
||||||
|
reply->deleteLater();
|
||||||
|
reply = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SubsurfaceWebServices::setStatusText(int status)
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
switch (status) {
|
||||||
|
case DD_STATUS_ERROR_CONNECT: text = tr("Connection Error: "); break;
|
||||||
|
case DD_STATUS_ERROR_ID: text = tr("Invalid user identifier!"); break;
|
||||||
|
case DD_STATUS_ERROR_PARSE: text = tr("Cannot parse response!"); break;
|
||||||
|
case DD_STATUS_OK: text = tr("Download Success!"); break;
|
||||||
|
}
|
||||||
|
ui.status->setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* requires that there is a <download> or <error> tag under the <root> tag */
|
||||||
|
void SubsurfaceWebServices::download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status)
|
||||||
|
{
|
||||||
|
xmlNodePtr cur_node;
|
||||||
|
for (cur_node = node; cur_node; cur_node = cur_node->next) {
|
||||||
|
if ((!strcmp((const char *)cur_node->name, (const char *)"download")) &&
|
||||||
|
(!strcmp((const char *)xmlNodeGetContent(cur_node), (const char *)"ok"))) {
|
||||||
|
*download_status = DD_STATUS_OK;
|
||||||
|
return;
|
||||||
|
} else if (!strcmp((const char *)cur_node->name, (const char *)"error")) {
|
||||||
|
*download_status = DD_STATUS_ERROR_ID;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int SubsurfaceWebServices::download_dialog_parse_response(const QByteArray& xml)
|
||||||
|
{
|
||||||
|
xmlNodePtr root;
|
||||||
|
xmlDocPtr doc = xmlParseMemory(xml.data(), xml.length());
|
||||||
|
unsigned int status = DD_STATUS_ERROR_PARSE;
|
||||||
|
|
||||||
|
if (!doc)
|
||||||
|
return DD_STATUS_ERROR_PARSE;
|
||||||
|
root = xmlDocGetRootElement(doc);
|
||||||
|
if (!root) {
|
||||||
|
status = DD_STATUS_ERROR_PARSE;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
if (root->children)
|
||||||
|
download_dialog_traverse_xml(root->children, &status);
|
||||||
|
end:
|
||||||
|
xmlFreeDoc(doc);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
// #
|
// #
|
||||||
// #
|
// #
|
||||||
// # Divelogs DE Web Service Implementation.
|
// # Divelogs DE Web Service Implementation.
|
||||||
// #
|
// #
|
||||||
// #
|
// #
|
||||||
|
|
||||||
|
struct DiveListResult
|
||||||
|
{
|
||||||
|
QString errorCondition;
|
||||||
|
QString errorDetails;
|
||||||
|
QByteArray idList; // comma-separated, suitable to be sent in the fetch request
|
||||||
|
int idCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
static DiveListResult parseDiveLogsDeDiveList(const QByteArray &xmlData)
|
||||||
|
{
|
||||||
|
/* XML format seems to be:
|
||||||
|
* <DiveDateReader version="1.0">
|
||||||
|
* <DiveDates>
|
||||||
|
* <date diveLogsId="nnn" lastModified="YYYY-MM-DD hh:mm:ss">DD.MM.YYYY hh:mm</date>
|
||||||
|
* [repeat <date></date>]
|
||||||
|
* </DiveDates>
|
||||||
|
* </DiveDateReader>
|
||||||
|
*/
|
||||||
|
QXmlStreamReader reader(xmlData);
|
||||||
|
const QString invalidXmlError = DivelogsDeWebServices::tr("Invalid response from server");
|
||||||
|
bool seenDiveDates = false;
|
||||||
|
DiveListResult result;
|
||||||
|
result.idCount = 0;
|
||||||
|
|
||||||
|
if (reader.readNextStartElement() && reader.name() != "DiveDateReader") {
|
||||||
|
result.errorCondition = invalidXmlError;
|
||||||
|
result.errorDetails =
|
||||||
|
DivelogsDeWebServices::tr("Expected XML tag 'DiveDateReader', got instead '%1")
|
||||||
|
.arg(reader.name().toString());
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (reader.readNextStartElement()) {
|
||||||
|
if (reader.name() != "DiveDates") {
|
||||||
|
if (reader.name() == "Login") {
|
||||||
|
QString status = reader.readElementText();
|
||||||
|
// qDebug() << "Login status:" << status;
|
||||||
|
|
||||||
|
// Note: there has to be a better way to determine a successful login...
|
||||||
|
if (status == "failed") {
|
||||||
|
result.errorCondition = "Login failed";
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// qDebug() << "Skipping" << reader.name();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// process <DiveDates>
|
||||||
|
seenDiveDates = true;
|
||||||
|
while (reader.readNextStartElement()) {
|
||||||
|
if (reader.name() != "date") {
|
||||||
|
// qDebug() << "Skipping" << reader.name();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QStringRef id = reader.attributes().value("divelogsId");
|
||||||
|
// qDebug() << "Found" << reader.name() << "with id =" << id;
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
result.idList += id.toLatin1();
|
||||||
|
result.idList += ',';
|
||||||
|
++result.idCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.skipCurrentElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// chop the ending comma, if any
|
||||||
|
result.idList.chop(1);
|
||||||
|
|
||||||
|
if (!seenDiveDates) {
|
||||||
|
result.errorCondition = invalidXmlError;
|
||||||
|
result.errorDetails = DivelogsDeWebServices::tr("Expected XML tag 'DiveDates' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
if (reader.hasError()) {
|
||||||
|
// if there was an XML error, overwrite the result or other error conditions
|
||||||
|
result.errorCondition = invalidXmlError;
|
||||||
|
result.errorDetails = DivelogsDeWebServices::tr("Malformed XML response. Line %1: %2")
|
||||||
|
.arg(reader.lineNumber()).arg(reader.errorString());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
DivelogsDeWebServices* DivelogsDeWebServices::instance()
|
DivelogsDeWebServices* DivelogsDeWebServices::instance()
|
||||||
{
|
{
|
||||||
static DivelogsDeWebServices *self = new DivelogsDeWebServices(mainWindow());
|
static DivelogsDeWebServices *self = new DivelogsDeWebServices(mainWindow());
|
||||||
|
@ -275,24 +438,217 @@ DivelogsDeWebServices* DivelogsDeWebServices::instance()
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DivelogsDeWebServices::downloadDives()
|
||||||
|
{
|
||||||
|
hideUpload();
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DivelogsDeWebServices::uploadDives(QIODevice *dldContent)
|
||||||
|
{
|
||||||
|
QHttpMultiPart mp(QHttpMultiPart::FormDataType);
|
||||||
|
QHttpPart part;
|
||||||
|
part.setRawHeader("Content-Disposition", "form-data; name=\"userfile\"");
|
||||||
|
part.setBodyDevice(dldContent);
|
||||||
|
mp.append(part);
|
||||||
|
|
||||||
|
multipart = ∓
|
||||||
|
hideDownload();
|
||||||
|
exec();
|
||||||
|
multipart = NULL;
|
||||||
|
|
||||||
|
delete reply; // we need to ensure it has stopped using our QHttpMultiPart
|
||||||
|
}
|
||||||
|
|
||||||
DivelogsDeWebServices::DivelogsDeWebServices(QWidget* parent, Qt::WindowFlags f): WebServices(parent, f)
|
DivelogsDeWebServices::DivelogsDeWebServices(QWidget* parent, Qt::WindowFlags f): WebServices(parent, f)
|
||||||
{
|
{
|
||||||
|
QSettings s;
|
||||||
|
ui.userID->setText(s.value("divelogde_user").toString());
|
||||||
|
ui.password->setText(s.value("divelogde_pass").toString());
|
||||||
|
hideUpload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DivelogsDeWebServices::startUpload()
|
void DivelogsDeWebServices::startUpload()
|
||||||
{
|
{
|
||||||
|
ui.status->setText(tr("Uploading dive list..."));
|
||||||
|
ui.progressBar->setRange(0,0); // this makes the progressbar do an 'infinite spin'
|
||||||
|
ui.upload->setEnabled(false);
|
||||||
|
ui.userID->setEnabled(false);
|
||||||
|
ui.password->setEnabled(false);
|
||||||
|
|
||||||
|
QNetworkRequest request;
|
||||||
|
request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php"));
|
||||||
|
request.setRawHeader("Accept", "text/xml, application/xml");
|
||||||
|
|
||||||
|
QHttpPart part;
|
||||||
|
part.setRawHeader("Content-Disposition", "form-data; name=\"user\"");
|
||||||
|
part.setBody(ui.userID->text().toUtf8());
|
||||||
|
multipart->append(part);
|
||||||
|
|
||||||
|
part.setRawHeader("Content-Disposition", "form-data; name=\"pass\"");
|
||||||
|
part.setBody(ui.password->text().toUtf8());
|
||||||
|
multipart->append(part);
|
||||||
|
|
||||||
|
reply = manager()->post(request, multipart);
|
||||||
|
connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished()));
|
||||||
|
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this,
|
||||||
|
SLOT(uploadError(QNetworkReply::NetworkError)));
|
||||||
|
connect(reply, SIGNAL(uploadProgress(qint64,qint64)), this,
|
||||||
|
SLOT(updateProgress(qint64,qint64)));
|
||||||
|
|
||||||
|
timeout.start(30000); // 30s
|
||||||
}
|
}
|
||||||
|
|
||||||
void DivelogsDeWebServices::startDownload()
|
void DivelogsDeWebServices::startDownload()
|
||||||
{
|
{
|
||||||
|
ui.status->setText(tr("Downloading dive list..."));
|
||||||
|
ui.progressBar->setRange(0,0); // this makes the progressbar do an 'infinite spin'
|
||||||
|
ui.download->setEnabled(false);
|
||||||
|
ui.userID->setEnabled(false);
|
||||||
|
ui.password->setEnabled(false);
|
||||||
|
|
||||||
|
QNetworkRequest request;
|
||||||
|
request.setUrl(QUrl("https://divelogs.de/xml_available_dives.php"));
|
||||||
|
request.setRawHeader("Accept", "text/xml, application/xml");
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5,0,0)
|
||||||
|
QUrl body;
|
||||||
|
body.addQueryItem("user", ui.userID->text());
|
||||||
|
body.addQueryItem("pass", ui.password->text());
|
||||||
|
|
||||||
|
reply = manager()->post(request, body.encodedQuery());
|
||||||
|
#else
|
||||||
|
QUrlQuery body;
|
||||||
|
body.addQueryItem("user", ui.userID->text());
|
||||||
|
body.addQueryItem("pass", ui.password->text());
|
||||||
|
|
||||||
|
reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1())
|
||||||
|
#endif
|
||||||
|
connect(reply, SIGNAL(finished()), this, SLOT(listDownloadFinished()));
|
||||||
|
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
|
||||||
|
this, SLOT(downloadError(QNetworkReply::NetworkError)));
|
||||||
|
|
||||||
|
timeout.start(30000); // 30s
|
||||||
|
}
|
||||||
|
|
||||||
|
void DivelogsDeWebServices::listDownloadFinished()
|
||||||
|
{
|
||||||
|
if (!reply)
|
||||||
|
return;
|
||||||
|
QByteArray xmlData = reply->readAll();
|
||||||
|
reply->deleteLater();
|
||||||
|
reply = NULL;
|
||||||
|
|
||||||
|
// parse the XML data we downloaded
|
||||||
|
DiveListResult diveList = parseDiveLogsDeDiveList(xmlData);
|
||||||
|
if (!diveList.errorCondition.isEmpty()) {
|
||||||
|
// error condition
|
||||||
|
resetState();
|
||||||
|
ui.status->setText(diveList.errorCondition);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.status->setText(tr("Downloading %1 dives...").arg(diveList.idCount));
|
||||||
|
|
||||||
|
QNetworkRequest request;
|
||||||
|
// request.setUrl(QUrl("https://divelogs.de/DivelogsDirectExport.php"));
|
||||||
|
request.setUrl(QUrl("http://divelogs.de/DivelogsDirectExport.php"));
|
||||||
|
request.setRawHeader("Accept", "application/zip, */*");
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5,0,0)
|
||||||
|
QUrl body;
|
||||||
|
body.addQueryItem("user", ui.userID->text());
|
||||||
|
body.addQueryItem("pass", ui.password->text());
|
||||||
|
body.addQueryItem("ids", diveList.idList);
|
||||||
|
|
||||||
|
reply = manager()->post(request, body.encodedQuery());
|
||||||
|
#else
|
||||||
|
QUrlQuery body;
|
||||||
|
body.addQueryItem("user", ui.userID->text());
|
||||||
|
body.addQueryItem("pass", ui.password->text());
|
||||||
|
body.addQueryItem("ids", diveList.idList);
|
||||||
|
|
||||||
|
reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1())
|
||||||
|
#endif
|
||||||
|
|
||||||
|
connect(reply, SIGNAL(readyRead()), this, SLOT(saveToZipFile()));
|
||||||
|
connectSignalsForDownload(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DivelogsDeWebServices::saveToZipFile()
|
||||||
|
{
|
||||||
|
if (!zipFile.isOpen()) {
|
||||||
|
zipFile.setFileTemplate(QDir::tempPath() + "/import-XXXXXX.dld");
|
||||||
|
zipFile.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile.write(reply->readAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
void DivelogsDeWebServices::downloadFinished()
|
void DivelogsDeWebServices::downloadFinished()
|
||||||
{
|
{
|
||||||
|
if (!reply)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ui.download->setEnabled(true);
|
||||||
|
ui.status->setText(tr("Download finished - %1").arg(reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()));
|
||||||
|
reply->deleteLater();
|
||||||
|
reply = NULL;
|
||||||
|
|
||||||
|
int errorcode;
|
||||||
|
zipFile.seek(0);
|
||||||
|
#ifdef Q_OS_UNIX
|
||||||
|
int duppedfd = dup(zipFile.handle());
|
||||||
|
struct zip *zip = zip_fdopen(duppedfd, 0, &errorcode);
|
||||||
|
if (!zip)
|
||||||
|
::close(duppedfd);
|
||||||
|
#else
|
||||||
|
struct zip *zip = zip_open(zipFile.fileName().toLocal8Bit().data(), 0, &errorcode);
|
||||||
|
#endif
|
||||||
|
if (!zip) {
|
||||||
|
char buf[512];
|
||||||
|
zip_error_to_str(buf, sizeof(buf), errorcode, errno);
|
||||||
|
QMessageBox::critical(this, tr("Corrupted download"),
|
||||||
|
tr("The archive could not be opened:\n%1").arg(QString::fromLocal8Bit(buf)));
|
||||||
|
zipFile.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 entries = zip_get_num_entries(zip, 0);
|
||||||
|
for (quint64 i = 0; i < entries; ++i) {
|
||||||
|
struct zip_file *zip_file = zip_fopen_index(zip, i, 0);
|
||||||
|
if (!zip_file) {
|
||||||
|
QMessageBox::critical(this, tr("Corrupted download"),
|
||||||
|
tr("The archive contains corrupt data:\n%1").arg(QString::fromLocal8Bit(zip_strerror(zip))));
|
||||||
|
goto close_zip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ### FIXME: What do I do with this?
|
||||||
|
|
||||||
|
zip_fclose(zip_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
close_zip:
|
||||||
|
zip_close(zip);
|
||||||
|
zipFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DivelogsDeWebServices::uploadFinished()
|
||||||
|
{
|
||||||
|
if (!reply)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ui.progressBar->setRange(0,1);
|
||||||
|
ui.upload->setEnabled(true);
|
||||||
|
ui.status->setText(tr("Upload finished"));
|
||||||
|
|
||||||
|
// check what the server sent us: it might contain
|
||||||
|
// an error condition, such as a failed login
|
||||||
|
QByteArray xmlData = reply->readAll();
|
||||||
|
|
||||||
|
// ### FIXME: what's the format?
|
||||||
}
|
}
|
||||||
|
|
||||||
void DivelogsDeWebServices::setStatusText(int status)
|
void DivelogsDeWebServices::setStatusText(int status)
|
||||||
|
@ -300,12 +656,21 @@ void DivelogsDeWebServices::setStatusText(int status)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DivelogsDeWebServices::downloadError(QNetworkReply::NetworkError error)
|
void DivelogsDeWebServices::downloadError(QNetworkReply::NetworkError)
|
||||||
{
|
{
|
||||||
|
resetState();
|
||||||
|
ui.status->setText(tr("Download error: %1").arg(reply->errorString()));
|
||||||
|
reply->deleteLater();
|
||||||
|
reply = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DivelogsDeWebServices::uploadError(QNetworkReply::NetworkError error)
|
||||||
|
{
|
||||||
|
downloadError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DivelogsDeWebServices::buttonClicked(QAbstractButton* button)
|
void DivelogsDeWebServices::buttonClicked(QAbstractButton* button)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,29 +3,43 @@
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
#include <QTimer>
|
||||||
#include <libxml/tree.h>
|
#include <libxml/tree.h>
|
||||||
|
|
||||||
#include "ui_webservices.h"
|
#include "ui_webservices.h"
|
||||||
|
|
||||||
class QAbstractButton;
|
class QAbstractButton;
|
||||||
class QNetworkReply;
|
class QNetworkReply;
|
||||||
|
class QHttpMultiPart;
|
||||||
|
|
||||||
class WebServices : public QDialog{
|
class WebServices : public QDialog{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit WebServices(QWidget* parent = 0, Qt::WindowFlags f = 0);
|
explicit WebServices(QWidget* parent = 0, Qt::WindowFlags f = 0);
|
||||||
void hidePassword();
|
void hidePassword();
|
||||||
void hideUpload();
|
void hideUpload();
|
||||||
|
void hideDownload();
|
||||||
|
|
||||||
|
static QNetworkAccessManager *manager();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
virtual void startDownload() = 0;
|
virtual void startDownload() = 0;
|
||||||
virtual void startUpload() = 0;
|
virtual void startUpload() = 0;
|
||||||
virtual void buttonClicked(QAbstractButton* button) = 0;
|
virtual void buttonClicked(QAbstractButton* button) = 0;
|
||||||
|
virtual void downloadTimedOut();
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
void updateProgress(qint64 current, qint64 total);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void resetState();
|
||||||
|
void connectSignalsForDownload(QNetworkReply *reply);
|
||||||
|
void connectSignalsForUpload();
|
||||||
|
|
||||||
Ui::WebServices ui;
|
Ui::WebServices ui;
|
||||||
QNetworkReply *reply;
|
QNetworkReply *reply;
|
||||||
QNetworkAccessManager *manager;
|
QTimer timeout;
|
||||||
QByteArray downloadedData;
|
QByteArray downloadedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +55,7 @@ private slots:
|
||||||
void downloadError(QNetworkReply::NetworkError error);
|
void downloadError(QNetworkReply::NetworkError error);
|
||||||
void startUpload(){} /*no op*/
|
void startUpload(){} /*no op*/
|
||||||
private:
|
private:
|
||||||
explicit SubsurfaceWebServices(QWidget* parent = 0, Qt::WindowFlags f = 0);
|
explicit SubsurfaceWebServices(QWidget* parent = 0, Qt::WindowFlags f = 0);
|
||||||
void setStatusText(int status);
|
void setStatusText(int status);
|
||||||
void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status);
|
void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status);
|
||||||
unsigned int download_dialog_parse_response(const QByteArray& length);
|
unsigned int download_dialog_parse_response(const QByteArray& length);
|
||||||
|
@ -51,18 +65,27 @@ class DivelogsDeWebServices : public WebServices {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
static DivelogsDeWebServices * instance();
|
static DivelogsDeWebServices * instance();
|
||||||
|
void downloadDives();
|
||||||
|
void uploadDives(QIODevice *dldContent);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void startDownload();
|
void startDownload();
|
||||||
void buttonClicked(QAbstractButton* button);
|
void buttonClicked(QAbstractButton* button);
|
||||||
|
void saveToZipFile();
|
||||||
|
void listDownloadFinished();
|
||||||
void downloadFinished();
|
void downloadFinished();
|
||||||
|
void uploadFinished();
|
||||||
void downloadError(QNetworkReply::NetworkError error);
|
void downloadError(QNetworkReply::NetworkError error);
|
||||||
void startUpload();
|
void uploadError(QNetworkReply::NetworkError error);
|
||||||
|
void startUpload();
|
||||||
private:
|
private:
|
||||||
explicit DivelogsDeWebServices (QWidget* parent = 0, Qt::WindowFlags f = 0);
|
explicit DivelogsDeWebServices (QWidget* parent = 0, Qt::WindowFlags f = 0);
|
||||||
void setStatusText(int status);
|
void setStatusText(int status);
|
||||||
void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status);
|
void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status);
|
||||||
unsigned int download_dialog_parse_response(const QByteArray& length);
|
unsigned int download_dialog_parse_response(const QByteArray& length);
|
||||||
|
|
||||||
|
QHttpMultiPart *multipart;
|
||||||
|
QTemporaryFile zipFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -69,6 +69,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="textInteractionFlags">
|
||||||
|
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
|
|
Loading…
Add table
Reference in a new issue