diff --git a/CMakeLists.txt b/CMakeLists.txt index 79dbb34..3bb61a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,29 +90,36 @@ set(DOC_FILES ) set(REQUIRED_ICONS - network-card - window-close + dialog-cancel + dialog-ok + dialog-ok-apply edit-copy edit-paste - preferences-other - view-barcode - folder-open - media-playback-start - text-plain - help-about - media-playback-pause - view-refresh folder - network-server + folder-open folder-sync + help-about internet-web-browser + list-add + list-remove + media-playback-pause + media-playback-start + network-card network-connect - system-run - system-search + network-server preferences-desktop - preferences-desktop-notification preferences-desktop-icons preferences-desktop-locale + preferences-desktop-notification + preferences-other + qtcreator + system-run + system-search + system-file-manager + text-plain + view-barcode + view-refresh + window-close ) # find c++utilities diff --git a/README.md b/README.md index 4ae8e32..a8d6470 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,11 @@ Qt 5-based tray application for [Syncthing](https://github.com/syncthing/syncthi * 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) +* Allows quickly switching between multiple Syncthing instances * Still under development; the following features are planned - * Connect to multiple instances of Syncthing at a time * Show currently processed items * Show recently processed items + * Improve notification handling ## Screenshots ### Under Openbox/Tint2 diff --git a/application/main.cpp b/application/main.cpp index 1227923..eda1e7b 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -64,12 +64,12 @@ int main(int argc, char *argv[]) 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(); + trayIcon.trayMenu().widget()->showSettingsDialog(); } res = application.exec(); } else { diff --git a/application/settings.cpp b/application/settings.cpp index cac1fee..9f51f7a 100644 --- a/application/settings.cpp +++ b/application/settings.cpp @@ -7,7 +7,11 @@ #include #include #include +#include +#include +#include +using namespace std; using namespace Media; namespace Settings { @@ -19,29 +23,15 @@ bool &firstLaunch() } // connection -QString &syncthingUrl() +ConnectionSettings &primaryConnectionSettings() { - static QString v; + static ConnectionSettings v; return v; } -bool &authEnabled() + +std::vector &secondaryConnectionSettings() { - static bool v = false; - return v; -} -QString &userName() -{ - static QString v; - return v; -} -QString &password() -{ - static QString v; - return v; -} -QByteArray &apiKey() -{ - static QByteArray v; + static vector v; return v; } @@ -141,17 +131,45 @@ 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(); - 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(); + + const int connectionCount = settings.beginReadArray(QStringLiteral("connections")); + if(connectionCount > 0) { + secondaryConnectionSettings().clear(); + secondaryConnectionSettings().reserve(static_cast(connectionCount)); + for(int i = 0; i < connectionCount; ++i) { + ConnectionSettings *connectionSettings; + if(i == 0) { + connectionSettings = &primaryConnectionSettings(); + } else { + secondaryConnectionSettings().emplace_back(); + connectionSettings = &secondaryConnectionSettings().back(); + } + settings.setArrayIndex(i); + connectionSettings->label = settings.value(QStringLiteral("label")).toString(); + if(connectionSettings->label.isEmpty()) { + connectionSettings->label = (i == 0 ? QStringLiteral("Primary instance") : QStringLiteral("Secondary instance %1").arg(i)); + } + connectionSettings->syncthingUrl = settings.value(QStringLiteral("syncthingUrl"), connectionSettings->syncthingUrl).toString(); + connectionSettings->authEnabled = settings.value(QStringLiteral("authEnabled"), connectionSettings->authEnabled).toBool(); + connectionSettings->userName = settings.value(QStringLiteral("userName")).toString(); + connectionSettings->password = settings.value(QStringLiteral("password")).toString(); + connectionSettings->apiKey = settings.value(QStringLiteral("apiKey")).toByteArray(); + connectionSettings->httpsCertPath = settings.value(QStringLiteral("httpsCertPath")).toString(); + if(!connectionSettings->loadHttpsCert()) { + QMessageBox::critical(nullptr, QCoreApplication::applicationName(), QCoreApplication::translate("Settings::restore", "Unable to load certificate \"%1\" when restoring settings.").arg(connectionSettings->httpsCertPath)); + } + } + } else { + firstLaunch() = true; + primaryConnectionSettings().label = QStringLiteral("Primary instance"); + } + settings.endArray(); + + notifyOnDisconnect() = settings.value(QStringLiteral("notifyOnDisconnect"), notifyOnDisconnect()).toBool(); + notifyOnInternalErrors() = settings.value(QStringLiteral("notifyOnErrors"), notifyOnInternalErrors()).toBool(); + notifyOnSyncComplete() = settings.value(QStringLiteral("notifyOnSyncComplete"), notifyOnSyncComplete()).toBool(); + showSyncthingNotifications() = settings.value(QStringLiteral("showSyncthingNotifications"), showSyncthingNotifications()).toBool(); + showTraffic() = settings.value(QStringLiteral("showTraffic"), showTraffic()).toBool(); trayMenuSize() = settings.value(QStringLiteral("trayMenuSize"), trayMenuSize()).toSize(); frameStyle() = settings.value(QStringLiteral("frameStyle"), frameStyle()).toInt(); settings.endGroup(); @@ -179,11 +197,21 @@ void save() QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); settings.beginGroup(QStringLiteral("tray")); - settings.setValue(QStringLiteral("syncthingUrl"), syncthingUrl()); - settings.setValue(QStringLiteral("authEnabled"), authEnabled()); - settings.setValue(QStringLiteral("userName"), userName()); - settings.setValue(QStringLiteral("password"), password()); - settings.setValue(QStringLiteral("apiKey"), apiKey()); + const int connectionCount = static_cast(1 + secondaryConnectionSettings().size()); + settings.beginWriteArray(QStringLiteral("connections"), connectionCount); + for(int i = 0; i < connectionCount; ++i) { + const ConnectionSettings *connectionSettings = (i == 0 ? &primaryConnectionSettings() : &secondaryConnectionSettings()[static_cast(i - 1)]); + settings.setArrayIndex(i); + settings.setValue(QStringLiteral("label"), connectionSettings->label); + settings.setValue(QStringLiteral("syncthingUrl"), connectionSettings->syncthingUrl); + settings.setValue(QStringLiteral("authEnabled"), connectionSettings->authEnabled); + settings.setValue(QStringLiteral("userName"), connectionSettings->userName); + settings.setValue(QStringLiteral("password"), connectionSettings->password); + settings.setValue(QStringLiteral("apiKey"), connectionSettings->apiKey); + settings.setValue(QStringLiteral("httpsCertPath"), connectionSettings->httpsCertPath); + } + settings.endArray(); + settings.setValue(QStringLiteral("notifyOnDisconnect"), notifyOnDisconnect()); settings.setValue(QStringLiteral("notifyOnErrors"), notifyOnInternalErrors()); settings.setValue(QStringLiteral("notifyOnSyncComplete"), notifyOnSyncComplete()); @@ -211,4 +239,21 @@ void save() qtSettings().save(settings); } +bool ConnectionSettings::loadHttpsCert() +{ + if(!httpsCertPath.isEmpty()) { + const QList cert = QSslCertificate::fromPath(httpsCertPath); + if(cert.isEmpty()) { + return false; + } + expectedSslErrors.clear(); + expectedSslErrors.reserve(4); + expectedSslErrors << QSslError(QSslError::UnableToGetLocalIssuerCertificate, cert.at(0)); + expectedSslErrors << QSslError(QSslError::UnableToVerifyFirstCertificate, cert.at(0)); + expectedSslErrors << QSslError(QSslError::SelfSignedCertificate, cert.at(0)); + expectedSslErrors << QSslError(QSslError::HostNameMismatch, cert.at(0)); + } + return true; +} + } diff --git a/application/settings.h b/application/settings.h index 780b389..6cfc417 100644 --- a/application/settings.h +++ b/application/settings.h @@ -3,10 +3,12 @@ #include -#include +#include +#include +#include + +#include -QT_FORWARD_DECLARE_CLASS(QByteArray) -QT_FORWARD_DECLARE_CLASS(QString) QT_FORWARD_DECLARE_CLASS(QSize) namespace Media { @@ -23,11 +25,19 @@ namespace Settings { bool &firstLaunch(); // connection -QString &syncthingUrl(); -bool &authEnabled(); -QString &userName(); -QString &password(); -QByteArray &apiKey(); +struct ConnectionSettings { + QString label; + QString syncthingUrl; + bool authEnabled = false; + QString userName; + QString password; + QByteArray apiKey; + QString httpsCertPath; + QList expectedSslErrors; + bool loadHttpsCert(); +}; +ConnectionSettings &primaryConnectionSettings(); +std::vector &secondaryConnectionSettings(); // notifications bool ¬ifyOnDisconnect(); diff --git a/data/syncthingconfig.cpp b/data/syncthingconfig.cpp index e0536cf..d9a59ee 100644 --- a/data/syncthingconfig.cpp +++ b/data/syncthingconfig.cpp @@ -8,7 +8,7 @@ namespace Data { /*! * \struct SyncthingConfig - * \brief The SyncthingConfig struct holds the configuration of Syncthing itself read from config.xml in the Syncthing home directory. + * \brief The SyncthingConfig struct holds the configuration of the local Syncthing instance 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. */ diff --git a/data/syncthingconnection.cpp b/data/syncthingconnection.cpp index 0aefdda..862237e 100644 --- a/data/syncthingconnection.cpp +++ b/data/syncthingconnection.cpp @@ -1,6 +1,8 @@ #include "./syncthingconnection.h" #include "./syncthingconfig.h" +#include "../application/settings.h" + #include #include @@ -102,8 +104,6 @@ bool SyncthingDir::assignStatus(DirStatus newStatus, DateTime time) * \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(). */ @@ -165,6 +165,10 @@ void SyncthingConnection::connect() { if(!isConnected()) { m_reconnecting = m_hasConfig = m_hasStatus = false; + if(m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) { + emit error(tr("Connection configuration is insufficient.")); + return; + } requestConfig(); requestStatus(); m_keepPolling = true; @@ -182,6 +186,7 @@ void SyncthingConnection::disconnect() /*! * \brief Disconnects if connected, then (re-)connects asynchronously. + * \remarks Clears the currently cached configuration. */ void SyncthingConnection::reconnect() { @@ -190,10 +195,63 @@ void SyncthingConnection::reconnect() m_hasConfig = m_hasStatus = false; abortAllRequests(); } else { - connect(); + continueReconnect(); } } +/*! + * \brief Applies the specifies configuration and tries to reconnect via reconnect(). + * \remarks The expected SSL errors of the specified configuration are updated accordingly. + */ +void SyncthingConnection::reconnect(Settings::ConnectionSettings &connectionSettings) +{ + setSyncthingUrl(connectionSettings.syncthingUrl); + setApiKey(connectionSettings.apiKey); + if(connectionSettings.authEnabled) { + setCredentials(connectionSettings.userName, connectionSettings.password); + } else { + setCredentials(QString(), QString()); + } + loadSelfSignedCertificate(); + if(connectionSettings.expectedSslErrors.isEmpty()) { + connectionSettings.expectedSslErrors = expectedSslErrors(); + } + reconnect(); +} + +/*! + * \brief Internally called to reconnect; ensures currently cached config is cleared. + */ +void SyncthingConnection::continueReconnect() +{ + emit newConfig(QJsonObject()); // configuration will be invalidated + m_status = SyncthingStatus::Disconnected; + m_keepPolling = true; + m_reconnecting = false; + m_lastEventId = 0; + m_configDir.clear(); + m_myId.clear(); + m_totalIncomingTraffic = 0; + m_totalOutgoingTraffic = 0; + m_totalIncomingRate = 0.0; + m_totalOutgoingRate = 0.0; + m_unreadNotifications = false; + m_hasConfig = false; + m_hasStatus = false; + m_dirs.clear(); + m_devs.clear(); + m_lastConnectionsUpdate = DateTime(); + m_lastFileTime = DateTime(); + m_lastFileName.clear(); + m_lastFileDeleted = false; + if(m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) { + emit error(tr("Connection configuration is insufficient.")); + return; + } + requestConfig(); + requestStatus(); +} + void SyncthingConnection::pause(const QString &dev) { QUrlQuery query; @@ -273,7 +331,7 @@ QNetworkRequest SyncthingConnection::prepareRequest(const QString &path, const Q QNetworkReply *SyncthingConnection::requestData(const QString &path, const QUrlQuery &query, bool rest) { auto *reply = networkAccessManager().get(prepareRequest(path, query, rest)); - reply->ignoreSslErrors(m_expectedCertificateErrors); + reply->ignoreSslErrors(m_expectedSslErrors); return reply; } @@ -283,7 +341,7 @@ QNetworkReply *SyncthingConnection::requestData(const QString &path, const QUrlQ QNetworkReply *SyncthingConnection::postData(const QString &path, const QUrlQuery &query, const QByteArray &data) { auto *reply = networkAccessManager().post(prepareRequest(path, query), data); - reply->ignoreSslErrors(m_expectedCertificateErrors); + reply->ignoreSslErrors(m_expectedSslErrors); return reply; } @@ -470,14 +528,15 @@ QMetaObject::Connection SyncthingConnection::requestLog(std::function &devInfo() const; QMetaObject::Connection requestQrCode(const QString &text, std::function callback); QMetaObject::Connection requestLog(std::function &)> callback); - static const QList &expectedCertificateErrors(); + const QList &expectedSslErrors(); public Q_SLOTS: void loadSelfSignedCertificate(); void connect(); void disconnect(); void reconnect(); + void reconnect(Settings::ConnectionSettings &connectionSettings); void pause(const QString &dev); void pauseAllDevs(); void resume(const QString &dev); @@ -263,6 +268,7 @@ private Q_SLOTS: void readPauseResume(); void readRestart(); + void continueReconnect(); void setStatus(SyncthingStatus status); private: @@ -300,7 +306,7 @@ private: ChronoUtilities::DateTime m_lastFileTime; QString m_lastFileName; bool m_lastFileDeleted; - static QList m_expectedCertificateErrors; + QList m_expectedSslErrors; }; /*! @@ -447,9 +453,9 @@ inline const std::vector &SyncthingConnection::devInfo() const * \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() +inline const QList &SyncthingConnection::expectedSslErrors() { - return m_expectedCertificateErrors; + return m_expectedSslErrors; } } diff --git a/gui/autostartoptionpage.ui b/gui/autostartoptionpage.ui index a11b367..d9d3040 100644 --- a/gui/autostartoptionpage.ui +++ b/gui/autostartoptionpage.ui @@ -47,6 +47,12 @@ + + + 0 + 0 + + true diff --git a/gui/connectionoptionpage.ui b/gui/connectionoptionpage.ui index 45f8ba4..bbf21f1 100644 --- a/gui/connectionoptionpage.ui +++ b/gui/connectionoptionpage.ui @@ -2,6 +2,14 @@ QtGui::ConnectionOptionPage + + + 0 + 0 + 502 + 368 + + Connection @@ -10,30 +18,114 @@ .. - - 6 - + + + + 75 + true + + + + Config label + + + + + + + + 0 + 0 + + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + true + + + + + + + + 0 + 0 + + + + Add secondary instance + + + + .. + + + + + + + + 0 + 0 + + + + Remove currently selected secondary instance + + + + .. + + + + + + + Syncthing URL - + - + Authentication - + - + false @@ -43,7 +135,14 @@ - + + + + false + + + + false @@ -53,14 +152,7 @@ - - - - false - - - - + false @@ -70,42 +162,17 @@ - - - - disconnected - - - - - - - Status - - - - - - - Apply connection settings and try to reconnect - - - - .. - - - - - - - + API key - + + + + font-weight: bold; @@ -119,8 +186,8 @@ - - + + 0 @@ -132,6 +199,83 @@ + + + + Status + + + + + + + disconnected + + + + + + + Apply connection settings and try to reconnect with the currently selected config + + + + .. + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + 16777215 + 42 + + + + It is possible to save multiple configurations. This allows switching quickly between multiple Syncthing instances using the arrow in the right corner of the tray menu. The config label is an arbitrary name to identify a configuration and must not match the name of the corresponding Syncthing device. + + + true + + + + + + + HTTPS certificate + + + + + + + + 0 + 0 + + + + @@ -140,6 +284,12 @@ QLineEdit
qtutilities/widgets/clearlineedit.h
+ + Widgets::PathSelection + QWidget +
qtutilities/widgets/pathselection.h
+ 1 +
diff --git a/gui/launcheroptionpage.ui b/gui/launcheroptionpage.ui index 374cd83..cfbef8b 100644 --- a/gui/launcheroptionpage.ui +++ b/gui/launcheroptionpage.ui @@ -6,7 +6,7 @@ 0 0 - 457 + 517 345 @@ -36,6 +36,15 @@ 30 + + 0 + + + 0 + + + 0 + diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp index e494821..82756d0 100644 --- a/gui/settingsdialog.cpp +++ b/gui/settingsdialog.cpp @@ -1,6 +1,5 @@ #include "./settingsdialog.h" -#include "../application/settings.h" #include "../data/syncthingconnection.h" #include "../data/syncthingconfig.h" #include "../data/syncthingprocess.h" @@ -14,9 +13,6 @@ #include "resources/config.h" -#include -#include - #include #include #include @@ -45,7 +41,8 @@ namespace QtGui { // ConnectionOptionPage ConnectionOptionPage::ConnectionOptionPage(Data::SyncthingConnection *connection, QWidget *parentWidget) : ConnectionOptionPageBase(parentWidget), - m_connection(connection) + m_connection(connection), + m_currentIndex(0) {} ConnectionOptionPage::~ConnectionOptionPage() @@ -55,9 +52,15 @@ QWidget *ConnectionOptionPage::setupWidget() { auto *w = ConnectionOptionPageBase::setupWidget(); updateConnectionStatus(); + ui()->certPathSelection->provideCustomFileMode(QFileDialog::ExistingFile); + ui()->certPathSelection->lineEdit()->setPlaceholderText(QCoreApplication::translate("QtGui::ConnectionOptionPage", "Auto-detected for local instance")); 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)); + QObject::connect(ui()->selectionComboBox, static_cast(&QComboBox::currentIndexChanged), bind(&ConnectionOptionPage::showConnectionSettings, this, _1)); + QObject::connect(ui()->selectionComboBox, static_cast(&QComboBox::editTextChanged), bind(&ConnectionOptionPage::saveCurrentConnectionName, this, _1)); + QObject::connect(ui()->addPushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::addConnectionSettings, this)); + QObject::connect(ui()->removePushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::removeConnectionSettings, this)); return w; } @@ -102,41 +105,114 @@ void ConnectionOptionPage::updateConnectionStatus() } } -bool ConnectionOptionPage::apply() +bool ConnectionOptionPage::showConnectionSettings(int index) { - if(hasBeenShown()) { - syncthingUrl() = ui()->urlLineEdit->text(); - authEnabled() = ui()->authCheckBox->isChecked(); - userName() = ui()->userNameLineEdit->text(); - password() = ui()->passwordLineEdit->text(); - apiKey() = ui()->apiKeyLineEdit->text().toUtf8(); + bool ok = true; + if(index != m_currentIndex) { + if((ok = cacheCurrentSettings(false))) { + const ConnectionSettings &connectionSettings = (index == 0 ? m_primarySettings : m_secondarySettings[static_cast(index - 1)]); + ui()->urlLineEdit->setText(connectionSettings.syncthingUrl); + ui()->authCheckBox->setChecked(connectionSettings.authEnabled); + ui()->userNameLineEdit->setText(connectionSettings.userName); + ui()->passwordLineEdit->setText(connectionSettings.password); + ui()->apiKeyLineEdit->setText(connectionSettings.apiKey); + ui()->certPathSelection->lineEdit()->setText(connectionSettings.httpsCertPath); + m_currentIndex = index; + } else { + ui()->selectionComboBox->setCurrentIndex(m_currentIndex); + } } - return true; + ui()->removePushButton->setEnabled(index); + return ok; +} + +bool ConnectionOptionPage::cacheCurrentSettings(bool applying) +{ + bool ok = true; + if(m_currentIndex >= 0) { + ConnectionSettings &connectionSettings = (m_currentIndex == 0 ? m_primarySettings : m_secondarySettings[static_cast(m_currentIndex - 1)]); + connectionSettings.syncthingUrl = ui()->urlLineEdit->text(); + connectionSettings.authEnabled = ui()->authCheckBox->isChecked(); + connectionSettings.userName = ui()->userNameLineEdit->text(); + connectionSettings.password = ui()->passwordLineEdit->text(); + connectionSettings.apiKey = ui()->apiKeyLineEdit->text().toUtf8(); + connectionSettings.expectedSslErrors.clear(); + connectionSettings.httpsCertPath = ui()->certPathSelection->lineEdit()->text(); + if(!connectionSettings.loadHttpsCert()) { + const QString errorMessage = QCoreApplication::translate("QtGui::ConnectionOptionPage", "Unable to load specified certificate \"%1\".").arg(connectionSettings.httpsCertPath); + if(!applying) { + QMessageBox::critical(widget(), QCoreApplication::applicationName(), errorMessage); + } else { + errors() << errorMessage; + } + ok = false; + } + } + return ok; +} + +void ConnectionOptionPage::saveCurrentConnectionName(const QString &name) +{ + const int index = ui()->selectionComboBox->currentIndex(); + if(index == m_currentIndex && index >= 0) { + (index == 0 ? m_primarySettings : m_secondarySettings[static_cast(index - 1)]).label = name; + ui()->selectionComboBox->setItemText(index, name); + } +} + +void ConnectionOptionPage::addConnectionSettings() +{ + m_secondarySettings.emplace_back(); + m_secondarySettings.back().label = QCoreApplication::translate("QtGui::ConnectionOptionPage", "Instance %1").arg(ui()->selectionComboBox->count() + 1); + ui()->selectionComboBox->addItem(m_secondarySettings.back().label); + ui()->selectionComboBox->setCurrentIndex(ui()->selectionComboBox->count() - 1); +} + +void ConnectionOptionPage::removeConnectionSettings() +{ + int index = ui()->selectionComboBox->currentIndex(); + if(index > 0) { + m_secondarySettings.erase(m_secondarySettings.begin() + (index - 1)); + m_currentIndex = -1; + ui()->selectionComboBox->removeItem(index); + } +} + +bool ConnectionOptionPage::apply() +{ + bool ok = true; + if(hasBeenShown()) { + ok = cacheCurrentSettings(true); + Settings::primaryConnectionSettings() = m_primarySettings; + Settings::secondaryConnectionSettings() = m_secondarySettings; + } + return ok; } void ConnectionOptionPage::reset() { if(hasBeenShown()) { - ui()->urlLineEdit->setText(syncthingUrl()); - ui()->authCheckBox->setChecked(authEnabled()); - ui()->userNameLineEdit->setText(userName()); - ui()->passwordLineEdit->setText(password()); - ui()->apiKeyLineEdit->setText(apiKey()); + m_primarySettings = primaryConnectionSettings(); + m_secondarySettings = secondaryConnectionSettings(); + m_currentIndex = -1; + + QStringList itemTexts; + itemTexts.reserve(1 + static_cast(m_secondarySettings.size())); + itemTexts << m_primarySettings.label; + for(const ConnectionSettings &settings : m_secondarySettings) { + itemTexts << settings.label; + } + ui()->selectionComboBox->clear(); + ui()->selectionComboBox->addItems(itemTexts); + ui()->selectionComboBox->setCurrentIndex(0); } } void ConnectionOptionPage::applyAndReconnect() { apply(); - m_connection->setSyncthingUrl(Settings::syncthingUrl()); - m_connection->setApiKey(Settings::apiKey()); - if(Settings::authEnabled()) { - m_connection->setCredentials(Settings::userName(), Settings::password()); - } else { - m_connection->setCredentials(QString(), QString()); - } - m_connection->reconnect(); + m_connection->reconnect(primaryConnectionSettings()); } // NotificationsOptionPage @@ -312,7 +388,7 @@ bool setAutostartEnabled(bool enabled) #elif defined(PLATFORM_WINDOWS) QSettings settings(QStringLiteral("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"), QSettings::NativeFormat); if(enabled) { - settings.setValue(QStringLiteral(PROJECT_NAME), QCoreApplication::applicationFilePath().replace(QChar('/'), QChar("\\"))); + settings.setValue(QStringLiteral(PROJECT_NAME), QCoreApplication::applicationFilePath().replace(QChar('/'), QChar('\\'))); } else { settings.remove(QStringLiteral(PROJECT_NAME)); } @@ -515,7 +591,8 @@ SettingsDialog::SettingsDialog(Data::SyncthingConnection *connection, QWidget *p categoryModel()->setCategories(categories); - setMinimumSize(800, 450); + setMinimumSize(800, 550); + setWindowTitle(tr("Settings") + QStringLiteral(" - " APP_NAME)); setWindowIcon(QIcon::fromTheme(QStringLiteral("preferences-other"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/preferences-other.svg")))); // some settings could be applied without restarting the application, good idea? diff --git a/gui/settingsdialog.h b/gui/settingsdialog.h index 6a98cea..c571059 100644 --- a/gui/settingsdialog.h +++ b/gui/settingsdialog.h @@ -1,6 +1,8 @@ #ifndef SETTINGS_DIALOG_H #define SETTINGS_DIALOG_H +#include "../application/settings.h" + #include #include #include @@ -8,11 +10,6 @@ #include #include -namespace Settings { -class KnownFieldModel; -class TargetLevelModel; -} - namespace Data { class SyncthingConnection; } @@ -27,7 +24,15 @@ private: void insertFromConfigFile(); void updateConnectionStatus(); void applyAndReconnect(); + bool showConnectionSettings(int index); + bool cacheCurrentSettings(bool applying); + void saveCurrentConnectionName(const QString &name); + void addConnectionSettings(); + void removeConnectionSettings(); Data::SyncthingConnection *m_connection; + Settings::ConnectionSettings m_primarySettings; + std::vector m_secondarySettings; + int m_currentIndex; END_DECLARE_OPTION_PAGE DECLARE_UI_FILE_BASED_OPTION_PAGE(NotificationsOptionPage) diff --git a/gui/trayicon.cpp b/gui/trayicon.cpp index 5a988ef..2695e90 100644 --- a/gui/trayicon.cpp +++ b/gui/trayicon.cpp @@ -33,6 +33,7 @@ TrayIcon::TrayIcon(QObject *parent) : // set context menu connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("internet-web-browser"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/internet-web-browser.svg"))), tr("Web UI")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showWebUi); connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("preferences-other"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/preferences-other.svg"))), tr("Settings")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showSettingsDialog); + m_contextMenu.addMenu(m_trayMenu.widget()->connectionsMenu()); connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("help-about"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/help-about.svg"))), tr("About")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showAboutDialog); m_contextMenu.addSeparator(); connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("window-close"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/window-close.svg"))), tr("Close")), &QAction::triggered, &QCoreApplication::quit); diff --git a/gui/traywidget.cpp b/gui/traywidget.cpp index a55989f..862ae63 100644 --- a/gui/traywidget.cpp +++ b/gui/traywidget.cpp @@ -49,7 +49,8 @@ TrayWidget::TrayWidget(TrayMenu *parent) : m_webViewDlg(nullptr), #endif m_dirModel(m_connection), - m_devModel(m_connection) + m_devModel(m_connection), + m_selectedConnection(nullptr) { m_ui->setupUi(this); @@ -87,6 +88,11 @@ TrayWidget::TrayWidget(TrayMenu *parent) : cornerFrameLayout->addWidget(scanAllButton); m_ui->tabWidget->setCornerWidget(m_cornerFrame, Qt::BottomRightCorner); + // setup connection menu + m_connectionsActionGroup = new QActionGroup(m_connectionsMenu = new QMenu(tr("Connection"), this)); + m_connectionsMenu->setIcon(QIcon::fromTheme(QStringLiteral("network-connect"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/network-connect.svg")))); + connect(m_ui->connectionsPushButton, &QPushButton::clicked, this, &TrayWidget::showConnectionsMenu); + // apply settings, this also establishes the connection to Syncthing applySettings(); @@ -106,6 +112,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) : connect(m_ui->devsTreeView, &DevView::pauseResumeDev, this, &TrayWidget::pauseResumeDev); connect(scanAllButton, &QPushButton::clicked, &m_connection, &SyncthingConnection::rescanAllDirs); connect(viewIdButton, &QPushButton::clicked, this, &TrayWidget::showOwnDeviceId); + connect(m_connectionsActionGroup, &QActionGroup::triggered, this, &TrayWidget::handleConnectionSelected); } TrayWidget::~TrayWidget() @@ -115,13 +122,7 @@ void TrayWidget::showSettingsDialog() { if(!m_settingsDlg) { m_settingsDlg = new SettingsDialog(&m_connection, this); - m_settingsDlg->setWindowTitle(tr("Settings") + QStringLiteral(" - " APP_NAME)); connect(m_settingsDlg, &SettingsDialog::applied, this, &TrayWidget::applySettings); -#ifndef SYNCTHINGTRAY_NO_WEBVIEW - if(m_webViewDlg) { - connect(m_settingsDlg, &SettingsDialog::applied, m_webViewDlg, &WebViewDialog::applySettings); - } -#endif } m_settingsDlg->show(); centerWidget(m_settingsDlg); @@ -151,15 +152,15 @@ void TrayWidget::showWebUi() #ifndef SYNCTHINGTRAY_NO_WEBVIEW if(Settings::webViewDisabled()) { #endif - QDesktopServices::openUrl(Settings::syncthingUrl()); + QDesktopServices::openUrl(m_connection.syncthingUrl()); #ifndef SYNCTHINGTRAY_NO_WEBVIEW } else { if(!m_webViewDlg) { m_webViewDlg = new WebViewDialog(this); - connect(m_webViewDlg, &WebViewDialog::destroyed, this, &TrayWidget::handleWebViewDeleted); - if(m_settingsDlg) { - connect(m_settingsDlg, &SettingsDialog::applied, m_webViewDlg, &WebViewDialog::applySettings); + if(m_selectedConnection) { + m_webViewDlg->applySettings(*m_selectedConnection); } + connect(m_webViewDlg, &WebViewDialog::destroyed, this, &TrayWidget::handleWebViewDeleted); } m_webViewDlg->show(); if(m_menu) { @@ -263,16 +264,44 @@ void TrayWidget::updateStatusButton(SyncthingStatus status) void TrayWidget::applySettings() { - m_connection.setSyncthingUrl(Settings::syncthingUrl()); - m_connection.setApiKey(Settings::apiKey()); - if(Settings::authEnabled()) { - m_connection.setCredentials(Settings::userName(), Settings::password()); - } else { - m_connection.setCredentials(QString(), QString()); + // update connections menu + int connectionIndex = 0; + const int connectionCount = static_cast(1 + Settings::secondaryConnectionSettings().size()); + const QList connectionActions = m_connectionsActionGroup->actions(); + m_selectedConnection = nullptr; + for(; connectionIndex < connectionCount; ++connectionIndex) { + Settings::ConnectionSettings &connectionSettings = (connectionIndex == 0 ? Settings::primaryConnectionSettings() : Settings::secondaryConnectionSettings()[static_cast(connectionIndex - 1)]); + if(connectionIndex < connectionActions.size()) { + QAction *action = connectionActions.at(connectionIndex); + action->setText(connectionSettings.label); + if(action->isChecked()) { + m_selectedConnection = &connectionSettings; + } + } else { + QAction *action = m_connectionsMenu->addAction(connectionSettings.label); + action->setCheckable(true); + m_connectionsActionGroup->addAction(action); + } } - m_connection.loadSelfSignedCertificate(); - m_connection.reconnect(); - m_ui->trafficFrame->setVisible(Settings::showTraffic()); + for(; connectionIndex < connectionActions.size(); ++connectionIndex) { + m_connectionsActionGroup->removeAction(connectionActions.at(connectionIndex)); + } + if(!m_selectedConnection) { + m_selectedConnection = &Settings::primaryConnectionSettings(); + m_connectionsMenu->actions().at(0)->setChecked(true); + } + + m_connection.reconnect(*m_selectedConnection); + + // web view +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + if(m_webViewDlg) { + m_webViewDlg->applySettings(*m_selectedConnection); + } +#endif + + // update visual appearance + m_ui->trafficFormWidget->setVisible(Settings::showTraffic()); if(Settings::showTraffic()) { updateTraffic(); } @@ -354,14 +383,37 @@ void TrayWidget::updateTraffic() } +#ifndef SYNCTHINGTRAY_NO_WEBVIEW void TrayWidget::handleWebViewDeleted() { m_webViewDlg = nullptr; } +#endif void TrayWidget::handleNewNotification(const QString &msg) { // FIXME } +void TrayWidget::handleConnectionSelected(QAction *connectionAction) +{ + int index = m_connectionsMenu->actions().indexOf(connectionAction); + if(index >= 0) { + m_selectedConnection = (index == 0) + ? &Settings::primaryConnectionSettings() + : &Settings::secondaryConnectionSettings()[static_cast(index - 1)]; + m_connection.reconnect(*m_selectedConnection); +#ifndef SYNCTHINGTRAY_NO_WEBVIEW + if(m_webViewDlg) { + m_webViewDlg->applySettings(*m_selectedConnection); + } +#endif + } +} + +void TrayWidget::showConnectionsMenu() +{ + m_connectionsMenu->exec(QCursor::pos()); +} + } diff --git a/gui/traywidget.h b/gui/traywidget.h index 07f2399..545f4d3 100644 --- a/gui/traywidget.h +++ b/gui/traywidget.h @@ -7,12 +7,15 @@ #include "../data/syncthingdirectorymodel.h" #include "../data/syncthingdevicemodel.h" #include "../data/syncthingprocess.h" +#include "../application/settings.h" #include #include QT_FORWARD_DECLARE_CLASS(QFrame) +QT_FORWARD_DECLARE_CLASS(QMenu) +QT_FORWARD_DECLARE_CLASS(QActionGroup) namespace ApplicationUtilities { class QtConfigArguments; @@ -41,6 +44,7 @@ public: ~TrayWidget(); Data::SyncthingConnection &connection(); + QMenu *connectionsMenu(); public slots: void showSettingsDialog(); @@ -57,8 +61,12 @@ private slots: void pauseResumeDev(const QModelIndex &devIndex); void changeStatus(); void updateTraffic(); +#ifndef SYNCTHINGTRAY_NO_WEBVIEW void handleWebViewDeleted(); +#endif void handleNewNotification(const QString &msg); + void handleConnectionSelected(QAction *connectionAction); + void showConnectionsMenu(); private: TrayMenu *m_menu; @@ -72,6 +80,9 @@ private: Data::SyncthingConnection m_connection; Data::SyncthingDirectoryModel m_dirModel; Data::SyncthingDeviceModel m_devModel; + QMenu *m_connectionsMenu; + QActionGroup *m_connectionsActionGroup; + Settings::ConnectionSettings *m_selectedConnection; }; inline Data::SyncthingConnection &TrayWidget::connection() @@ -79,6 +90,11 @@ inline Data::SyncthingConnection &TrayWidget::connection() return m_connection; } +inline QMenu *TrayWidget::connectionsMenu() +{ + return m_connectionsMenu; +} + } #endif // TRAY_WIDGET_H diff --git a/gui/traywidget.ui b/gui/traywidget.ui index 66e8da6..e638949 100644 --- a/gui/traywidget.ui +++ b/gui/traywidget.ui @@ -178,7 +178,7 @@ 0 - 3 + 0 0 @@ -254,6 +254,22 @@ + + + + + 0 + 0 + + + + + + + true + + +
diff --git a/gui/webpage.cpp b/gui/webpage.cpp index d7ee66c..d52987f 100644 --- a/gui/webpage.cpp +++ b/gui/webpage.cpp @@ -1,5 +1,6 @@ #ifndef SYNCTHINGTRAY_NO_WEBVIEW #include "./webpage.h" +#include "./webviewdialog.h" #include "../application/settings.h" #include "../data/syncthingconnection.h" @@ -25,8 +26,9 @@ using namespace Data; namespace QtGui { -WebPage::WebPage(WEB_VIEW_PROVIDER *view) : +WebPage::WebPage(WebViewDialog *dlg, WEB_VIEW_PROVIDER *view) : WEB_PAGE_PROVIDER(view), + m_dlg(dlg), m_view(view) { #ifdef SYNCTHINGTRAY_USE_WEBENGINE @@ -52,6 +54,7 @@ WebPage::WebPage(WEB_VIEW_PROVIDER *view) : WEB_PAGE_PROVIDER *WebPage::createWindow(WEB_PAGE_PROVIDER::WebWindowType type) { + Q_UNUSED(type) return new WebPage; } @@ -92,17 +95,18 @@ void WebPage::supplyCredentials(QNetworkReply *reply, QAuthenticator *authentica void WebPage::supplyCredentials(QAuthenticator *authenticator) { - if(Settings::authEnabled()) { - authenticator->setUser(Settings::userName()); - authenticator->setPassword(Settings::password()); + if(m_dlg && m_dlg->settings().authEnabled) { + authenticator->setUser(m_dlg->settings().userName); + authenticator->setPassword(m_dlg->settings().password); } } #ifdef SYNCTHINGTRAY_USE_WEBKIT void WebPage::handleSslErrors(QNetworkReply *reply, const QList &errors) { - if(reply->request().url().host() == m_view->url().host()) { - reply->ignoreSslErrors(SyncthingConnection::expectedCertificateErrors()); + Q_UNUSED(errors) + if(m_dlg && reply->request().url().host() == m_view->url().host()) { + reply->ignoreSslErrors(m_dlg->settings().expectedSslErrors); } } #endif diff --git a/gui/webpage.h b/gui/webpage.h index 4da6d45..4e713fb 100644 --- a/gui/webpage.h +++ b/gui/webpage.h @@ -17,11 +17,13 @@ QT_FORWARD_DECLARE_CLASS(QSslError) namespace QtGui { +class WebViewDialog; + class WebPage : public WEB_PAGE_PROVIDER { Q_OBJECT public: - WebPage(WEB_VIEW_PROVIDER *view = nullptr); + WebPage(WebViewDialog *dlg = nullptr, WEB_VIEW_PROVIDER *view = nullptr); protected: WEB_PAGE_PROVIDER *createWindow(WebWindowType type); @@ -39,6 +41,7 @@ private slots: #endif private: + WebViewDialog *m_dlg; WEB_VIEW_PROVIDER *m_view; }; diff --git a/gui/webviewdialog.cpp b/gui/webviewdialog.cpp index 7ebdf95..c386f92 100644 --- a/gui/webviewdialog.cpp +++ b/gui/webviewdialog.cpp @@ -26,10 +26,8 @@ WebViewDialog::WebViewDialog(QWidget *parent) : setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); setCentralWidget(m_view); - m_view->setPage(new WebPage(m_view)); - - - applySettings(); + m_view->setPage(new WebPage(this, m_view)); + connect(m_view, &WEB_VIEW_PROVIDER::titleChanged, this, &WebViewDialog::setWindowTitle); if(Settings::webViewGeometry().isEmpty()) { resize(1200, 800); @@ -44,9 +42,10 @@ QtGui::WebViewDialog::~WebViewDialog() Settings::webViewGeometry() = saveGeometry(); } -void QtGui::WebViewDialog::applySettings() +void QtGui::WebViewDialog::applySettings(const Settings::ConnectionSettings &connectionSettings) { - m_view->setUrl(Settings::syncthingUrl()); + m_settings = connectionSettings; + m_view->setUrl(connectionSettings.syncthingUrl); m_view->setZoomFactor(Settings::webViewZoomFactor()); } diff --git a/gui/webviewdialog.h b/gui/webviewdialog.h index 0b1eff0..c672cf9 100644 --- a/gui/webviewdialog.h +++ b/gui/webviewdialog.h @@ -4,10 +4,16 @@ #include "./webviewprovider.h" +#include "../application/settings.h" + #include QT_FORWARD_DECLARE_CLASS(WEB_VIEW_PROVIDER) +namespace Settings { +struct ConnectionSettings; +} + namespace QtGui { class WebViewDialog : public QMainWindow @@ -18,15 +24,22 @@ public: ~WebViewDialog(); public slots: - void applySettings(); + void applySettings(const Settings::ConnectionSettings &connectionSettings); + const Settings::ConnectionSettings &settings() const; protected: void closeEvent(QCloseEvent *event); private: WEB_VIEW_PROVIDER *m_view; + Settings::ConnectionSettings m_settings; }; +inline const Settings::ConnectionSettings &WebViewDialog::settings() const +{ + return m_settings; +} + } #endif // SYNCTHINGTRAY_NO_WEBVIEW diff --git a/resources/icons.qrc b/resources/icons.qrc index cba338d..aefab24 100644 --- a/resources/icons.qrc +++ b/resources/icons.qrc @@ -26,5 +26,6 @@ icons/hicolor/scalable/places/folder.svg icons/hicolor/scalable/places/network-workgroup.svg icons/hicolor/scalable/apps/system-run.svg + icons/hicolor/scalable/actions/network-connect.svg diff --git a/resources/icons/hicolor/scalable/actions/network-connect.svg b/resources/icons/hicolor/scalable/actions/network-connect.svg new file mode 100644 index 0000000..fbc915e --- /dev/null +++ b/resources/icons/hicolor/scalable/actions/network-connect.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/translations/syncthingtray_de_DE.ts b/translations/syncthingtray_de_DE.ts index ee4dc56..90770f6 100644 --- a/translations/syncthingtray_de_DE.ts +++ b/translations/syncthingtray_de_DE.ts @@ -34,94 +34,100 @@ - + + + Connection configuration is insufficient. + + + + 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 restart: - + Unable to request QR-Code: @@ -444,22 +450,22 @@ - + This is achieved by adding a *.desktop file under <i>~/.config/autostart</i> so the setting only affects the current user. - + This is achieved by adding a registry key under <i>HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run</i> so the setting only affects the current user. Note that the startup entry is invalidated when moving <i>syncthingtray.exe</i>. - + This feature has not been implemented for your platform (yet). - + unable to modify startup entry @@ -467,65 +473,105 @@ QtGui::ConnectionOptionPage - + Connection - + + Config label + + + + + Add secondary instance + + + + + Remove currently selected secondary instance + + + + Syncthing URL - + Authentication - + User - + disconnected - + + Apply connection settings and try to reconnect with the currently selected config + + + + + It is possible to save multiple configurations. This allows switching quickly between multiple Syncthing instances using the arrow in the right corner of the tray menu. The config label is an arbitrary name to identify a configuration and must not match the name of the corresponding Syncthing device. + + + + + HTTPS certificate + + + + Status - - Apply connection settings and try to reconnect - - - - + API key - + Insert values from local Syncthing configuration - + Password - + + Auto-detected for local instance + + + + Select Syncthing config file - + Unable to parse the Syncthing config file. + + + Unable to load specified certificate "%1". + + + + + Instance %1 + + QtGui::DevView @@ -576,48 +622,48 @@ - + Syncthing executable - + Arguments - - Syncthing log (stdout/stderr) + + Syncthing log (interleaved stdout/stderr) - + Apply and launch now - + Stop launched instance - + No log messages available yet - + Ensure latest log is visible - + Syncthing existed with exit code %1 - + Syncthing crashed with exit code %1 @@ -659,17 +705,22 @@ QtGui::SettingsDialog - + Tray - + Startup - + + Settings + + + + Web view @@ -687,62 +738,62 @@ - + About - + Close - + Error - + Syncthing notification - + Not connected to Syncthing - + Disconnected from Syncthing - + Syncthing is idling - + Syncthing is scanning - + Notifications available - + At least one device is paused - + Synchronization is ongoing - + Synchronization complete @@ -756,7 +807,7 @@ - + Connect @@ -783,8 +834,8 @@ - - + + unknown @@ -799,24 +850,23 @@ - + Directories - + Devices - + About - Settings @@ -826,72 +876,77 @@ - + View own device ID - + Rescan all directories - + Show Syncthing log - + Restart Syncthing - + + Connection + + + + device ID is unknown - + Copy to clipboard - + Not connected to Syncthing, click to connect - + Pause - + Syncthing is running, click to pause all devices - + At least one device is paused, click to resume - + The directory <i>%1</i> does not exist on the local machine. - + Continue - + Own device ID - + Log @@ -908,7 +963,7 @@ QtGui::WebViewOptionPage - + General @@ -938,21 +993,29 @@ - + 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. + + Settings::restore + + + Unable to load certificate "%1" when restoring settings. + + + main - + You must configure how to connect to Syncthing when using Syncthing Tray the first time. - + Note that the settings dialog allows importing URL, credentials and API-key from the local Syncthing configuration. diff --git a/translations/syncthingtray_en_US.ts b/translations/syncthingtray_en_US.ts index b713609..5e8bd42 100644 --- a/translations/syncthingtray_en_US.ts +++ b/translations/syncthingtray_en_US.ts @@ -34,94 +34,100 @@ - + + + Connection configuration is insufficient. + + + + 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 restart: - + Unable to request QR-Code: @@ -444,22 +450,22 @@ - + This is achieved by adding a *.desktop file under <i>~/.config/autostart</i> so the setting only affects the current user. - + This is achieved by adding a registry key under <i>HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run</i> so the setting only affects the current user. Note that the startup entry is invalidated when moving <i>syncthingtray.exe</i>. - + This feature has not been implemented for your platform (yet). - + unable to modify startup entry @@ -467,65 +473,105 @@ QtGui::ConnectionOptionPage - + Connection - + + Config label + + + + + Add secondary instance + + + + + Remove currently selected secondary instance + + + + Syncthing URL - + Authentication - + User - + disconnected - + + Apply connection settings and try to reconnect with the currently selected config + + + + + It is possible to save multiple configurations. This allows switching quickly between multiple Syncthing instances using the arrow in the right corner of the tray menu. The config label is an arbitrary name to identify a configuration and must not match the name of the corresponding Syncthing device. + + + + + HTTPS certificate + + + + Status - - Apply connection settings and try to reconnect - - - - + API key - + Insert values from local Syncthing configuration - + Password - + + Auto-detected for local instance + + + + Select Syncthing config file - + Unable to parse the Syncthing config file. + + + Unable to load specified certificate "%1". + + + + + Instance %1 + + QtGui::DevView @@ -576,48 +622,48 @@ - + Syncthing executable - + Arguments - - Syncthing log (stdout/stderr) + + Syncthing log (interleaved stdout/stderr) - + Apply and launch now - + Stop launched instance - + No log messages available yet - + Ensure latest log is visible - + Syncthing existed with exit code %1 - + Syncthing crashed with exit code %1 @@ -659,17 +705,22 @@ QtGui::SettingsDialog - + Tray - + Startup - + + Settings + + + + Web view @@ -687,62 +738,62 @@ - + About - + Close - + Error - + Syncthing notification - + Not connected to Syncthing - + Disconnected from Syncthing - + Syncthing is idling - + Syncthing is scanning - + Notifications available - + At least one device is paused - + Synchronization is ongoing - + Synchronization complete @@ -756,7 +807,7 @@ - + Connect @@ -783,8 +834,8 @@ - - + + unknown @@ -799,24 +850,23 @@ - + Directories - + Devices - + About - Settings @@ -826,72 +876,77 @@ - + View own device ID - + Rescan all directories - + Show Syncthing log - + Restart Syncthing - + + Connection + + + + device ID is unknown - + Copy to clipboard - + Not connected to Syncthing, click to connect - + Pause - + Syncthing is running, click to pause all devices - + At least one device is paused, click to resume - + The directory <i>%1</i> does not exist on the local machine. - + Continue - + Own device ID - + Log @@ -908,7 +963,7 @@ QtGui::WebViewOptionPage - + General @@ -938,21 +993,29 @@ - + 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. + + Settings::restore + + + Unable to load certificate "%1" when restoring settings. + + + main - + You must configure how to connect to Syncthing when using Syncthing Tray the first time. - + Note that the settings dialog allows importing URL, credentials and API-key from the local Syncthing configuration.