diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b8543d..3717e57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,11 +19,15 @@ set(HEADER_FILES data/syncthingconnection.h data/syncthingdirectorymodel.h data/syncthingdevicemodel.h + data/syncthingconfig.h + data/utils.h ) set(SRC_FILES data/syncthingconnection.cpp data/syncthingdirectorymodel.cpp data/syncthingdevicemodel.cpp + data/syncthingconfig.cpp + data/utils.cpp ) set(WIDGETS_HEADER_FILES @@ -86,6 +90,7 @@ set(REQUIRED_ICONS network-card window-close edit-copy + edit-paste preferences-other view-barcode folder-open @@ -98,6 +103,13 @@ set(REQUIRED_ICONS network-server folder-sync internet-web-browser + network-connect + system-run + system-search + preferences-desktop + preferences-desktop-notification + preferences-desktop-icons + preferences-desktop-locale ) # find c++utilities diff --git a/README.md b/README.md index 3946655..f39cb90 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,27 @@ Qt 5-based tray application for [Syncthing](https://github.com/syncthing/syncthing) -* Still under development * Designed to work under any desktop environment with tray icon support + * Tested under Plasma 5 and Openbox/qt5ct/Tint2 + * Could be shown as regular window if no tray icon support is available * Doesn't require desktop environment specific libraries * Provides quick access to most frequently used features but does not intend to replace the official web UI + * Check state of directories and devices + * Check current traffic statistics + * Display further details about direcoties and devices, like last file, last + scan, ... + * Trigger re-scan of a specific directory + * Open a directory with the default file browser + * Pause/resume devices * Shows Syncthing notifications +* Does *not* allow configuring Syncthing itself (currently I do not intend to add this feature as it could cause more harm than good when not implemented correctly) * Provides quick access to the official web UI * Utilizes either Qt WebKit or Qt WebEngine * Can be built without web view support as well (then the web UI is opened in the regular browser) +* Still under development; the following features are planned + * Connect to multiple instances of Syncthing at a time + * Add option to conveniently add the tray to the applications launched when the desktop environment starts + * Add option to launch Syncthing when the tray is started and log stdout/stderr (would make sense for me under Windows, otherwise starting Syncthing via systemd is more preferable of course) ## Screenshots ### Under Openbox/Tint2 @@ -36,3 +49,9 @@ The following Qt 5 modules are requried: core network gui widgets webenginewidge #### Select Qt modules for WebView * If Qt WebKitWidgets is installed on the system, the tray will link against it. Otherwise it will link against Qt WebEngineWidgets. * To force usage of Qt WebKit/Qt WebEngine or to disable both add `-DWEBVIEW_PROVIDER=webkit/webengine/none` to the CMake arguments. + +BTW: I still prefer the deprecated Qt WebKit because +* I currently don't know how to allow a particular self-signed certificate in Qt WebEngine. (Currently any self-signed certificate is accepted!) +* Qt WebEngine can not be built with mingw-w64. +* Qt WebEngine is more buggy in my experience. +* security issues are not a concern because no other website than the Syncthing web UI is shown. diff --git a/application/main.cpp b/application/main.cpp index 849806f..1174b52 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -1,79 +1,87 @@ -#include "./settings.h" -#include "../gui/trayicon.h" -#include "../gui/traywidget.h" - -#include "resources/config.h" - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include - -#include - -using namespace std; -using namespace ApplicationUtilities; -using namespace QtGui; - -int main(int argc, char *argv[]) -{ - SET_APPLICATION_INFO; - // setup argument parser - ArgumentParser parser; - HelpArgument helpArg(parser); - // Qt configuration arguments - QT_CONFIG_ARGUMENTS qtConfigArgs; - Argument windowedArg("windowed", 'w', "shows the UI in a regular window"); - windowedArg.setCombinable(true); - qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&windowedArg); - parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &helpArg}); - try { - parser.parseArgs(argc, argv); - if(qtConfigArgs.qtWidgetsGuiArg().isPresent()) { - SET_QT_APPLICATION_INFO; - QApplication application(argc, argv); - Settings::restore(); - Settings::qtSettings().apply(); - qtConfigArgs.applySettings(true); - LOAD_QT_TRANSLATIONS; - QtUtilitiesResources::init(); - int res; -#ifndef QT_NO_SYSTEMTRAYICON - if(QSystemTrayIcon::isSystemTrayAvailable()) { - if(windowedArg.isPresent()) { - TrayWidget trayWidget; - trayWidget.show(); - res = application.exec(); - } else { - application.setQuitOnLastWindowClosed(false); - TrayIcon trayIcon; - trayIcon.show(); - res = application.exec(); - } - } else { - QMessageBox::critical(nullptr, QApplication::applicationName(), QApplication::translate("main", "The system tray is (currently) not available.")); - res = -1; - } -#else - QMessageBox::critical(nullptr, QApplication::applicationName(), QApplication::translate("main", "The Qt libraries have not been built with tray icon support.")); - res = -2; -#endif - Settings::save(); - QtUtilitiesResources::cleanup(); - return res; - } - } catch(const Failure &ex) { - CMD_UTILS_START_CONSOLE; - cout << "Unable to parse arguments. " << ex.what() << "\nSee --help for available commands." << endl; - } - - return 0; -} - +#include "./settings.h" +#include "../gui/trayicon.h" +#include "../gui/traywidget.h" + +#include "resources/config.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include + +using namespace std; +using namespace ApplicationUtilities; +using namespace QtGui; + +int main(int argc, char *argv[]) +{ + SET_APPLICATION_INFO; + // setup argument parser + ArgumentParser parser; + HelpArgument helpArg(parser); + // Qt configuration arguments + QT_CONFIG_ARGUMENTS qtConfigArgs; + Argument windowedArg("windowed", 'w', "opens the tray menu as a regular window"); + windowedArg.setCombinable(true); + qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&windowedArg); + parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &helpArg}); + try { + parser.parseArgs(argc, argv); + if(qtConfigArgs.qtWidgetsGuiArg().isPresent()) { + SET_QT_APPLICATION_INFO; + QApplication application(argc, argv); + Settings::restore(); + Settings::qtSettings().apply(); + qtConfigArgs.applySettings(true); + LOAD_QT_TRANSLATIONS; + QtUtilitiesResources::init(); + int res; + if(windowedArg.isPresent()) { + TrayWidget trayWidget; + trayWidget.show(); + res = application.exec(); + } else { +#ifndef QT_NO_SYSTEMTRAYICON + if(QSystemTrayIcon::isSystemTrayAvailable()) { + application.setQuitOnLastWindowClosed(false); + TrayIcon trayIcon; + trayIcon.show(); + if(Settings::firstLaunch()) { + trayIcon.trayMenu().widget()->showSettingsDialog(); + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Information); + msgBox.setText(QCoreApplication::translate("main", "You must configure how to connect to Syncthing when using Syncthing Tray the first time.")); + msgBox.setInformativeText(QCoreApplication::translate("main", "Note that the settings dialog allows importing URL, credentials and API-key from the local Syncthing configuration.")); + msgBox.exec(); + } + res = application.exec(); + } else { + QMessageBox::critical(nullptr, QApplication::applicationName(), QApplication::translate("main", "The system tray is (currently) not available. You could open the tray menu as a regular window using the -w flag, though.")); + res = -1; + } +#else + QMessageBox::critical(nullptr, QApplication::applicationName(), QApplication::translate("main", "The Qt libraries have not been built with tray icon support. You could open the tray menu as a regular window using the -w flag, though.")); + res = -2; +#endif + } + Settings::save(); + QtUtilitiesResources::cleanup(); + return res; + } + } catch(const Failure &ex) { + CMD_UTILS_START_CONSOLE; + cout << "Unable to parse arguments. " << ex.what() << "\nSee --help for available commands." << endl; + } + + return 0; +} + diff --git a/application/settings.cpp b/application/settings.cpp index 7861582..f5068c3 100644 --- a/application/settings.cpp +++ b/application/settings.cpp @@ -1,4 +1,5 @@ #include "./settings.h" + #include #include @@ -10,7 +11,13 @@ using namespace Media; namespace Settings { -// tray +bool &firstLaunch() +{ + static bool v = false; + return v; +} + +// connection QString &syncthingUrl() { static QString v; @@ -36,12 +43,14 @@ QByteArray &apiKey() static QByteArray v; return v; } + +// notifications bool ¬ifyOnDisconnect() { static bool v = true; return v; } -bool ¬ifyOnErrors() +bool ¬ifyOnInternalErrors() { static bool v = true; return v; @@ -56,11 +65,20 @@ bool &showSyncthingNotifications() static bool v = true; return v; } + +// appearance bool &showTraffic() { static bool v = true; return v; } +QSize &trayMenuSize() +{ + static QSize v(350, 300); + return v; +} + +// autostart/launcher bool &launchSynchting() { static bool v = false; @@ -108,16 +126,18 @@ void restore() QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); settings.beginGroup(QStringLiteral("tray")); + firstLaunch() = !settings.contains(QStringLiteral("syncthingUrl")); syncthingUrl() = settings.value(QStringLiteral("syncthingUrl"), QStringLiteral("http://localhost:8080/")).toString(); authEnabled() = settings.value(QStringLiteral("authEnabled"), false).toBool(); userName() = settings.value(QStringLiteral("userName")).toString(); password() = settings.value(QStringLiteral("password")).toString(); apiKey() = settings.value(QStringLiteral("apiKey")).toByteArray(); notifyOnDisconnect() = settings.value(QStringLiteral("notifyOnDisconnect"), true).toBool(); - notifyOnErrors() = settings.value(QStringLiteral("notifyOnErrors"), true).toBool(); + notifyOnInternalErrors() = settings.value(QStringLiteral("notifyOnErrors"), true).toBool(); notifyOnSyncComplete() = settings.value(QStringLiteral("notifyOnSyncComplete"), true).toBool(); showSyncthingNotifications() = settings.value(QStringLiteral("showSyncthingNotifications"), true).toBool(); showTraffic() = settings.value(QStringLiteral("showTraffic"), true).toBool(); + trayMenuSize() = settings.value(QStringLiteral("trayMenuSize"), trayMenuSize()).toSize(); launchSynchting() = settings.value(QStringLiteral("launchSynchting"), false).toBool(); syncthingCommand() = settings.value(QStringLiteral("syncthingCommand"), QStringLiteral("syncthing")).toString(); settings.endGroup(); @@ -145,10 +165,11 @@ void save() settings.setValue(QStringLiteral("password"), password()); settings.setValue(QStringLiteral("apiKey"), apiKey()); settings.setValue(QStringLiteral("notifyOnDisconnect"), notifyOnDisconnect()); - settings.setValue(QStringLiteral("notifyOnErrors"), notifyOnErrors()); + settings.setValue(QStringLiteral("notifyOnErrors"), notifyOnInternalErrors()); settings.setValue(QStringLiteral("notifyOnSyncComplete"), notifyOnSyncComplete()); settings.setValue(QStringLiteral("showSyncthingNotifications"), showSyncthingNotifications()); settings.setValue(QStringLiteral("showTraffic"), showTraffic()); + settings.setValue(QStringLiteral("trayMenuSize"), trayMenuSize()); settings.setValue(QStringLiteral("launchSynchting"), launchSynchting()); settings.setValue(QStringLiteral("syncthingCommand"), syncthingCommand()); settings.endGroup(); diff --git a/application/settings.h b/application/settings.h index 5c35417..4503018 100644 --- a/application/settings.h +++ b/application/settings.h @@ -7,6 +7,7 @@ QT_FORWARD_DECLARE_CLASS(QByteArray) QT_FORWARD_DECLARE_CLASS(QString) +QT_FORWARD_DECLARE_CLASS(QSize) namespace Media { enum class TagUsage; @@ -19,21 +20,30 @@ class QtSettings; namespace Settings { +bool &firstLaunch(); + +// connection QString &syncthingUrl(); bool &authEnabled(); QString &userName(); QString &password(); QByteArray &apiKey(); +// notifications bool ¬ifyOnDisconnect(); -bool ¬ifyOnErrors(); +bool ¬ifyOnInternalErrors(); bool ¬ifyOnSyncComplete(); bool &showSyncthingNotifications(); -bool &showTraffic(); +// apprearance +bool &showTraffic(); +QSize &trayMenuSize(); + +// autostart/launcher bool &launchSynchting(); QString &syncthingCommand(); +// web view #if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) bool &webViewDisabled(); double &webViewZoomFactor(); diff --git a/data/syncthingconfig.cpp b/data/syncthingconfig.cpp new file mode 100644 index 0000000..e0536cf --- /dev/null +++ b/data/syncthingconfig.cpp @@ -0,0 +1,70 @@ +#include "syncthingconfig.h" + +#include +#include +#include + +namespace Data { + +/*! + * \struct SyncthingConfig + * \brief The SyncthingConfig struct holds the configuration of Syncthing itself read from config.xml in the Syncthing home directory. + * \remarks Only a few fields are required since most of the Syncthing config can be accessed via SyncthingConnection class. + */ + +QString SyncthingConfig::locateConfigFile() +{ + QString path = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("syncthing/config.xml")); + if(path.isEmpty()) { + path = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("Syncthing/config.xml")); + } + return path; +} + +QString SyncthingConfig::locateHttpsCertificate() +{ + QString path = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("syncthing/https-cert.pem")); + if(path.isEmpty()) { + path = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("Syncthing/https-cert.pem")); + } + return path; +} + +bool SyncthingConfig::restore(const QString &configFilePath) +{ + QFile configFile(configFilePath); + if(!configFile.open(QFile::ReadOnly)) { + return false; + } + + QXmlStreamReader xmlReader(&configFile); + bool ok = false; +#include + children { + // only version 16 supported, try to parse other versions anyways since the changes might not affect + // the few parts read here + version = attribute("version").toString(); + children { + iftag("gui") { + ok = true; + guiEnabled = attributeFlag("enabled"); + guiEnforcesSecureConnection = attributeFlag("tls"); + children { + iftag("address") { + guiAddress = text; + } eliftag("user") { + guiUser = text; + } eliftag("password") { + guiPasswordHash = text; + } eliftag("apikey") { + guiApiKey = text; + } else_skip + } + } else_skip + } + } +#include + return ok; +} + +} // namespace Data diff --git a/data/syncthingconfig.h b/data/syncthingconfig.h new file mode 100644 index 0000000..e764e34 --- /dev/null +++ b/data/syncthingconfig.h @@ -0,0 +1,26 @@ +#ifndef DATA_SYNCTHINGCONFIG_H +#define DATA_SYNCTHINGCONFIG_H + +#include + +namespace Data { + +struct SyncthingConfig +{ + QString version; + bool guiEnabled = false; + bool guiEnforcesSecureConnection = false; + QString guiAddress; + QString guiUser; + QString guiPasswordHash; + QString guiApiKey; + + static QString locateConfigFile(); + static QString locateHttpsCertificate(); + bool restore(const QString &configFilePath); +}; + + +} // namespace Data + +#endif // DATA_SYNCTHINGCONFIG_H diff --git a/data/syncthingconnection.cpp b/data/syncthingconnection.cpp index c303236..6803aee 100644 --- a/data/syncthingconnection.cpp +++ b/data/syncthingconnection.cpp @@ -1,4 +1,5 @@ #include "./syncthingconnection.h" +#include "./syncthingconfig.h" #include @@ -13,6 +14,7 @@ #include #include #include +#include #include @@ -56,6 +58,13 @@ bool SyncthingDir::assignStatus(const QString &statusStr) newStatus = DirStatus::Unknown; } if(newStatus != status) { + switch(status) { + case DirStatus::Scanning: + lastScanTime = DateTime::now(); + break; + default: + ; + } status = newStatus; return true; } @@ -67,6 +76,8 @@ bool SyncthingDir::assignStatus(const QString &statusStr) * \brief The SyncthingConnection class allows Qt applications to access Syncthing. */ +QList SyncthingConnection::m_expectedCertificateErrors; + /*! * \brief Constructs a new instance ready to connect. To establish the connection, call connect(). */ @@ -84,10 +95,12 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt m_totalOutgoingRate(0), m_configReply(nullptr), m_statusReply(nullptr), + m_connectionsReply(nullptr), m_eventsReply(nullptr), m_unreadNotifications(false), m_hasConfig(false), - m_hasStatus(false) + m_hasStatus(false), + m_lastFileDeleted(false) {} /*! @@ -226,17 +239,21 @@ QNetworkRequest SyncthingConnection::prepareRequest(const QString &path, const Q /*! * \brief Requests asynchronously data using the rest API. */ -inline QNetworkReply *SyncthingConnection::requestData(const QString &path, const QUrlQuery &query, bool rest) +QNetworkReply *SyncthingConnection::requestData(const QString &path, const QUrlQuery &query, bool rest) { - return networkAccessManager().get(prepareRequest(path, query, rest)); + auto *reply = networkAccessManager().get(prepareRequest(path, query, rest)); + reply->ignoreSslErrors(m_expectedCertificateErrors); + return reply; } /*! * \brief Posts asynchronously data using the rest API. */ -inline QNetworkReply *SyncthingConnection::postData(const QString &path, const QUrlQuery &query, const QByteArray &data) +QNetworkReply *SyncthingConnection::postData(const QString &path, const QUrlQuery &query, const QByteArray &data) { - return networkAccessManager().post(prepareRequest(path, query), data); + auto *reply = networkAccessManager().post(prepareRequest(path, query), data); + reply->ignoreSslErrors(m_expectedCertificateErrors); + return reply; } SyncthingDir *SyncthingConnection::findDirInfo(const QString &dir, int &row) @@ -270,6 +287,8 @@ void SyncthingConnection::continueConnecting() { if(m_keepPolling && m_hasConfig && m_hasStatus) { requestConnections(); + requestDirStatistics(); + requestDeviceStatistics(); // since config and status could be read successfully, let's poll for events m_lastEventId = 0; requestEvents(); @@ -318,13 +337,29 @@ void SyncthingConnection::requestStatus() /*! * \brief Requests current connections asynchronously. * - * The signal devStatusChanged() is emitted for each device where the connection status has changed updated; error() is emitted in the error case. + * The signal devStatusChanged() is emitted for each device where the connection status has changed; error() is emitted in the error case. */ void SyncthingConnection::requestConnections() { QObject::connect(m_connectionsReply = requestData(QStringLiteral("system/connections"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readConnections); } +/*! + * \brief Requests directory statistics asynchronously. + */ +void SyncthingConnection::requestDirStatistics() +{ + QObject::connect(requestData(QStringLiteral("stats/folder"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readDirStatistics); +} + +/*! + * \brief Requests device statistics asynchronously. + */ +void SyncthingConnection::requestDeviceStatistics() +{ + QObject::connect(requestData(QStringLiteral("stats/device"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readDeviceStatistics); +} + /*! * \brief Requests the Syncthing events (since the last successful call) asynchronously. * @@ -399,6 +434,37 @@ void SyncthingConnection::requestLog(std::function cert = QSslCertificate::fromPath(certPath); + if(cert.isEmpty()) { + emit error(tr("Unable to load certificate used by Syncthing GUI.")); + return; + } + m_expectedCertificateErrors << QSslError(QSslError::UnableToGetLocalIssuerCertificate, cert.at(0)); + m_expectedCertificateErrors << QSslError(QSslError::UnableToVerifyFirstCertificate, cert.at(0)); + m_expectedCertificateErrors << QSslError(QSslError::SelfSignedCertificate, cert.at(0)); + m_expectedCertificateErrors << QSslError(QSslError::HostNameMismatch, cert.at(0)); +} + /*! * \brief Reads results of requestConfig(). */ @@ -556,7 +622,7 @@ void SyncthingConnection::readConnections() double transferTime; if(!m_lastConnectionsUpdate.isNull() && ((transferTime = (DateTime::gmtNow() - m_lastConnectionsUpdate).totalSeconds()) != 0.0)) { m_totalIncomingRate = (totalIncomingTraffic - m_totalIncomingTraffic) * 0.008 / transferTime, - m_totalOutgoingRate = (totalOutgoingTraffic - m_totalOutgoingTraffic) * 0.008 / transferTime; + m_totalOutgoingRate = (totalOutgoingTraffic - m_totalOutgoingTraffic) * 0.008 / transferTime; } else { m_totalIncomingRate = m_totalOutgoingRate = 0.0; } @@ -611,6 +677,108 @@ void SyncthingConnection::readConnections() } } +/*! + * \brief Reads results of requestDirStatistics(). + * \remarks TODO + */ +void SyncthingConnection::readDirStatistics() +{ + auto *reply = static_cast(sender()); + reply->deleteLater(); + + switch(reply->error()) { + case QNetworkReply::NoError: { + QJsonParseError jsonError; + const QJsonDocument replyDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) { + const QJsonObject replyObj(replyDoc.object()); + int index = 0; + for(SyncthingDir &dirInfo : m_dirs) { + const QJsonObject dirObj(replyObj.value(dirInfo.id).toObject()); + if(!dirObj.isEmpty()) { + bool mod = false; + try { + dirInfo.lastScanTime = DateTime::fromIsoStringLocal(dirObj.value(QStringLiteral("lastScan")).toString().toUtf8().data()); + mod = true; + } catch(const ConversionException &) { + dirInfo.lastScanTime = DateTime(); + } + const QJsonObject lastFileObj(dirObj.value(QStringLiteral("lastFile")).toObject()); + if(!lastFileObj.isEmpty()) { + dirInfo.lastFileName = lastFileObj.value(QStringLiteral("filename")).toString(); + mod = true; + if(!dirInfo.lastFileName.isEmpty()) { + dirInfo.lastFileDeleted = lastFileObj.value(QStringLiteral("deleted")).toBool(false); + try { + dirInfo.lastFileTime = DateTime::fromIsoStringLocal(lastFileObj.value(QStringLiteral("at")).toString().toUtf8().data()); + if(dirInfo.lastFileTime > m_lastFileTime) { + m_lastFileTime = dirInfo.lastFileTime, + m_lastFileName = dirInfo.lastFileName, + m_lastFileDeleted = dirInfo.lastFileDeleted; + } + } catch(const ConversionException &) { + dirInfo.lastFileTime = DateTime(); + } + } + } + if(mod) { + emit dirStatusChanged(dirInfo, index); + } + } + ++index; + } + } else { + emit error(tr("Unable to parse directory statistics: ") + jsonError.errorString()); + } + } case QNetworkReply::OperationCanceledError: + return; // intended, not an error + default: + emit error(tr("Unable to request directory statistics: ") + reply->errorString()); + } +} + +/*! + * \brief Reads results of requestDeviceStatistics(). + * \remarks TODO + */ +void SyncthingConnection::readDeviceStatistics() +{ + auto *reply = static_cast(sender()); + reply->deleteLater(); + + switch(reply->error()) { + case QNetworkReply::NoError: { + QJsonParseError jsonError; + const QJsonDocument replyDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) { + const QJsonObject replyObj(replyDoc.object()); + int index = 0; + for(SyncthingDev &devInfo : m_devs) { + const QJsonObject devObj(replyObj.value(devInfo.id).toObject()); + if(!devObj.isEmpty()) { + try { + devInfo.lastSeen = DateTime::fromIsoStringLocal(devObj.value(QStringLiteral("lastSeen")).toString().toUtf8().data()); + emit devStatusChanged(devInfo, index); + } catch(const ConversionException &) { + devInfo.lastSeen = DateTime(); + } + } + ++index; + } + // since there seems no event for this data, just request every minute, FIXME: make interval configurable + if(m_keepPolling) { + QTimer::singleShot(60000, Qt::VeryCoarseTimer, this, SLOT(requestConnections())); + } + } else { + emit error(tr("Unable to parse device statistics: ") + jsonError.errorString()); + } + } case QNetworkReply::OperationCanceledError: + return; // intended, not an error + default: + emit error(tr("Unable to request device statistics: ") + reply->errorString()); + } +} + /*! * \brief Reads results of requestEvents(). */ @@ -635,7 +803,7 @@ void SyncthingConnection::readEvents() m_lastEventId = event.value(QStringLiteral("id")).toInt(m_lastEventId); DateTime eventTime; try { - eventTime = DateTime::fromIsoString(event.value(QStringLiteral("time")).toString().toLocal8Bit().data()).first; + eventTime = DateTime::fromIsoStringGmt(event.value(QStringLiteral("time")).toString().toLocal8Bit().data()); } catch(const ConversionException &) { // ignore conversion error } @@ -651,6 +819,10 @@ void SyncthingConnection::readEvents() readDirEvent(eventType, eventData); } else if(eventType.startsWith(QLatin1String("Device"))) { readDeviceEvent(eventTime, eventType, eventData); + } else if(eventType == QLatin1String("ItemStarted")) { + readItemStarted(eventTime, eventData); + } else if(eventType == QLatin1String("ItemFinished")) { + readItemFinished(eventTime, eventData); } } } else { @@ -727,8 +899,13 @@ void SyncthingConnection::readStatusChangedEvent(const QJsonObject &eventData) * \remarks TODO */ void SyncthingConnection::readDownloadProgressEvent(const QJsonObject &eventData) -{} +{ + VAR_UNUSED(eventData) +} +/*! + * \brief Reads results of requestEvents(). + */ void SyncthingConnection::readDirEvent(const QString &eventType, const QJsonObject &eventData) { const QString dir(eventData.value(QStringLiteral("folder")).toString()); @@ -743,6 +920,7 @@ void SyncthingConnection::readDirEvent(const QString &eventType, const QJsonObje const QJsonObject error(errorVal.toObject()); if(!error.isEmpty()) { dirInfo->errors.emplace_back(error.value(QStringLiteral("error")).toString(), error.value(QStringLiteral("path")).toString()); + dirInfo->status = DirStatus::OutOfSync; emit newNotification(dirInfo->errors.back().message); } } @@ -774,9 +952,9 @@ void SyncthingConnection::readDirEvent(const QString &eventType, const QJsonObje } } else if(eventType == QLatin1String("FolderScanProgress")) { // FIXME: for some reason current is always 0 - int current = eventData.value(QStringLiteral("current")).toInt(0); - int total = eventData.value(QStringLiteral("total")).toInt(0); - int rate = eventData.value(QStringLiteral("rate")).toInt(0); + int current = eventData.value(QStringLiteral("current")).toInt(0), + total = eventData.value(QStringLiteral("total")).toInt(0), + rate = eventData.value(QStringLiteral("rate")).toInt(0); if(current > 0 && total > 0) { dirInfo->progressPercentage = current * 100 / total; dirInfo->progressRate = rate; @@ -830,6 +1008,46 @@ void SyncthingConnection::readDeviceEvent(DateTime eventTime, const QString &eve } } +/*! + * \brief Reads results of requestEvents(). + */ +void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject &eventData) +{ + VAR_UNUSED(eventTime) + VAR_UNUSED(eventData) +} + +/*! + * \brief Reads results of requestEvents(). + */ +void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject &eventData) +{ + const QString dir(eventData.value(QStringLiteral("folder")).toString()); + if(!dir.isEmpty()) { + int index; + if(SyncthingDir *dirInfo = findDirInfo(dir, index)) { + const QString error(eventData.value(QStringLiteral("error")).toString()), + item(eventData.value(QStringLiteral("item")).toString()); + if(error.isEmpty()) { + if(dirInfo->lastFileTime.isNull() || eventTime < dirInfo->lastFileTime) { + dirInfo->lastFileTime = eventTime, + dirInfo->lastFileName = item, + dirInfo->lastFileDeleted = (eventData.value(QStringLiteral("action")) != QLatin1String("delete")); + if(eventTime > m_lastFileTime) { + m_lastFileTime = dirInfo->lastFileTime, + m_lastFileName = dirInfo->lastFileName, + m_lastFileDeleted = dirInfo->lastFileDeleted; + } + emit dirStatusChanged(*dirInfo, index); + } + } else { + dirInfo->errors.emplace_back(error, item); + emit newNotification(error); + } + } + } +} + /*! * \brief Reads results of rescan(). */ @@ -841,7 +1059,7 @@ void SyncthingConnection::readRescan() case QNetworkReply::NoError: break; default: - emit error(tr("Unable to request rescan: ") + reply->errorString()); + emit error(tr("Unable to request rescan: ") + reply->errorString()); } } @@ -856,7 +1074,7 @@ void SyncthingConnection::readPauseResume() case QNetworkReply::NoError: break; default: - emit error(tr("Unable to request pause/resume: ") + reply->errorString()); + emit error(tr("Unable to request pause/resume: ") + reply->errorString()); } } diff --git a/data/syncthingconnection.h b/data/syncthingconnection.h index b2a99f1..a69c7b6 100644 --- a/data/syncthingconnection.h +++ b/data/syncthingconnection.h @@ -4,6 +4,8 @@ #include #include +#include +#include #include #include @@ -59,13 +61,17 @@ struct SyncthingDir bool autoNormalize = false; int rescanInterval = 0; int minDiskFreePercentage = 0; - DirStatus status = DirStatus::Unknown; + DirStatus status = DirStatus::Idle; int progressPercentage = 0; int progressRate = 0; std::vector errors; int globalBytes = 0, globalDeleted = 0, globalFiles = 0; int localBytes = 0, localDeleted = 0, localFiles = 0; int neededByted = 0, neededFiles = 0; + ChronoUtilities::DateTime lastScanTime; + ChronoUtilities::DateTime lastFileTime; + QString lastFileName; + bool lastFileDeleted = false; bool assignStatus(const QString &statusStr); }; @@ -98,6 +104,7 @@ struct SyncthingDev QString connectionAddress; QString connectionType; QString clientVersion; + ChronoUtilities::DateTime lastSeen; }; struct SyncthingLogEntry @@ -141,8 +148,10 @@ public: const std::vector &devInfo() const; void requestQrCode(const QString &text, std::function callback); void requestLog(std::function &)> callback); + static const QList &expectedCertificateErrors(); public Q_SLOTS: + void loadSelfSignedCertificate(); void connect(); void disconnect(); void reconnect(); @@ -226,6 +235,8 @@ private Q_SLOTS: void requestConfig(); void requestStatus(); void requestConnections(); + void requestDirStatistics(); + void requestDeviceStatistics(); void requestEvents(); void abortAllRequests(); @@ -234,12 +245,16 @@ private Q_SLOTS: void readDevs(const QJsonArray &devs); void readStatus(); void readConnections(); + void readDirStatistics(); + void readDeviceStatistics(); void readEvents(); void readStartingEvent(const QJsonObject &eventData); void readStatusChangedEvent(const QJsonObject &eventData); void readDownloadProgressEvent(const QJsonObject &eventData); void readDirEvent(const QString &eventType, const QJsonObject &eventData); void readDeviceEvent(ChronoUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData); + void readItemStarted(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData); + void readItemFinished(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData); void readRescan(); void readPauseResume(); @@ -277,6 +292,10 @@ private: std::vector m_dirs; std::vector m_devs; ChronoUtilities::DateTime m_lastConnectionsUpdate; + ChronoUtilities::DateTime m_lastFileTime; + QString m_lastFileName; + bool m_lastFileDeleted; + static QList m_expectedCertificateErrors; }; /*! @@ -419,6 +438,15 @@ inline const std::vector &SyncthingConnection::devInfo() const return m_devs; } +/*! + * \brief Returns a list of all expected certificate errors. + * \remarks This list is shared by all instances and updated via loadSelfSignedCertificate(). + */ +inline const QList &SyncthingConnection::expectedCertificateErrors() +{ + return m_expectedCertificateErrors; +} + } #endif // SYNCTHINGCONNECTION_H diff --git a/data/syncthingdevicemodel.cpp b/data/syncthingdevicemodel.cpp index 3ba0ff4..7aed6e4 100644 --- a/data/syncthingdevicemodel.cpp +++ b/data/syncthingdevicemodel.cpp @@ -1,5 +1,8 @@ #include "./syncthingdevicemodel.h" #include "./syncthingconnection.h" +#include "./utils.h" + +using namespace ChronoUtilities; namespace Data { @@ -83,9 +86,10 @@ QVariant SyncthingDeviceModel::data(const QModelIndex &index, int role) const switch(index.row()) { case 0: return tr("ID"); case 1: return tr("Addresses"); - case 2: return tr("Compression"); - case 3: return tr("Certificate"); - case 4: return tr("Introducer"); + case 2: return tr("Last seen"); + case 3: return tr("Compression"); + case 4: return tr("Certificate"); + case 5: return tr("Introducer"); } break; case 1: // attribute values @@ -93,13 +97,44 @@ QVariant SyncthingDeviceModel::data(const QModelIndex &index, int role) const switch(index.row()) { case 0: return dev.id; case 1: return dev.addresses.join(QStringLiteral(", ")); - case 2: return dev.compression; - case 3: return dev.certName.isEmpty() ? tr("none") : dev.certName; - case 4: return dev.introducer ? tr("yes") : tr("no"); + case 2: return dev.lastSeen.isNull() ? tr("unknown or own device") : QString::fromLatin1(dev.lastSeen.toString(DateTimeOutputFormat::DateAndTime, true).data()); + case 3: return dev.compression; + case 4: return dev.certName.isEmpty() ? tr("none") : dev.certName; + case 5: return dev.introducer ? tr("yes") : tr("no"); } break; } break; + case Qt::ForegroundRole: + switch(index.column()) { + case 1: + const SyncthingDev &dev = m_devs[index.parent().row()]; + switch(index.row()) { + case 2: + if(dev.lastSeen.isNull()) { + return QColor(Qt::gray); + } + break; + case 4: + if(dev.certName.isEmpty()) { + return QColor(Qt::gray); + } + break; + } + } + break; + case Qt::ToolTipRole: + switch(index.column()) { + case 1: + switch(index.row()) { + case 2: + const SyncthingDev &dev = m_devs[index.parent().row()]; + if(!dev.lastSeen.isNull()) { + return agoString(dev.lastSeen); + } + break; + } + } default: ; } @@ -196,7 +231,7 @@ int SyncthingDeviceModel::rowCount(const QModelIndex &parent) const if(!parent.isValid()) { return m_devs.size(); } else if(!parent.parent().isValid()) { - return 5; + return 6; } else { return 0; } diff --git a/data/syncthingdirectorymodel.cpp b/data/syncthingdirectorymodel.cpp index 5ac0852..0fc42cb 100644 --- a/data/syncthingdirectorymodel.cpp +++ b/data/syncthingdirectorymodel.cpp @@ -1,5 +1,8 @@ #include "./syncthingdirectorymodel.h" #include "./syncthingconnection.h" +#include "./utils.h" + +using namespace ChronoUtilities; namespace Data { @@ -86,6 +89,8 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const case 2: return tr("Devices"); case 3: return tr("Read-only"); case 4: return tr("Rescan interval"); + case 5: return tr("Last scan"); + case 6: return tr("Last file"); } break; case 1: // attribute values @@ -96,10 +101,53 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const case 2: return dir.devices.join(QStringLiteral(", ")); case 3: return dir.readOnly ? tr("yes") : tr("no"); case 4: return QStringLiteral("%1 s").arg(dir.rescanInterval); + case 5: return dir.lastScanTime.isNull() ? tr("unknown") : QString::fromLatin1(dir.lastScanTime.toString(DateTimeOutputFormat::DateAndTime, true).data()); + case 6: return dir.lastFileName.isEmpty() ? tr("unknown") : dir.lastFileName; } break; } break; + case Qt::ForegroundRole: + switch(index.column()) { + case 1: + const SyncthingDir &dir = m_dirs[index.parent().row()]; + switch(index.row()) { + case 5: + if(dir.lastScanTime.isNull()) { + return QColor(Qt::gray); + } + break; + case 6: + if(dir.lastFileName.isEmpty()) { + return QColor(Qt::gray); + } else if(dir.lastFileDeleted) { + return QColor(Qt::red); + } + break; + } + } + break; + case Qt::ToolTipRole: + switch(index.column()) { + case 1: + const SyncthingDir &dir = m_dirs[index.parent().row()]; + switch(index.row()) { + case 5: + if(!dir.lastScanTime.isNull()) { + return agoString(dir.lastScanTime); + } + break; + case 6: + if(!dir.lastFileTime.isNull()) { + if(dir.lastFileDeleted) { + return tr("Deleted at %1").arg(QString::fromLatin1(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true).data())); + } else { + return tr("Updated at %1").arg(QString::fromLatin1(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true).data())); + } + } + break; + } + } default: ; } @@ -177,7 +225,7 @@ int SyncthingDirectoryModel::rowCount(const QModelIndex &parent) const if(!parent.isValid()) { return m_dirs.size(); } else if(!parent.parent().isValid()) { - return 5; + return 7; } else { return 0; } diff --git a/data/utils.cpp b/data/utils.cpp new file mode 100644 index 0000000..2a64fbe --- /dev/null +++ b/data/utils.cpp @@ -0,0 +1,22 @@ +#include "utils.h" + +#include + +#include +#include + +using namespace ChronoUtilities; + +namespace Data { + +QString agoString(DateTime dateTime) +{ + const TimeSpan delta(DateTime::now() - dateTime); + if(!delta.isNegative() && static_cast(delta.totalTicks()) > (TimeSpan::ticksPerMinute / 4uL)) { + return QCoreApplication::translate("Data::Utils", "%1 ago").arg(QString::fromLatin1(delta.toString(TimeSpanOutputFormat::WithMeasures, true).data())); + } else { + return QCoreApplication::translate("Data::Utils", "right now"); + } +} + +} diff --git a/data/utils.h b/data/utils.h new file mode 100644 index 0000000..badfdef --- /dev/null +++ b/data/utils.h @@ -0,0 +1,18 @@ +#ifndef DATA_UTILS_H +#define DATA_UTILS_H + +#include + +QT_FORWARD_DECLARE_CLASS(QString) + +namespace ChronoUtilities { +class DateTime; +} + +namespace Data { + +QString agoString(ChronoUtilities::DateTime dateTime); + +} + +#endif // DATA_UTILS_H diff --git a/gui/appearanceoptionpage.ui b/gui/appearanceoptionpage.ui index 253e360..f62bc10 100644 --- a/gui/appearanceoptionpage.ui +++ b/gui/appearanceoptionpage.ui @@ -2,29 +2,85 @@ QtGui::AppearanceOptionPage + + + 0 + 0 + 283 + 74 + + Appearance - - - + + + + + + - Show traffic + Menu size - - - - Qt::Vertical + + + + Optional GUI elements - - - 20 - 40 - + + + + + + Traffic statistics - + + + + + + + + 150 + + + 1000 + + + + + + + x + + + Qt::PlainText + + + + + + + 150 + + + 1000 + + + + + + + px + + + Qt::PlainText + + + + diff --git a/gui/autostartoptionpage.ui b/gui/autostartoptionpage.ui index ce4eb3f..2e6c270 100644 --- a/gui/autostartoptionpage.ui +++ b/gui/autostartoptionpage.ui @@ -2,9 +2,20 @@ QtGui::AutostartOptionPage + + + 0 + 0 + 575 + 52 + + Autostart + + + diff --git a/gui/connectionoptionpage.ui b/gui/connectionoptionpage.ui index 5def1b0..fc78718 100644 --- a/gui/connectionoptionpage.ui +++ b/gui/connectionoptionpage.ui @@ -2,17 +2,12 @@ QtGui::ConnectionOptionPage - - - 0 - 0 - 433 - 267 - - Connection + + + @@ -71,28 +66,21 @@ - - - - Qt::Horizontal - - - - + disconnected - + Status - + Apply connection settings and try to reconnect @@ -113,6 +101,33 @@ + + + + font-weight: bold; + + + Insert values from local Syncthing configuration + + + + .. + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + diff --git a/gui/notificationsoptionpage.ui b/gui/notificationsoptionpage.ui index 658c950..39be196 100644 --- a/gui/notificationsoptionpage.ui +++ b/gui/notificationsoptionpage.ui @@ -2,43 +2,46 @@ QtGui::NotificationsOptionPage - - - 0 - 0 - 175 - 156 - - Notifications + + + .. + + + + + Notify on + + + - Notify on disconnect + disconnect - Notify on internal errors + internal errors - Notify on Syncthing errors + errors/notifications from Syncthing - Notify on sync complete + sync complete diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp index cfe9daa..a405f02 100644 --- a/gui/settingsdialog.cpp +++ b/gui/settingsdialog.cpp @@ -2,6 +2,7 @@ #include "../application/settings.h" #include "../data/syncthingconnection.h" +#include "../data/syncthingconfig.h" #include "ui_connectionoptionpage.h" #include "ui_notificationsoptionpage.h" @@ -9,6 +10,8 @@ #include "ui_autostartoptionpage.h" #include "ui_webviewoptionpage.h" +#include "resources/config.h" + #include #include @@ -16,6 +19,10 @@ #include #include +#include +#include +#include + #include using namespace std; @@ -40,12 +47,49 @@ QWidget *ConnectionOptionPage::setupWidget() updateConnectionStatus(); QObject::connect(m_connection, &SyncthingConnection::statusChanged, bind(&ConnectionOptionPage::updateConnectionStatus, this)); QObject::connect(ui()->connectPushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::applyAndReconnect, this)); + QObject::connect(ui()->insertFromConfigFilePushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::insertFromConfigFile, this)); return w; } +void ConnectionOptionPage::insertFromConfigFile() +{ + if(hasBeenShown()) { + QString configFile = SyncthingConfig::locateConfigFile(); + if(configFile.isEmpty()) { + // allow user to select config file manually if it could not be located + configFile = QFileDialog::getOpenFileName(widget(), QCoreApplication::translate("QtGui::ConnectionOptionPage", "Select Syncthing config file") + QStringLiteral(" - " APP_NAME)); + } + if(configFile.isEmpty()) { + return; + } + SyncthingConfig config; + if(!config.restore(configFile)) { + QMessageBox::critical(widget(), widget()->windowTitle() + QStringLiteral(" - " APP_NAME), QCoreApplication::translate("QtGui::ConnectionOptionPage", "Unable to parse the Syncthing config file.")); + return; + } + if(!config.guiAddress.isEmpty()) { + ui()->urlLineEdit->selectAll(); + ui()->urlLineEdit->insert(((config.guiEnforcesSecureConnection || !QHostAddress(config.guiAddress.mid(0, config.guiAddress.indexOf(QChar(':')))).isLoopback()) ? QStringLiteral("https://") : QStringLiteral("http://")) + config.guiAddress); + } + if(!config.guiUser.isEmpty() || !config.guiPasswordHash.isEmpty()) { + ui()->authCheckBox->setChecked(true); + ui()->userNameLineEdit->selectAll(); + ui()->userNameLineEdit->insert(config.guiUser); + } else { + ui()->authCheckBox->setChecked(false); + } + if(!config.guiApiKey.isEmpty()) { + ui()->apiKeyLineEdit->selectAll(); + ui()->apiKeyLineEdit->insert(config.guiApiKey); + } + } +} + void ConnectionOptionPage::updateConnectionStatus() { - ui()->statusLabel->setText(m_connection->statusText()); + if(hasBeenShown()) { + ui()->statusLabel->setText(m_connection->statusText()); + } } bool ConnectionOptionPage::apply() @@ -97,7 +141,7 @@ bool NotificationsOptionPage::apply() { if(hasBeenShown()) { notifyOnDisconnect() = ui()->notifyOnDisconnectCheckBox->isChecked(); - notifyOnErrors() = ui()->notifyOnErrorsCheckBox->isChecked(); + notifyOnInternalErrors() = ui()->notifyOnErrorsCheckBox->isChecked(); notifyOnSyncComplete() = ui()->notifyOnSyncCompleteCheckBox->isChecked(); showSyncthingNotifications() = ui()->showSyncthingNotificationsCheckBox->isChecked(); } @@ -108,7 +152,7 @@ void NotificationsOptionPage::reset() { if(hasBeenShown()) { ui()->notifyOnDisconnectCheckBox->setChecked(notifyOnDisconnect()); - ui()->notifyOnErrorsCheckBox->setChecked(notifyOnErrors()); + ui()->notifyOnErrorsCheckBox->setChecked(notifyOnInternalErrors()); ui()->notifyOnSyncCompleteCheckBox->setChecked(notifyOnSyncComplete()); ui()->showSyncthingNotificationsCheckBox->setChecked(showSyncthingNotifications()); } @@ -125,6 +169,8 @@ AppearanceOptionPage::~AppearanceOptionPage() bool AppearanceOptionPage::apply() { if(hasBeenShown()) { + trayMenuSize().setWidth(ui()->widthSpinBox->value()); + trayMenuSize().setHeight(ui()->heightSpinBox->value()); showTraffic() = ui()->showTrafficCheckBox->isChecked(); } return true; @@ -133,6 +179,8 @@ bool AppearanceOptionPage::apply() void AppearanceOptionPage::reset() { if(hasBeenShown()) { + ui()->widthSpinBox->setValue(trayMenuSize().width()); + ui()->heightSpinBox->setValue(trayMenuSize().height()); ui()->showTrafficCheckBox->setChecked(showTraffic()); } } diff --git a/gui/settingsdialog.h b/gui/settingsdialog.h index 0f4fb07..52ae62a 100644 --- a/gui/settingsdialog.h +++ b/gui/settingsdialog.h @@ -23,6 +23,7 @@ public: ConnectionOptionPage(Data::SyncthingConnection *connection, QWidget *parentWidget = nullptr); private: DECLARE_SETUP_WIDGETS + void insertFromConfigFile(); void updateConnectionStatus(); void applyAndReconnect(); Data::SyncthingConnection *m_connection; diff --git a/gui/trayicon.cpp b/gui/trayicon.cpp index e7e26cc..ab444fd 100644 --- a/gui/trayicon.cpp +++ b/gui/trayicon.cpp @@ -43,7 +43,7 @@ TrayIcon::TrayIcon(QObject *parent) : // connect signals and slots SyncthingConnection *connection = &(m_trayMenu.widget()->connection()); connect(this, &TrayIcon::activated, this, &TrayIcon::handleActivated); - connect(connection, &SyncthingConnection::error, this, &TrayIcon::showSyncthingError); + connect(connection, &SyncthingConnection::error, this, &TrayIcon::showInternalError); connect(connection, &SyncthingConnection::newNotification, this, &TrayIcon::showSyncthingNotification); connect(connection, &SyncthingConnection::statusChanged, this, &TrayIcon::updateStatusIconAndText); } @@ -72,17 +72,17 @@ void TrayIcon::handleActivated(QSystemTrayIcon::ActivationReason reason) } } -void TrayIcon::showSyncthingError(const QString &errorMsg) +void TrayIcon::showInternalError(const QString &errorMsg) { - if(Settings::notifyOnErrors()) { - showMessage(tr("Syncthing error"), errorMsg, QSystemTrayIcon::Critical); + if(Settings::notifyOnInternalErrors()) { + showMessage(tr("Error"), errorMsg, QSystemTrayIcon::Critical); } } void TrayIcon::showSyncthingNotification(const QString &message) { if(Settings::showSyncthingNotifications()) { - showMessage(tr("Syncthing notification"), message, QSystemTrayIcon::Information); + showMessage(tr("Syncthing notification"), message, QSystemTrayIcon::Warning); } } diff --git a/gui/trayicon.h b/gui/trayicon.h index 24ebea6..071d430 100644 --- a/gui/trayicon.h +++ b/gui/trayicon.h @@ -22,10 +22,11 @@ class TrayIcon : public QSystemTrayIcon public: TrayIcon(QObject *parent = nullptr); + TrayMenu &trayMenu(); private slots: void handleActivated(QSystemTrayIcon::ActivationReason reason); - void showSyncthingError(const QString &errorMsg); + void showInternalError(const QString &errorMsg); void showSyncthingNotification(const QString &message); void updateStatusIconAndText(Data::SyncthingStatus status); @@ -43,6 +44,11 @@ private: Data::SyncthingStatus m_status; }; +inline TrayMenu &TrayIcon::trayMenu() +{ + return m_trayMenu; +} + } #endif // TRAY_ICON_H diff --git a/gui/traymenu.cpp b/gui/traymenu.cpp index cc83719..474d196 100644 --- a/gui/traymenu.cpp +++ b/gui/traymenu.cpp @@ -1,6 +1,8 @@ #include "./traymenu.h" #include "./traywidget.h" +#include "../application/settings.h" + #include namespace QtGui { @@ -17,7 +19,7 @@ TrayMenu::TrayMenu(QWidget *parent) : QSize TrayMenu::sizeHint() const { - return QSize(350, 300); + return Settings::trayMenuSize(); } } diff --git a/gui/traywidget.cpp b/gui/traywidget.cpp index 77e05a9..922da4d 100644 --- a/gui/traywidget.cpp +++ b/gui/traywidget.cpp @@ -95,6 +95,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) : connect(m_ui->settingsPushButton, &QPushButton::clicked, this, &TrayWidget::showSettingsDialog); connect(&m_connection, &SyncthingConnection::statusChanged, this, &TrayWidget::updateStatusButton); connect(&m_connection, &SyncthingConnection::trafficChanged, this, &TrayWidget::updateTraffic); + connect(&m_connection, &SyncthingConnection::newNotification, this, &TrayWidget::handleNewNotification); connect(m_ui->dirsTreeView, &DirView::openDir, this, &TrayWidget::openDir); connect(m_ui->dirsTreeView, &DirView::scanDir, this, &TrayWidget::scanDir); connect(m_ui->devsTreeView, &DevView::pauseResumeDev, this, &TrayWidget::pauseResumeDev); @@ -109,7 +110,7 @@ void TrayWidget::showSettingsDialog() { if(!m_settingsDlg) { m_settingsDlg = new SettingsDialog(&m_connection, this); - m_settingsDlg->setWindowTitle(tr("Settings - Syncthing tray")); + m_settingsDlg->setWindowTitle(tr("Settings") + QStringLiteral(" - " APP_NAME)); connect(m_settingsDlg, &SettingsDialog::applied, this, &TrayWidget::applySettings); #ifndef SYNCTHINGTRAY_NO_WEBVIEW if(m_webViewDlg) { @@ -128,8 +129,8 @@ void TrayWidget::showSettingsDialog() void TrayWidget::showAboutDialog() { if(!m_aboutDlg) { - m_aboutDlg = new AboutDialog(this, QString(), QStringLiteral(APP_AUTHOR "\nfallback icons from KDE/Breeze project\nSyncthing icons from Syncthing project"), QString(), QString(), tr("Tray application for Syncthing"), QImage(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); - m_aboutDlg->setWindowTitle(tr("About - Syncthing Tray")); + m_aboutDlg = new AboutDialog(this, QString(), QStringLiteral(APP_AUTHOR "\nfallback icons from KDE/Breeze project\nSyncthing icons from Syncthing project"), QString(), QString(), QStringLiteral(APP_DESCRIPTION), QImage(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); + m_aboutDlg->setWindowTitle(tr("About") + QStringLiteral(" - " APP_NAME)); m_aboutDlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); } m_aboutDlg->show(); @@ -167,7 +168,7 @@ void TrayWidget::showWebUi() void TrayWidget::showOwnDeviceId() { auto *dlg = new QDialog(this); - dlg->setWindowTitle(tr("Own device ID - Syncthing Tray")); + dlg->setWindowTitle(tr("Own device ID") + QStringLiteral(" - " APP_NAME)); dlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setBackgroundRole(QPalette::Background); @@ -201,7 +202,7 @@ void TrayWidget::showOwnDeviceId() void TrayWidget::showLog() { auto *dlg = new QDialog(this); - dlg->setWindowTitle(tr("Log - Syncthing")); + dlg->setWindowTitle(tr("Log") + QStringLiteral(" - " APP_NAME)); dlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); dlg->setAttribute(Qt::WA_DeleteOnClose); auto *layout = new QVBoxLayout(dlg); @@ -255,6 +256,7 @@ void TrayWidget::applySettings() } else { m_connection.setCredentials(QString(), QString()); } + m_connection.loadSelfSignedCertificate(); m_connection.reconnect(); m_ui->trafficFrame->setVisible(Settings::showTraffic()); if(Settings::showTraffic()) { @@ -268,7 +270,7 @@ void TrayWidget::openDir(const QModelIndex &dirIndex) if(QDir(dir->path).exists()) { DesktopUtils::openLocalFileOrDir(dir->path); } else { - QMessageBox::warning(this, QCoreApplication::applicationName(), tr("The directly %1 does not exist on the local machine.").arg(dir->path)); + QMessageBox::warning(this, QCoreApplication::applicationName(), tr("The directory %1 does not exist on the local machine.").arg(dir->path)); } } } @@ -335,4 +337,9 @@ void TrayWidget::handleWebViewDeleted() m_webViewDlg = nullptr; } +void TrayWidget::handleNewNotification(const QString &msg) +{ + // FIXME +} + } diff --git a/gui/traywidget.h b/gui/traywidget.h index 8acb5cd..220eca9 100644 --- a/gui/traywidget.h +++ b/gui/traywidget.h @@ -55,6 +55,7 @@ private slots: void changeStatus(); void updateTraffic(); void handleWebViewDeleted(); + void handleNewNotification(const QString &msg); private: TrayMenu *m_menu; diff --git a/gui/webpage.cpp b/gui/webpage.cpp index 403bf00..e0d2953 100644 --- a/gui/webpage.cpp +++ b/gui/webpage.cpp @@ -13,12 +13,16 @@ #if defined(SYNCTHINGTRAY_USE_WEBENGINE) # include # include +# include #elif defined(SYNCTHINGTRAY_USE_WEBKIT) # include # include # include +# include #endif +using namespace Data; + namespace QtGui { WebPage::WebPage(WEB_VIEW_PROVIDER *view) : @@ -32,6 +36,7 @@ WebPage::WebPage(WEB_VIEW_PROVIDER *view) : settings()->setAttribute(QWebSettings::JavascriptCanOpenWindows, true); setNetworkAccessManager(&Data::networkAccessManager()); connect(&Data::networkAccessManager(), &QNetworkAccessManager::authenticationRequired, this, static_cast(&WebPage::supplyCredentials)); + connect(&Data::networkAccessManager(), &QNetworkAccessManager::sslErrors, this, static_cast &errors)>(&WebPage::handleSslErrors)); #endif if(!m_view) { // delegate to external browser if no view is assigned @@ -50,9 +55,24 @@ WEB_PAGE_PROVIDER *WebPage::createWindow(WEB_PAGE_PROVIDER::WebWindowType type) return new WebPage; } +#ifdef SYNCTHINGTRAY_USE_WEBENGINE +bool WebPage::certificateError(const QWebEngineCertificateError &certificateError) +{ + switch(certificateError.error()) { + case QWebEngineCertificateError::CertificateCommonNameInvalid: + case QWebEngineCertificateError::CertificateAuthorityInvalid: + // FIXME: only ignore the error if the used certificate matches the certificate + // known to be used by the Syncthing GUI + return true; + default: + return false; + } +} +#endif + void WebPage::delegateToExternalBrowser(const QUrl &url) { - openUrlExternal(url); + QDesktopServices::openUrl(url); // this page and the associated view are useless m_view->deleteLater(); deleteLater(); @@ -78,10 +98,14 @@ void WebPage::supplyCredentials(QAuthenticator *authenticator) } } -void WebPage::openUrlExternal(const QUrl &url) +#ifdef SYNCTHINGTRAY_USE_WEBKIT +void WebPage::handleSslErrors(QNetworkReply *reply, const QList &errors) { - QDesktopServices::openUrl(url); + if(reply->request().url().host() == m_view->url().host()) { + reply->ignoreSslErrors(SyncthingConnection::expectedCertificateErrors()); + } } +#endif } diff --git a/gui/webpage.h b/gui/webpage.h index a1cbaf5..37a7d11 100644 --- a/gui/webpage.h +++ b/gui/webpage.h @@ -13,6 +13,7 @@ QT_FORWARD_DECLARE_CLASS(WEB_VIEW_PROVIDER) QT_FORWARD_DECLARE_CLASS(QAuthenticator) QT_FORWARD_DECLARE_CLASS(QNetworkReply) +QT_FORWARD_DECLARE_CLASS(QSslError) namespace QtGui { @@ -22,17 +23,20 @@ class WebPage : public WEB_PAGE_PROVIDER public: WebPage(WEB_VIEW_PROVIDER *view = nullptr); -public slots: - void openUrlExternal(const QUrl &url); - protected: WEB_PAGE_PROVIDER *createWindow(WebWindowType type); +#ifdef SYNCTHINGTRAY_USE_WEBENGINE + bool certificateError(const QWebEngineCertificateError &certificateError); +#endif private slots: void delegateToExternalBrowser(const QUrl &url); void supplyCredentials(const QUrl &requestUrl, QAuthenticator *authenticator); void supplyCredentials(QNetworkReply *reply, QAuthenticator *authenticator); void supplyCredentials(QAuthenticator *authenticator); +#ifdef SYNCTHINGTRAY_USE_WEBKIT + void handleSslErrors(QNetworkReply *, const QList &errors); +#endif private: WEB_VIEW_PROVIDER *m_view; diff --git a/gui/webviewdialog.cpp b/gui/webviewdialog.cpp index 21ac7df..7ebdf95 100644 --- a/gui/webviewdialog.cpp +++ b/gui/webviewdialog.cpp @@ -3,7 +3,6 @@ #include "./webpage.h" #include "../application/settings.h" -#include "../data/syncthingconnection.h" #include @@ -29,6 +28,7 @@ WebViewDialog::WebViewDialog(QWidget *parent) : m_view->setPage(new WebPage(m_view)); + applySettings(); if(Settings::webViewGeometry().isEmpty()) { @@ -60,4 +60,4 @@ void QtGui::WebViewDialog::closeEvent(QCloseEvent *event) } -#endif +#endif // SYNCTHINGTRAY_NO_WEBVIEW diff --git a/gui/webviewdialog.h b/gui/webviewdialog.h index 54bc1f7..0b1eff0 100644 --- a/gui/webviewdialog.h +++ b/gui/webviewdialog.h @@ -29,5 +29,5 @@ private: } -#endif +#endif // SYNCTHINGTRAY_NO_WEBVIEW #endif // WEBVIEW_DIALOG_H diff --git a/translations/syncthingtray_de_DE.ts b/translations/syncthingtray_de_DE.ts index 995d9ed..62cc2f3 100644 --- a/translations/syncthingtray_de_DE.ts +++ b/translations/syncthingtray_de_DE.ts @@ -4,89 +4,119 @@ Data::SyncthingConnection - + disconnected - + connected - + connected, notifications available - + connected, paused - + connected, synchronizing - + unknown - + Unable to parse Syncthing log: - + Unable to request system log: - - + + Unable to locate certificate used by Syncthing GUI. + + + + + Unable to load certificate used by Syncthing GUI. + + + + + Unable to parse Syncthing config: - - + + Unable to request Syncthing config: - + Unable to parse connections: - + Unable to request connections: - + + Unable to parse directory statistics: + + + + + Unable to request directory statistics: + + + + + Unable to parse device statistics: + + + + + Unable to request device statistics: + + + + Unable to parse Syncthing events: - + Unable to request Syncthing events: - + Unable to request rescan: - + Unable to request pause/resume: - + Unable to request QR-Code: @@ -94,93 +124,103 @@ Data::SyncthingDeviceModel - - + + ID - + Status - + Addresses - + + Last seen + + + + Compression - + Certificate - + Introducer - + none - + yes - + no - + Unknown status - + Idle - + Disconnected - + Synchronizing (%1 %) - + Synchronizing - + Own device - + + unknown or own device + + + + Paused - + Out of sync - + Rejected @@ -188,109 +228,168 @@ Data::SyncthingDirectoryModel - - + + ID - + Status - + Path - + Devices - + Rescan interval - + + Last scan + + + + + Last file + + + + yes - + no - + + + unknown + + + + + Deleted at %1 + + + + + Updated at %1 + + + + Idle - + Scanning (%1 %) - + Synchronizing (%1 %) - + Out of sync - + Scanning - + Read-only - + Unknown status - + Synchronizing - + Paused + + Data::Utils + + + %1 ago + + + + + right now + + + QtGui::AppearanceOptionPage - + Appearance - - Show traffic + + Menu size + + + + + Optional GUI elements + + + + + Traffic statistics + + + + + x + + + + + px QtGui::AutostartOptionPage - + Autostart - + <html><head/><body><p><span style=" font-weight:600;">Not implemented yet</span></p><p>This will allow launching the tray when the desktop environment starts and to launch Syncthing when the tray is started.</p></body></html> @@ -298,50 +397,65 @@ QtGui::ConnectionOptionPage - + Connection - + Syncthing URL - + Authentication - + User - + disconnected - + Status - + Apply connection settings and try to reconnect - + API key - + + Insert values from local Syncthing configuration + + + + Password + + + Select Syncthing config file + + + + + Unable to parse the Syncthing config file. + + QtGui::DevView @@ -387,35 +501,40 @@ - - Notify on disconnect + + Notify on - - Notify on internal errors + + disconnect - - Notify on Syncthing errors + + internal errors - - Notify on sync complete + + Syncthing errors + + + + + sync complete QtGui::SettingsDialog - + Tray - + Web view @@ -524,8 +643,8 @@ - - + + unknown @@ -551,11 +670,13 @@ + About + Settings @@ -569,16 +690,6 @@ View own device ID - - - Settings - Syncthing tray - - - - - Tray application for Syncthing - - Rescan all directories @@ -589,16 +700,6 @@ Show Syncthing log - - - About - Syncthing Tray - - - - - Own device ID - Syncthing Tray - - device ID is unknown @@ -609,11 +710,6 @@ Copy to clipboard - - - Log - Syncthing - - Not connected to Syncthing, click to connect @@ -640,7 +736,17 @@ - + + Own device ID + + + + + Log + + + + The directly <i>%1</i> does not exist on the local machine. @@ -648,7 +754,7 @@ QtGui::WebViewDialog - + Syncthing @@ -657,7 +763,7 @@ QtGui::WebViewOptionPage - + General @@ -687,7 +793,7 @@ - + Syncthing Tray has not been built with vieb view support utilizing either Qt WebKit or Qt WebEngine. The Web UI will be opened in the default web browser instead. @@ -696,13 +802,23 @@ The Web UI will be opened in the default web browser instead. main - - The system tray is (currently) not available. + + You must configure how to connect to Syncthing when using Syncthing Tray the first time. - - The Qt libraries have not been built with tray icon support. + + Note that the settings dialog allows importing URL, credentials and API-key from the local Syncthing configuration. + + + + + The system tray is (currently) not available. You could open the tray menu as a regular window using the -w flag, though. + + + + + The Qt libraries have not been built with tray icon support. You could open the tray menu as a regular window using the -w flag, though. diff --git a/translations/syncthingtray_en_US.ts b/translations/syncthingtray_en_US.ts index 94e7b1c..a3cff65 100644 --- a/translations/syncthingtray_en_US.ts +++ b/translations/syncthingtray_en_US.ts @@ -4,89 +4,119 @@ Data::SyncthingConnection - + disconnected - + connected - + connected, notifications available - + connected, paused - + connected, synchronizing - + unknown - + Unable to parse Syncthing log: - + Unable to request system log: - - + + Unable to locate certificate used by Syncthing GUI. + + + + + Unable to load certificate used by Syncthing GUI. + + + + + Unable to parse Syncthing config: - - + + Unable to request Syncthing config: - + Unable to parse connections: - + Unable to request connections: - + + Unable to parse directory statistics: + + + + + Unable to request directory statistics: + + + + + Unable to parse device statistics: + + + + + Unable to request device statistics: + + + + Unable to parse Syncthing events: - + Unable to request Syncthing events: - + Unable to request rescan: - + Unable to request pause/resume: - + Unable to request QR-Code: @@ -94,93 +124,103 @@ Data::SyncthingDeviceModel - - + + ID - + Status - + Addresses - + + Last seen + + + + Compression - + Certificate - + Introducer - + none - + yes - + no - + Unknown status - + Idle - + Disconnected - + Synchronizing (%1 %) - + Synchronizing - + Own device - + + unknown or own device + + + + Paused - + Out of sync - + Rejected @@ -188,109 +228,168 @@ Data::SyncthingDirectoryModel - - + + ID - + Status - + Path - + Devices - + Rescan interval - + + Last scan + + + + + Last file + + + + yes - + no - + + + unknown + + + + + Deleted at %1 + + + + + Updated at %1 + + + + Idle - + Scanning (%1 %) - + Synchronizing (%1 %) - + Out of sync - + Scanning - + Read-only - + Unknown status - + Synchronizing - + Paused + + Data::Utils + + + %1 ago + + + + + right now + + + QtGui::AppearanceOptionPage - + Appearance - - Show traffic + + Menu size + + + + + Optional GUI elements + + + + + Traffic statistics + + + + + x + + + + + px QtGui::AutostartOptionPage - + Autostart - + <html><head/><body><p><span style=" font-weight:600;">Not implemented yet</span></p><p>This will allow launching the tray when the desktop environment starts and to launch Syncthing when the tray is started.</p></body></html> @@ -298,50 +397,65 @@ QtGui::ConnectionOptionPage - + Connection - + Syncthing URL - + Authentication - + User - + disconnected - + Status - + Apply connection settings and try to reconnect - + API key - + + Insert values from local Syncthing configuration + + + + Password + + + Select Syncthing config file + + + + + Unable to parse the Syncthing config file. + + QtGui::DevView @@ -387,35 +501,40 @@ - - Notify on disconnect + + Notify on - - Notify on internal errors + + disconnect - - Notify on Syncthing errors + + internal errors - - Notify on sync complete + + Syncthing errors + + + + + sync complete QtGui::SettingsDialog - + Tray - + Web view @@ -524,8 +643,8 @@ - - + + unknown @@ -551,11 +670,13 @@ + About + Settings @@ -569,16 +690,6 @@ View own device ID - - - Settings - Syncthing tray - - - - - Tray application for Syncthing - - Rescan all directories @@ -589,16 +700,6 @@ Show Syncthing log - - - About - Syncthing Tray - - - - - Own device ID - Syncthing Tray - - device ID is unknown @@ -609,11 +710,6 @@ Copy to clipboard - - - Log - Syncthing - - Not connected to Syncthing, click to connect @@ -640,7 +736,17 @@ - + + Own device ID + + + + + Log + + + + The directly <i>%1</i> does not exist on the local machine. @@ -648,7 +754,7 @@ QtGui::WebViewDialog - + Syncthing @@ -657,7 +763,7 @@ QtGui::WebViewOptionPage - + General @@ -687,7 +793,7 @@ - + Syncthing Tray has not been built with vieb view support utilizing either Qt WebKit or Qt WebEngine. The Web UI will be opened in the default web browser instead. @@ -696,13 +802,23 @@ The Web UI will be opened in the default web browser instead. main - - The system tray is (currently) not available. + + You must configure how to connect to Syncthing when using Syncthing Tray the first time. - - The Qt libraries have not been built with tray icon support. + + Note that the settings dialog allows importing URL, credentials and API-key from the local Syncthing configuration. + + + + + The system tray is (currently) not available. You could open the tray menu as a regular window using the -w flag, though. + + + + + The Qt libraries have not been built with tray icon support. You could open the tray menu as a regular window using the -w flag, though.