Co-Authored-By: selsta <selsta@sent.at>
Co-Authored-By: Gene Peters <gene@telligent-data.com>
pull/2/head
dsc 5 years ago committed by xmrdsc
parent 3c1fe1da8b
commit d4be7634cb

@ -32,6 +32,7 @@ import QtGraphicalEffects 1.0
import moneroComponents.Wallet 1.0
import moneroComponents.NetworkType 1.0
import moneroComponents.Clipboard 1.0
import FontAwesome 1.0
import "components" as MoneroComponents
import "components/effects/" as MoneroEffects
@ -44,10 +45,13 @@ Rectangle {
property alias unlockedBalanceLabelVisible: unlockedBalanceLabel.visible
property alias balanceLabelText: balanceLabel.text
property alias balanceText: balanceText.text
property alias balanceTextFiat: balanceTextFiat.text
property alias unlockedBalanceTextFiat: unlockedBalanceTextFiat.text
property alias networkStatus : networkStatus
property alias progressBar : progressBar
property alias daemonProgressBar : daemonProgressBar
property alias minutesToUnlockTxt: unlockedBalanceLabel.text
property bool fiatBalance: false
property int titleBarHeight: 50
property string copyValue: ""
Clipboard { id: clipboard }
@ -202,6 +206,26 @@ Rectangle {
}
}
}
MoneroComponents.Label {
fontSize: 20
text: "¥"
color: "white"
visible: persistentSettings.fiatPriceEnabled
anchors.right: parent.right
anchors.rightMargin: 45
anchors.top: parent.top
anchors.topMargin: 28
themeTransition: false
MouseArea{
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
fiatBalance = !fiatBalance
}
}
}
}
Item {
@ -214,7 +238,7 @@ Rectangle {
width: 50
MoneroComponents.TextPlain {
visible: !isMobile
visible: !(fiatBalance && persistentSettings.fiatPriceEnabled)
id: balanceText
themeTransition: false
anchors.left: parent.left
@ -255,9 +279,39 @@ Rectangle {
}
}
MoneroComponents.TextPlain {
visible: !balanceText.visible
id: balanceTextFiat
themeTransition: false
anchors.left: parent.left
anchors.leftMargin: 20
anchors.top: parent.top
anchors.topMargin: 76
font.family: "Arial"
color: "#FFFFFF"
text: "N/A"
font.pixelSize: balanceText.font.pixelSize
MouseArea {
hoverEnabled: true
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onEntered: {
parent.color = MoneroComponents.Style.orange
}
onExited: {
parent.color = MoneroComponents.Style.white
}
onClicked: {
console.log("Copied to clipboard");
clipboard.setText(parent.text);
appWindow.showStatusMessage(qsTr("Copied to clipboard"),3)
}
}
}
MoneroComponents.TextPlain {
id: unlockedBalanceText
visible: true
visible: !(fiatBalance && persistentSettings.fiatPriceEnabled)
themeTransition: false
anchors.left: parent.left
anchors.leftMargin: 20
@ -297,6 +351,36 @@ Rectangle {
}
}
MoneroComponents.TextPlain {
id: unlockedBalanceTextFiat
themeTransition: false
visible: !unlockedBalanceText.visible
anchors.left: parent.left
anchors.leftMargin: 20
anchors.top: parent.top
anchors.topMargin: 126
font.family: "Arial"
color: "#FFFFFF"
text: "N/A"
font.pixelSize: unlockedBalanceText.font.pixelSize
MouseArea {
hoverEnabled: true
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onEntered: {
parent.color = MoneroComponents.Style.orange
}
onExited: {
parent.color = MoneroComponents.Style.white
}
onClicked: {
console.log("Copied to clipboard");
clipboard.setText(parent.text);
appWindow.showStatusMessage(qsTr("Copied to clipboard"),3)
}
}
}
MoneroComponents.Label {
id: unlockedBalanceLabel
visible: true

@ -154,3 +154,20 @@ function qmlEach(item, properties, ignoredObjectNames, arr){
return arr;
}
function capitalize(s){
if (typeof s !== 'string') return ''
return s.charAt(0).toUpperCase() + s.slice(1)
}
function formatMoney(n, c, d, t) {
// https://stackoverflow.com/a/149099
var c = isNaN(c = Math.abs(c)) ? 2 : c,
d = d == undefined ? "." : d,
t = t == undefined ? "," : t,
s = n < 0 ? "-" : "",
i = String(parseInt(n = Math.abs(Number(n) || 0).toFixed(c))),
j = (j = i.length) > 3 ? j % 3 : 0;
return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
};

@ -30,6 +30,7 @@
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QStandardPaths>
#include <QNetworkAccessManager>
#include <QIcon>
#include <QDebug>
#include <QDesktopServices>
@ -65,6 +66,7 @@
#include "qt/utils.h"
#include "qt/mime.h"
#include "src/qt/KeysFiles.h"
#include "qt/prices.h"
// IOS exclusions
#ifndef Q_OS_IOS
@ -356,6 +358,11 @@ int main(int argc, char *argv[])
builtWithScanner = true;
#endif
engine.rootContext()->setContextProperty("builtWithScanner", builtWithScanner);
QNetworkAccessManager *manager = new QNetworkAccessManager();
Prices prices(manager);
engine.rootContext()->setContextProperty("Prices", &prices);
// Load main window (context properties needs to be defined obove this line)
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
if (engine.rootObjects().isEmpty())

@ -91,6 +91,26 @@ ApplicationWindow {
property int appEpoch: Math.floor((new Date).getTime() / 1000)
property bool themeTransition: false
// fiat price conversion
property int fiatPriceXMRUSD: 0
property int fiatPriceXMREUR: 0
property var fiatPriceAPIs: {
return {
"kraken": {
"xmrusd": "https://api.kraken.com/0/public/Ticker?pair=XMRUSD",
"xmreur": "https://api.kraken.com/0/public/Ticker?pair=XMREUR"
},
"coingecko": {
"xmrusd": "https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=usd",
"xmreur": "https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=eur"
},
"cryptocompare": {
"xmrusd": "https://min-api.cryptocompare.com/data/price?fsym=XMR&tsyms=USD",
"xmreur": "https://min-api.cryptocompare.com/data/price?fsym=XMR&tsyms=EUR",
}
}
}
property string remoteNodeService: {
// support user-defined remote node aggregators
if(persistentSettings.remoteNodeService){
@ -394,6 +414,10 @@ ApplicationWindow {
middlePanel.balanceText = balance;
leftPanel.balanceText = balance;
if (persistentSettings.fiatPriceEnabled) {
appWindow.fiatApiUpdateBalance(balance, balance_unlocked);
}
var accountLabel = currentWallet.getSubaddressLabel(currentWallet.currentSubaddressAccount, 0);
leftPanel.balanceLabelText = qsTr("Balance (#%1%2)").arg(currentWallet.currentSubaddressAccount).arg(accountLabel === "" ? "" : (" " + accountLabel));
}
@ -1102,6 +1126,7 @@ ApplicationWindow {
rootItem.state = "wizard"
// reset balance
leftPanel.balanceText = leftPanel.unlockedBalanceText = walletManager.displayAmount(0);
fiatApiUpdateBalance(0, 0);
// disable timers
userInActivityTimer.running = false;
simpleModeConnectionTimer.running = false;
@ -1126,6 +1151,143 @@ ApplicationWindow {
flags: persistentSettings.customDecorations ? Windows.flagsCustomDecorations : Windows.flags
onWidthChanged: x -= 0
Timer {
id: fiatPriceTimer
interval: 1000 * 60;
running: persistentSettings.fiatPriceEnabled;
repeat: true
onTriggered: {
if(persistentSettings.fiatPriceEnabled)
appWindow.fiatApiRefresh();
}
triggeredOnStart: false
}
function fiatApiParseTicker(resp, currency){
// parse & validate incoming JSON
if(resp._url.startsWith("https://api.kraken.com/0/")){
if(resp.hasOwnProperty("error") && resp.error.length > 0 || !resp.hasOwnProperty("result")){
appWindow.fiatApiError("Kraken API has error(s)");
return;
}
var key = currency === "xmreur" ? "XXMRZEUR" : "XXMRZUSD";
var ticker = resp.result[key]["o"];
return ticker;
} else if(resp._url.startsWith("https://api.coingecko.com/api/v3/")){
var key = currency === "xmreur" ? "eur" : "usd";
if(!resp.hasOwnProperty("monero") || !resp["monero"].hasOwnProperty(key)){
appWindow.fiatApiError("Coingecko API has error(s)");
return;
}
return resp["monero"][key];
} else if(resp._url.startsWith("https://min-api.cryptocompare.com/data/")){
var key = currency === "xmreur" ? "EUR" : "USD";
if(!resp.hasOwnProperty(key)){
appWindow.fiatApiError("cryptocompare API has error(s)");
return;
}
return resp[key];
}
}
function fiatApiGetCurrency(resp){
// map response to `appWindow.fiatPriceAPIs` object
if (!resp.hasOwnProperty('_url')){
appWindow.fiatApiError("invalid JSON");
return;
}
var apis = appWindow.fiatPriceAPIs;
for (var api in apis){
if (!apis.hasOwnProperty(api))
continue;
for (var cur in apis[api]){
if(!apis[api].hasOwnProperty(cur))
continue;
var url = apis[api][cur];
if(url === resp._url){
return cur;
}
}
}
}
function fiatApiJsonReceived(resp){
// handle incoming JSON, set ticker
var currency = appWindow.fiatApiGetCurrency(resp);
if(typeof currency == "undefined"){
appWindow.fiatApiError("could not get currency");
return;
}
var ticker = appWindow.fiatApiParseTicker(resp, currency);
if(ticker <= 0){
appWindow.fiatApiError("could not get ticker");
return;
}
if(persistentSettings.fiatPriceCurrency === "xmrusd")
appWindow.fiatPriceXMRUSD = ticker;
else if(persistentSettings.fiatPriceCurrency === "xmreur")
appWindow.fiatPriceXMREUR = ticker;
appWindow.updateBalance();
}
function fiatApiRefresh(){
// trigger API call
if(!persistentSettings.fiatPriceEnabled)
return;
var userProvider = persistentSettings.fiatPriceProvider;
if(!appWindow.fiatPriceAPIs.hasOwnProperty(userProvider)){
appWindow.fiatApiError("provider \"" + userProvider + "\" not implemented");
return;
}
var provider = appWindow.fiatPriceAPIs[userProvider];
var userCurrency = persistentSettings.fiatPriceCurrency;
if(!provider.hasOwnProperty(userCurrency)){
appWindow.fiatApiError("currency \"" + userCurrency + "\" not implemented");
}
var url = provider[userCurrency];
Prices.getJSON(url);
}
function fiatApiUpdateBalance(balance, unlocked_balance){
// update balance card
var ticker = persistentSettings.fiatPriceCurrency === "xmrusd" ? appWindow.fiatPriceXMRUSD : appWindow.fiatPriceXMREUR;
var symbol = persistentSettings.fiatPriceCurrency === "xmrusd" ? "$" : "€"
if(ticker <= 0){
console.log(fiatApiError("Could not update balance card; invalid ticker value"));
leftPanel.unlockedBalanceTextFiat = "N/A";
leftPanel.balanceTextFiat = "N/A";
return;
}
var uFiat = Utils.formatMoney(unlocked_balance * ticker);
var bFiat = Utils.formatMoney(balance * ticker);
leftPanel.unlockedBalanceTextFiat = symbol + uFiat;
leftPanel.balanceTextFiat = symbol + bFiat;
}
function fiatTimerStart(){
fiatPriceTimer.start();
}
function fiatTimerStop(){
fiatPriceTimer.stop();
}
function fiatApiError(msg){
console.log("fiatPriceError: " + msg);
}
Component.onCompleted: {
x = (Screen.width - width) / 2
y = (Screen.height - maxWindowHeight) / 2
@ -1137,6 +1299,7 @@ ApplicationWindow {
walletManager.checkUpdatesComplete.connect(onWalletCheckUpdatesComplete);
walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded);
IPC.uriHandler.connect(onUriHandler);
Prices.priceJsonReceived.connect(appWindow.fiatApiJsonReceived);
if(typeof daemonManager != "undefined") {
daemonManager.daemonStarted.connect(onDaemonStarted);
@ -1144,8 +1307,6 @@ ApplicationWindow {
daemonManager.daemonStopped.connect(onDaemonStopped);
}
// Connect app exit to qml window exit handling
mainApp.closing.connect(appWindow.close);
@ -1177,6 +1338,11 @@ ApplicationWindow {
}
checkUpdates();
if(persistentSettings.fiatPriceEnabled){
appWindow.fiatApiRefresh();
appWindow.fiatTimerStart();
}
}
Settings {
@ -1223,6 +1389,10 @@ ApplicationWindow {
property bool showPid: false
property bool blackTheme: true
property bool fiatPriceEnabled: false
property string fiatPriceProvider: "kraken"
property string fiatPriceCurrency: "xmrusd"
Component.onCompleted: {
MoneroComponents.Style.blackTheme = persistentSettings.blackTheme
}

@ -66,6 +66,8 @@ HEADERS += \
src/qt/mime.h \
src/qt/KeysFiles.h \
src/qt/utils.h
src/qt/utils.h \
src/qt/prices.h
SOURCES += main.cpp \
filter.cpp \
@ -98,6 +100,8 @@ SOURCES += main.cpp \
src/qt/mime.cpp \
src/qt/KeysFiles.cpp \
src/qt/utils.cpp
src/qt/utils.cpp \
src/qt/prices.cpp
CONFIG(DISABLE_PASS_STRENGTH_METER) {
HEADERS -= src/zxcvbn-c/zxcvbn.h

@ -157,6 +157,106 @@ Rectangle {
}
}
//! Manage pricing
RowLayout {
MoneroComponents.CheckBox {
id: enableConvertCurrency
text: qsTr("Enable displaying balance in other currencies") + translationManager.emptyString
checked: persistentSettings.fiatPriceEnabled
onCheckedChanged: {
if (!checked) {
console.log("Disabled price conversion");
persistentSettings.fiatPriceEnabled = false;
appWindow.fiatTimerStop();
}
}
}
}
GridLayout {
visible: enableConvertCurrency.checked
columns: 2
Layout.fillWidth: true
Layout.leftMargin: 36
columnSpacing: 32
ColumnLayout {
spacing: 10
Layout.fillWidth: true
MoneroComponents.Label {
Layout.fillWidth: true
fontSize: 14
text: qsTr("Price source") + translationManager.emptyString
}
MoneroComponents.StandardDropdown {
id: fiatPriceProviderDropDown
Layout.fillWidth: true
dataModel: fiatPriceProvidersModel
onChanged: {
var obj = dataModel.get(currentIndex);
persistentSettings.fiatPriceProvider = obj.data;
if(persistentSettings.fiatPriceEnabled)
appWindow.fiatApiRefresh();
}
}
}
ColumnLayout {
spacing: 10
Layout.fillWidth: true
MoneroComponents.Label {
Layout.fillWidth: true
fontSize: 14
text: qsTr("Currency") + translationManager.emptyString
}
MoneroComponents.StandardDropdown {
id: fiatPriceCurrencyDropdown
Layout.fillWidth: true
dataModel: fiatPriceCurrencyModel
onChanged: {
var obj = dataModel.get(currentIndex);
persistentSettings.fiatPriceCurrency = obj.data;
if(persistentSettings.fiatPriceEnabled)
appWindow.fiatApiRefresh();
}
}
}
z: parent.z + 1
}
ColumnLayout {
// Feature needs to be double enabled for security purposes (miss-clicks)
visible: enableConvertCurrency.checked && !persistentSettings.fiatPriceEnabled
spacing: 0
Layout.topMargin: 5
Layout.leftMargin: 36
MoneroComponents.WarningBox {
text: qsTr("Enabling price conversion exposes your IP address to the selected price source.") + translationManager.emptyString;
}
MoneroComponents.StandardButton {
Layout.topMargin: 10
Layout.bottomMargin: 10
small: true
text: qsTr("Confirm and enable") + translationManager.emptyString
onClicked: {
console.log("Enabled price conversion");
persistentSettings.fiatPriceEnabled = true;
appWindow.fiatApiRefresh();
appWindow.fiatTimerStart();
}
}
}
MoneroComponents.StandardButton {
visible: !persistentSettings.customDecorations
Layout.topMargin: 10
@ -177,7 +277,43 @@ Rectangle {
}
}
ListModel {
id: fiatPriceProvidersModel
}
ListModel {
id: fiatPriceCurrencyModel
ListElement {
data: "xmrusd"
column1: "USD"
}
ListElement {
data: "xmreur"
column1: "EUR"
}
}
Component.onCompleted: {
// Dynamically fill fiatPrice dropdown based on `appWindow.fiatPriceAPIs`
var apis = appWindow.fiatPriceAPIs;
fiatPriceProvidersModel.clear();
var i = 0;
for (var api in apis){
if (!apis.hasOwnProperty(api))
continue;
fiatPriceProvidersModel.append({"column1": Utils.capitalize(api), "data": api});
if(api === persistentSettings.fiatPriceProvider)
fiatPriceProviderDropDown.currentIndex = i;
i += 1;
}
fiatPriceProviderDropDown.update();
fiatPriceCurrencyDropdown.currentIndex = persistentSettings.fiatPriceCurrency === "xmrusd" ? 0 : 1;
fiatPriceCurrencyDropdown.update();
console.log('SettingsLayout loaded');
}
}

@ -0,0 +1,110 @@
// Copyright (c) 2014-2019, 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 <QtCore>
#include <QNetworkAccessManager>
#include "utils.h"
#include "prices.h"
Prices::Prices(QNetworkAccessManager *networkAccessManager, QObject *parent)
: QObject(parent) {
this->m_networkAccessManager = networkAccessManager;
}
void Prices::getJSON(const QString url) {
qDebug() << QString("Fetching: %1").arg(url);
QNetworkRequest request;
request.setUrl(QUrl(url));
request.setRawHeader("User-Agent", randomUserAgent().toUtf8());
request.setRawHeader("Content-Type", "application/json");
m_reply = this->m_networkAccessManager->get(request);
connect(m_reply, SIGNAL(finished()), this, SLOT(gotJSON()));
}
void Prices::gotJSON() {
// Check connectivity
if (!m_reply || m_reply->error() != QNetworkReply::NoError){
this->gotError();
m_reply->deleteLater();
return;
}
// Check json header
QList<QByteArray> headerList = m_reply->rawHeaderList();
QByteArray headerJson = m_reply->rawHeader("Content-Type");
if(headerJson.length() <= 15){
this->gotError("Bad Content-Type");
m_reply->deleteLater();
return;
}
QString headerJsonStr = QTextCodec::codecForMib(106)->toUnicode(headerJson);
int _contentType = headerList.indexOf("Content-Type");
if (_contentType < 0 || !headerJsonStr.startsWith("application/json")){
this->gotError("Bad Content-Type");
m_reply->deleteLater();
return;
}
// Check valid json document
QByteArray data = m_reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
QString jsonString = doc.toJson(QJsonDocument::Indented);
if (jsonString.isEmpty()){
this->gotError("Bad JSON");
m_reply->deleteLater();
return;
}
// Insert source url for later reference
QUrl url = m_reply->url();
QJsonObject docobj = doc.object();
docobj["_url"] = url.toString();
doc.setObject(docobj);
qDebug() << QString("Fetched: %1").arg(url.toString());
// Emit signal
QVariantMap vMap = doc.object().toVariantMap();
emit priceJsonReceived(vMap);
m_reply->deleteLater();
}
void Prices::gotError() {
this->gotError("Unknown error");
}
void Prices::gotError(const QString &message) {
qCritical() << __FUNCTION__ << ": Error: " << message;
emit priceJsonError(message);
}

@ -0,0 +1,30 @@
#ifndef PRICES_H
#define PRICES_H
#include <QCoreApplication>
#include <QtNetwork>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDebug>
class Prices : public QObject
{
Q_OBJECT
public:
Prices(QNetworkAccessManager *networkAccessManager, QObject *parent = nullptr);
public slots:
Q_INVOKABLE void getJSON(const QString url);
void gotJSON();
void gotError();
void gotError(const QString &message);
signals:
void priceJsonReceived(QVariantMap document);
void priceJsonError(QString message);
private:
mutable QPointer<QNetworkReply> m_reply;
QNetworkAccessManager *m_networkAccessManager;
};
#endif // PRICES_H

@ -46,3 +46,84 @@ QString getAccountName(){
accountName = "My monero Account";
return accountName;
}
QString randomUserAgent(){
QStringList urand;
urand << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"
<< "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0"
<< "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0"
<< "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0"
<< "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0"
<< "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0"
<< "Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0"
<< "Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3"
<< "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0"
<< "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0"
<< "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0"
<< "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0"
<< "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0"
<< "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0"
<< "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/23.0"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20130406 Firefox/23.0"
<< "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0"
<< "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/22.0"
<< "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0"
<< "Mozilla/5.0 (Microsoft Windows NT 6.2.9200.0); rv:22.0) Gecko/20130405 Firefox/22.0"
<< "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1"
<< "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1"
<< "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:21.0.0) Gecko/20121011 Firefox/21.0.0"
<< "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20130331 Firefox/21.0"
<< "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0"
<< "Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20130514 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.2; rv:21.0) Gecko/20130326 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130401 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130331 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130330 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130401 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130328 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130401 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130331 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20100101 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 5.0; rv:21.0) Gecko/20100101 Firefox/21.0"
<< "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0"
<< "Mozilla/5.0 (Windows NT 6.2; Win64; x64;) Gecko/20100101 Firefox/20.0"
<< "Mozilla/5.0 (Windows x86; rv:19.0) Gecko/20100101 Firefox/19.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/19.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/18.0.1"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0"
<< "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0.6"
<< "Mozilla/5.0 (X11; Ubuntu; Linux armv7l; rv:17.0) Gecko/20100101 Firefox/17.0"
<< "Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1"
<< "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1"
<< "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1"
<< "Mozilla/5.0 (X11; NetBSD amd64; rv:16.0) Gecko/20121102 Firefox/16.0"
<< "Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2"
<< "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.16) Gecko/20120427 Firefox/15.0a1"
<< "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/15.0a1"
<< "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:15.0) Gecko/20120910144328 Firefox/15.0.2"
<< "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1"
<< "Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:15.0) Gecko/20121011 Firefox/15.0.1"
<< "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1"
<< "Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20120405 Firefox/14.0a1"
<< "Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1"
<< "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1"
<< "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1"
<< "Mozilla/5.0 (Windows; U; Windows NT 6.1; WOW64; en-US; rv:2.0.4) Gecko/20120718 AskTbAVR-IDW/3.12.5.17700 Firefox/14.0.1"
<< "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/14.0.1"
<< "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/ 20120405 Firefox/14.0.1"
<< "Mozilla/5.0 (Windows NT 6.0; rv:14.0) Gecko/20100101 Firefox/14.0.1"
<< "Mozilla/5.0 (Windows NT 5.1; rv:15.0) Gecko/20100101 Firefox/13.0.1"
<< "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0"
<< "Mozilla/5.0 (Windows NT 6.1; de;rv:12.0) Gecko/20120403211507 Firefox/12.0";
// @TODO: Qt 5.10 - QRandomGenerator
int irand = rand() % urand.length();
return urand.at(irand);
}

@ -35,5 +35,6 @@
bool fileExists(QString path);
QString getAccountName();
const static QRegExp reURI = QRegExp("^\\w+:\\/\\/([\\w+\\-?\\-_\\-=\\-&]+)");
QString randomUserAgent();
#endif // UTILS_H