diff --git a/components/StandardButton.qml b/components/StandardButton.qml index 77cd2dad..5b4e2279 100644 --- a/components/StandardButton.qml +++ b/components/StandardButton.qml @@ -33,11 +33,17 @@ import "../components" as MoneroComponents Item { id: button + property bool primary: true property string rightIcon: "" property string rightIconInactive: "" - property string textColor: button.enabled? MoneroComponents.Style.buttonTextColor: MoneroComponents.Style.buttonTextColorDisabled + property color textColor: !button.enabled + ? MoneroComponents.Style.buttonTextColorDisabled + : primary + ? MoneroComponents.Style.buttonTextColor + : MoneroComponents.Style.buttonSecondaryTextColor; property bool small: false property alias text: label.text + property alias fontBold: label.font.bold property int fontSize: { if(small) return 14; else return 16; @@ -70,7 +76,9 @@ Item { when: buttonArea.containsMouse || button.focus PropertyChanges { target: buttonRect - color: MoneroComponents.Style.buttonBackgroundColorHover + color: primary + ? MoneroComponents.Style.buttonBackgroundColorHover + : MoneroComponents.Style.buttonSecondaryBackgroundColorHover } }, State { @@ -78,7 +86,9 @@ Item { when: button.enabled PropertyChanges { target: buttonRect - color: MoneroComponents.Style.buttonBackgroundColor + color: primary + ? MoneroComponents.Style.buttonBackgroundColor + : MoneroComponents.Style.buttonSecondaryBackgroundColor } }, State { diff --git a/components/Style.qml b/components/Style.qml index fc840244..7b9f31ee 100644 --- a/components/Style.qml +++ b/components/Style.qml @@ -43,6 +43,9 @@ QtObject { property string buttonInlineBackgroundColor: blackTheme ? _b_buttonInlineBackgroundColor : _w_buttonInlineBackgroundColor property string buttonTextColor: blackTheme ? _b_buttonTextColor : _w_buttonTextColor property string buttonTextColorDisabled: blackTheme ? _b_buttonTextColorDisabled : _w_buttonTextColorDisabled + property string buttonSecondaryBackgroundColor: "#d9d9d9" + property string buttonSecondaryBackgroundColorHover: "#a6a6a6" + property string buttonSecondaryTextColor: "#4d4d4d" property string dividerColor: blackTheme ? _b_dividerColor : _w_dividerColor property real dividerOpacity: blackTheme ? _b_dividerOpacity : _w_dividerOpacity diff --git a/components/UpdateDialog.qml b/components/UpdateDialog.qml new file mode 100644 index 00000000..74c62516 --- /dev/null +++ b/components/UpdateDialog.qml @@ -0,0 +1,200 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 + +import moneroComponents.Downloader 1.0 + +import "../components" as MoneroComponents + +Popup { + id: updateDialog + + property bool allowed: true + property string error: "" + property string filename: "" + property double progress: url && downloader.total > 0 ? downloader.loaded * 100 / downloader.total : 0 + property bool active: false + property string url: "" + property bool valid: false + property string version: "" + + background: Rectangle { + border.color: MoneroComponents.Style.appWindowBorderColor + border.width: 1 + color: MoneroComponents.Style.middlePanelBackgroundColor + } + closePolicy: Popup.NoAutoClose + padding: 20 + visible: active && allowed + + function show(version, url) { + updateDialog.error = ""; + updateDialog.url = url; + updateDialog.valid = false; + updateDialog.version = version; + updateDialog.active = true; + } + + ColumnLayout { + id: mainLayout + spacing: updateDialog.padding + + Text { + color: MoneroComponents.Style.defaultFontColor + font.bold: true + font.family: MoneroComponents.Style.fontRegular.name + font.pixelSize: 18 + text: qsTr("New Monero version v%1 is available.").arg(updateDialog.version) + } + + Text { + id: errorText + color: "red" + font.family: MoneroComponents.Style.fontRegular.name + font.pixelSize: 18 + text: updateDialog.error + visible: text + } + + Text { + id: statusText + color: MoneroComponents.Style.defaultFontColor + font.family: MoneroComponents.Style.fontRegular.name + font.pixelSize: 18 + visible: !errorText.visible + + text: { + if (!updateDialog.url) { + return qsTr("Please visit getmonero.org for details") + translationManager.emptyString; + } + if (downloader.active) { + return "%1 (%2%)" + .arg(qsTr("Downloading")) + .arg(updateDialog.progress.toFixed(1)) + + translationManager.emptyString; + } + if (updateDialog.valid) { + return qsTr("Download finished") + translationManager.emptyString; + } + return qsTr("Do you want to download new version?") + translationManager.emptyString; + } + } + + Rectangle { + id: progressBar + color: MoneroComponents.Style.lightGreyFontColor + height: 3 + Layout.fillWidth: true + visible: updateDialog.valid || downloader.active + + Rectangle { + color: MoneroComponents.Style.buttonBackgroundColor + height: parent.height + width: parent.width * updateDialog.progress / 100 + } + } + + RowLayout { + Layout.alignment: Qt.AlignRight + spacing: parent.spacing + + MoneroComponents.StandardButton { + id: cancelButton + fontBold: false + primary: !updateDialog.url + text: { + if (!updateDialog.url) { + return qsTr("Ok") + translationManager.emptyString; + } + if (updateDialog.valid || downloader.active || errorText.visible) { + return qsTr("Cancel") + translationManager.emptyString; + } + return qsTr("Download later") + translationManager.emptyString; + } + + onClicked: { + downloader.cancel(); + updateDialog.active = false; + } + } + + MoneroComponents.StandardButton { + id: downloadButton + KeyNavigation.tab: cancelButton + fontBold: false + text: (updateDialog.error ? qsTr("Retry") : qsTr("Download")) + translationManager.emptyString + visible: updateDialog.url && !updateDialog.valid && !downloader.active + + onClicked: { + updateDialog.error = ""; + updateDialog.filename = updateDialog.url.replace(/^.*\//, ''); + const downloadingStarted = downloader.get(updateDialog.url, function(error) { + if (error) { + updateDialog.error = qsTr("Download failed") + translationManager.emptyString; + } else { + updateDialog.valid = true; + } + }); + if (!downloadingStarted) { + updateDialog.error = qsTr("Failed to start download") + translationManager.emptyString; + } + } + } + + MoneroComponents.StandardButton { + id: saveButton + KeyNavigation.tab: cancelButton + fontBold: false + onClicked: { + const fullPath = oshelper.openSaveFileDialog( + qsTr("Save as") + translationManager.emptyString, + oshelper.downloadLocation(), + updateDialog.filename); + if (!fullPath) { + return; + } + if (downloader.saveToFile(fullPath)) { + cancelButton.clicked(); + oshelper.openContainingFolder(fullPath); + } else { + updateDialog.error = qsTr("Save operation failed") + translationManager.emptyString; + } + } + text: qsTr("Save to file") + translationManager.emptyString + visible: updateDialog.valid + } + } + } + + Downloader { + id: downloader + } +} diff --git a/main.qml b/main.qml index 88e607b0..5c171400 100644 --- a/main.qml +++ b/main.qml @@ -1135,7 +1135,7 @@ ApplicationWindow { triggeredOnStart: false } - function fiatApiParseTicker(url, resp, currency){ + function fiatApiParseTicker(url, resp, currency){ // parse & validate incoming JSON if(url.startsWith("https://api.kraken.com/0/")){ if(resp.hasOwnProperty("error") && resp.error.length > 0 || !resp.hasOwnProperty("result")){ @@ -1181,7 +1181,7 @@ ApplicationWindow { } function fiatApiJsonReceived(url, resp, error) { - if (error) { + if (error) { appWindow.fiatApiError(error); return; } @@ -1436,6 +1436,14 @@ ApplicationWindow { } } + MoneroComponents.UpdateDialog { + id: updateDialog + + allowed: !passwordDialog.visible && !inputDialog.visible && !splash.visible + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + } + // Choose blockchain folder FileDialog { id: blockchainFileDialog @@ -1691,7 +1699,7 @@ ApplicationWindow { anchors.fill: blurredArea source: blurredArea radius: 64 - visible: passwordDialog.visible || inputDialog.visible || splash.visible + visible: passwordDialog.visible || inputDialog.visible || splash.visible || updateDialog.visible } @@ -1798,11 +1806,6 @@ ApplicationWindow { color: "#FFFFFF" } } - - Notifier { - visible:false - id: notifier - } } function toggleLanguageView(){ @@ -1981,16 +1984,7 @@ ApplicationWindow { print("Update found: " + update) var parts = update.split("|") if (parts.length == 4) { - var version = parts[0] - var hash = parts[1] - var user_url = parts[2] - var msg = qsTr("New version of Monero v%1 is available.").arg(version) - if (isMac || isWindows || isLinux) { - msg += "

%1:
%2

%3:
%4".arg(qsTr("Download")).arg(user_url).arg(qsTr("SHA256 Hash")).arg(hash) + translationManager.emptyString - } else { - msg += " " + qsTr("Check out getmonero.org") + translationManager.emptyString - } - notifier.show(msg) + updateDialog.show(parts[0], isMac || isWindows || isLinux ? parts[3] : ""); } else { print("Failed to parse update spec") } @@ -2102,6 +2096,11 @@ ApplicationWindow { blackColor: "black" whiteColor: "white" } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + } } // borders on white theme + linux diff --git a/monero-wallet-gui.pro b/monero-wallet-gui.pro index 04242c66..31e64af1 100644 --- a/monero-wallet-gui.pro +++ b/monero-wallet-gui.pro @@ -76,6 +76,7 @@ HEADERS += \ src/libwalletqt/UnsignedTransaction.h \ src/main/Logger.h \ src/main/MainApp.h \ + src/qt/downloader.h \ src/qt/FutureScheduler.h \ src/qt/ipc.h \ src/qt/KeysFiles.h \ @@ -112,6 +113,7 @@ SOURCES += src/main/main.cpp \ src/libwalletqt/UnsignedTransaction.cpp \ src/main/Logger.cpp \ src/main/MainApp.cpp \ + src/qt/downloader.cpp \ src/qt/FutureScheduler.cpp \ src/qt/ipc.cpp \ src/qt/KeysFiles.cpp \ diff --git a/qml.qrc b/qml.qrc index 04c447c0..4b6be793 100644 --- a/qml.qrc +++ b/qml.qrc @@ -5,6 +5,7 @@ MiddlePanel.qml components/Label.qml components/SettingsListItem.qml + components/UpdateDialog.qml images/whatIsIcon.png images/whatIsIcon@2x.png images/lockIcon.png @@ -100,7 +101,6 @@ components/DaemonManagerDialog.qml version.js components/QRCodeScanner.qml - components/Notifier.qml components/TextBlock.qml components/RemoteNodeEdit.qml pages/Keys.qml diff --git a/src/main/main.cpp b/src/main/main.cpp index c6c0ea52..c564c59f 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -61,6 +61,7 @@ #include "wallet/api/wallet2_api.h" #include "Logger.h" #include "MainApp.h" +#include "qt/downloader.h" #include "qt/ipc.h" #include "qt/network.h" #include "qt/utils.h" @@ -295,6 +296,7 @@ int main(int argc, char *argv[]) // registering types for QML qmlRegisterType("moneroComponents.Clipboard", 1, 0, "Clipboard"); + qmlRegisterType("moneroComponents.Downloader", 1, 0, "Downloader"); // Temporary Qt.labs.settings replacement qmlRegisterType("moneroComponents.Settings", 1, 0, "MoneroSettings"); diff --git a/src/main/oshelper.cpp b/src/main/oshelper.cpp index 4402db6d..75c668f5 100644 --- a/src/main/oshelper.cpp +++ b/src/main/oshelper.cpp @@ -27,6 +27,8 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "oshelper.h" +#include +#include #include #include #include @@ -82,6 +84,11 @@ OSHelper::OSHelper(QObject *parent) : QObject(parent) } +QString OSHelper::downloadLocation() const +{ + return QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); +} + bool OSHelper::openContainingFolder(const QString &filePath) const { #if defined(Q_OS_WIN) @@ -105,6 +112,12 @@ bool OSHelper::openContainingFolder(const QString &filePath) const return QDesktopServices::openUrl(url); } +QString OSHelper::openSaveFileDialog(const QString &title, const QString &folder, const QString &filename) const +{ + const QString hint = (folder.isEmpty() ? "" : folder + QDir::separator()) + filename; + return QFileDialog::getSaveFileName(nullptr, title, hint); +} + QString OSHelper::temporaryFilename() const { QString tempFileName; diff --git a/src/main/oshelper.h b/src/main/oshelper.h index 72f284af..ba498554 100644 --- a/src/main/oshelper.h +++ b/src/main/oshelper.h @@ -39,7 +39,9 @@ class OSHelper : public QObject public: explicit OSHelper(QObject *parent = 0); + Q_INVOKABLE QString downloadLocation() const; Q_INVOKABLE bool openContainingFolder(const QString &filePath) const; + Q_INVOKABLE QString openSaveFileDialog(const QString &title, const QString &folder, const QString &filename) const; Q_INVOKABLE QString temporaryFilename() const; Q_INVOKABLE QString temporaryPath() const; Q_INVOKABLE bool removeTemporaryWallet(const QString &walletName) const; diff --git a/src/qt/downloader.cpp b/src/qt/downloader.cpp new file mode 100644 index 00000000..afd2049e --- /dev/null +++ b/src/qt/downloader.cpp @@ -0,0 +1,207 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "downloader.h" + +#include +#include + +namespace +{ + +class DownloaderStateGuard +{ +public: + DownloaderStateGuard(bool &active, QReadWriteLock &mutex, std::function onActiveChanged) + : m_active(active) + , m_acquired(false) + , m_mutex(mutex) + , m_onActiveChanged(std::move(onActiveChanged)) + { + { + QWriteLocker locker(&m_mutex); + + if (m_active) + { + return; + } + + m_active = true; + } + m_onActiveChanged(); + + m_acquired = true; + } + + ~DownloaderStateGuard() + { + if (!m_acquired) + { + return; + } + + { + QWriteLocker locker(&m_mutex); + + m_active = false; + } + m_onActiveChanged(); + } + + bool acquired() const + { + return m_acquired; + } + +private: + bool &m_active; + bool m_acquired; + QReadWriteLock &m_mutex; + std::function m_onActiveChanged; +}; + +} // namespace + +Downloader::Downloader(QObject *parent) + : QObject(parent) + , m_active(false) + , m_httpClient(new HttpClient()) + , m_network(this) + , m_scheduler(this) +{ + QObject::connect(m_httpClient.get(), SIGNAL(contentLengthChanged()), this, SIGNAL(totalChanged())); + QObject::connect(m_httpClient.get(), SIGNAL(receivedChanged()), this, SIGNAL(loadedChanged())); +} + +Downloader::~Downloader() +{ + cancel(); +} + +void Downloader::cancel() +{ + m_httpClient->cancel(); + + QWriteLocker locker(&m_mutex); + + m_contents.clear(); +} + +bool Downloader::get(const QString &url, const QJSValue &callback) +{ + auto future = m_scheduler.run( + [this, url]() { + DownloaderStateGuard stateGuard(m_active, m_mutex, [this]() { + emit activeChanged(); + }); + if (!stateGuard.acquired()) + { + return QJSValueList({"downloading is already running"}); + } + + { + QWriteLocker locker(&m_mutex); + + m_contents.clear(); + } + + std::string response; + { + QString error; + auto task = m_scheduler.run([this, &error, &response, &url] { + error = m_network.get(m_httpClient, url, response); + }); + if (!task.first) + { + return QJSValueList({"failed to start downloading task"}); + } + task.second.waitForFinished(); + + if (!error.isEmpty()) + { + return QJSValueList({error}); + } + } + + if (response.empty()) + { + return QJSValueList({"empty response"}); + } + + { + QWriteLocker locker(&m_mutex); + + m_contents = std::move(response); + } + + return QJSValueList({}); + }, + callback); + + return future.first; +} + +bool Downloader::saveToFile(const QString &path) const +{ + QWriteLocker locker(&m_mutex); + + if (m_active || m_contents.empty()) + { + return false; + } + + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) + { + return false; + } + + if (static_cast(file.write(m_contents.data(), m_contents.size())) != m_contents.size()) + { + return false; + } + + return true; +} + +bool Downloader::active() const +{ + QReadLocker locker(&m_mutex); + + return m_active; +} + +quint64 Downloader::loaded() const +{ + return m_httpClient->received(); +} + +quint64 Downloader::total() const +{ + return m_httpClient->contentLength(); +} diff --git a/components/Notifier.qml b/src/qt/downloader.h similarity index 51% rename from components/Notifier.qml rename to src/qt/downloader.h index 127de390..44840535 100644 --- a/components/Notifier.qml +++ b/src/qt/downloader.h @@ -1,21 +1,21 @@ -// Copyright (c) 2017-2018, The Monero Project -// +// Copyright (c) 2020, The Monero Project +// // All rights reserved. -// +// // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: -// +// // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. -// +// // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. -// +// // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. -// +// // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL @@ -26,60 +26,42 @@ // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import QtQuick 2.9 -import QtQuick.Controls 1.4 -import moneroComponents.Wallet 1.0 -import "." as MoneroComponents +#pragma once -Item { - id: item - property string message: "" - property bool active: false - height: 180 - width: 320 - property int margin: 15 - x: parent.width - width - margin - y: parent.height - height * scale.yScale - margin * scale.yScale +#include - Rectangle { - color: "#FF6C3C" - border.color: "black" - anchors.fill: parent +#include "network.h" - TextArea { - id:versionText - readOnly: true - backgroundVisible: false - textFormat: TextEdit.AutoText - anchors.fill: parent - font.family: MoneroComponents.Style.fontRegular.name - font.pixelSize: 12 - textMargin: 20 - textColor: "white" - text: item.message - wrapMode: Text.WrapAnywhere - } - } +class Downloader : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool active READ active NOTIFY activeChanged); + Q_PROPERTY(quint64 loaded READ loaded NOTIFY loadedChanged); + Q_PROPERTY(quint64 total READ total NOTIFY totalChanged); - transform: Scale { - id: scale - yScale: item.active ? 1 : 0 +public: + Downloader(QObject *parent = nullptr); + ~Downloader(); - Behavior on yScale { - NumberAnimation { duration: 500; easing.type: Easing.InOutCubic } - } - } + Q_INVOKABLE void cancel(); + Q_INVOKABLE bool get(const QString &url, const QJSValue &callback); + Q_INVOKABLE bool saveToFile(const QString &path) const; - Timer { - id: hider - interval: 30000; running: false; repeat: false - onTriggered: { item.active = false } - } +signals: + void activeChanged() const; + void loadedChanged() const; + void totalChanged() const; - function show(message) { - item.visible = true - item.message = message - item.active = true - hider.running = true - } -} +private: + bool active() const; + quint64 loaded() const; + quint64 total() const; + +private: + bool m_active; + std::string m_contents; + std::shared_ptr m_httpClient; + mutable QReadWriteLock m_mutex; + Network m_network; + mutable FutureScheduler m_scheduler; +}; diff --git a/src/qt/network.cpp b/src/qt/network.cpp index 0d523a5c..01d0f296 100644 --- a/src/qt/network.cpp +++ b/src/qt/network.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2019, The Monero Project +// Copyright (c) 2020, The Monero Project // // All rights reserved. // @@ -31,57 +31,83 @@ #include #include -// TODO: wallet_merged - epee library triggers the warnings -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -#pragma GCC diagnostic ignored "-Wreorder" -#include -#pragma GCC diagnostic pop - #include "utils.h" +using epee::net_utils::http::fields_list; +using epee::net_utils::http::http_response_info; +using epee::net_utils::http::http_simple_client; + +HttpClient::HttpClient(QObject *parent /* = nullptr */) + : QObject(parent) + , m_cancel(false) + , m_contentLength(0) + , m_received(0) +{ +} + +void HttpClient::cancel() +{ + m_cancel = true; +} + +quint64 HttpClient::contentLength() const +{ + return m_contentLength; +} + +quint64 HttpClient::received() const +{ + return m_received; +} + +bool HttpClient::on_header(const http_response_info &headers) +{ + if (m_cancel.exchange(false)) + { + return false; + } + + size_t contentLength = 0; + if (!epee::string_tools::get_xtype_from_string(contentLength, headers.m_header_info.m_content_length)) + { + qWarning() << "Failed to get Content-Length"; + } + m_contentLength = contentLength; + emit contentLengthChanged(); + + m_received = 0; + emit receivedChanged(); + + return http_simple_client::on_header(headers); +} + +bool HttpClient::handle_target_data(std::string &piece_of_transfer) +{ + if (m_cancel.exchange(false)) + { + return false; + } + + m_received += piece_of_transfer.size(); + emit receivedChanged(); + + return http_simple_client::handle_target_data(piece_of_transfer); +} + Network::Network(QObject *parent) : QObject(parent) , m_scheduler(this) { } -void Network::get(const QString &url, const QJSValue &callback, const QString &contentType) const +void Network::get(const QString &url, const QJSValue &callback, const QString &contentType /* = {} */) const { - qDebug() << QString("Fetching: %1").arg(url); - m_scheduler.run( - [url, contentType] { - epee::net_utils::http::http_simple_client httpClient; - - const QUrl urlParsed(url); - httpClient.set_server(urlParsed.host().toStdString(), urlParsed.scheme() == "https" ? "443" : "80", {}); - - const QString uri = (urlParsed.hasQuery() ? urlParsed.path() + "?" + urlParsed.query() : urlParsed.path()); - const epee::net_utils::http::http_response_info *pri = NULL; - constexpr std::chrono::milliseconds timeout = std::chrono::seconds(15); - - epee::net_utils::http::fields_list headers({{"User-Agent", randomUserAgent().toStdString()}}); - if (!contentType.isEmpty()) - { - headers.push_back({"Content-Type", contentType.toStdString()}); - } - const bool result = httpClient.invoke(uri.toStdString(), "GET", {}, timeout, std::addressof(pri), headers); - - if (!result) - { - return QJSValueList({QJSValue(), QJSValue(), "unknown error"}); - } - if (!pri) - { - return QJSValueList({QJSValue(), QJSValue(), "internal error (null response ptr)"}); - } - if (pri->m_response_code != 200) - { - return QJSValueList({QJSValue(), QJSValue(), QString("response code: %1").arg(pri->m_response_code)}); - } - - return QJSValueList({url, QString::fromStdString(pri->m_body)}); + [this, url, contentType] { + std::string response; + std::shared_ptr httpClient(new http_simple_client()); + QString error = get(httpClient, url, response, contentType); + return QJSValueList({url, QString::fromStdString(response), error}); }, callback); } @@ -90,3 +116,39 @@ void Network::getJSON(const QString &url, const QJSValue &callback) const { get(url, callback, "application/json; charset=utf-8"); } + +QString Network::get( + std::shared_ptr httpClient, + const QString &url, + std::string &response, + const QString &contentType /* = {} */) const +{ + const QUrl urlParsed(url); + httpClient->set_server(urlParsed.host().toStdString(), urlParsed.scheme() == "https" ? "443" : "80", {}); + + const QString uri = (urlParsed.hasQuery() ? urlParsed.path() + "?" + urlParsed.query() : urlParsed.path()); + const http_response_info *pri = NULL; + constexpr std::chrono::milliseconds timeout = std::chrono::seconds(15); + + fields_list headers({{"User-Agent", randomUserAgent().toStdString()}}); + if (!contentType.isEmpty()) + { + headers.push_back({"Content-Type", contentType.toStdString()}); + } + const bool result = httpClient->invoke(uri.toStdString(), "GET", {}, timeout, std::addressof(pri), headers); + if (!result) + { + return "unknown error"; + } + if (!pri) + { + return "internal error"; + } + if (pri->m_response_code != 200) + { + return QString("response code %1").arg(pri->m_response_code); + } + + response = std::move(pri->m_body); + return {}; +} diff --git a/src/qt/network.h b/src/qt/network.h index 765b2512..acdd6280 100644 --- a/src/qt/network.h +++ b/src/qt/network.h @@ -1,10 +1,72 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #pragma once #include #include +// TODO: wallet_merged - epee library triggers the warnings +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wreorder" +#include +#pragma GCC diagnostic pop + #include "FutureScheduler.h" +class HttpClient : public QObject, public epee::net_utils::http::http_simple_client +{ + Q_OBJECT + Q_PROPERTY(quint64 contentLength READ contentLength NOTIFY contentLengthChanged); + Q_PROPERTY(quint64 received READ received NOTIFY receivedChanged); + +public: + HttpClient(QObject *parent = nullptr); + + void cancel(); + quint64 contentLength() const; + quint64 received() const; + +signals: + void contentLengthChanged() const; + void receivedChanged() const; + +protected: + bool on_header(const epee::net_utils::http::http_response_info &headers) final; + bool handle_target_data(std::string &piece_of_transfer) final; + +private: + std::atomic m_cancel; + std::atomic m_contentLength; + std::atomic m_received; +}; + class Network : public QObject { Q_OBJECT @@ -15,6 +77,12 @@ public: Q_INVOKABLE void get(const QString &url, const QJSValue &callback, const QString &contentType = {}) const; Q_INVOKABLE void getJSON(const QString &url, const QJSValue &callback) const; + QString get( + std::shared_ptr httpClient, + const QString &url, + std::string &response, + const QString &contentType = {}) const; + private: mutable FutureScheduler m_scheduler; };