From 23a4833b6ec70fb334955261f7af9c81c7da7d70 Mon Sep 17 00:00:00 2001 From: Martchus Date: Wed, 21 Sep 2016 21:09:12 +0200 Subject: [PATCH] Show ongoing downloads --- CMakeLists.txt | 9 +- README.md | 2 +- application/settings.cpp | 2 +- data/syncthingconnection.cpp | 53 ++++++- data/syncthingconnection.h | 34 ++++- data/syncthingdevicemodel.cpp | 4 +- data/syncthingdevicemodel.h | 2 +- data/syncthingdirectorymodel.cpp | 12 +- data/syncthingdirectorymodel.h | 2 +- data/syncthingdownloadmodel.cpp | 213 +++++++++++++++++++++++++++ data/syncthingdownloadmodel.h | 64 +++++++++ gui/devview.cpp | 24 ++-- gui/devview.h | 6 +- gui/dirview.cpp | 26 ++-- gui/dirview.h | 8 +- gui/downloaditemdelegate.cpp | 63 ++++++++ gui/downloaditemdelegate.h | 23 +++ gui/downloadview.cpp | 79 ++++++++++ gui/downloadview.h | 34 +++++ gui/traywidget.cpp | 41 +++--- gui/traywidget.h | 9 +- gui/traywidget.ui | 37 ++++- testdata/downloadprogressevent.json | 51 +++++++ translations/syncthingtray_de_DE.ts | 215 +++++++++++++++++----------- translations/syncthingtray_en_US.ts | 215 +++++++++++++++++----------- 25 files changed, 993 insertions(+), 235 deletions(-) create mode 100644 data/syncthingdownloadmodel.cpp create mode 100644 data/syncthingdownloadmodel.h create mode 100644 gui/downloaditemdelegate.cpp create mode 100644 gui/downloaditemdelegate.h create mode 100644 gui/downloadview.cpp create mode 100644 gui/downloadview.h create mode 100644 testdata/downloadprogressevent.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ee8b48..38f4ec9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ set(META_APP_CATEGORIES "Utility;Network;") set(META_GUI_OPTIONAL false) set(META_VERSION_MAJOR 0) set(META_VERSION_MINOR 0) -set(META_VERSION_PATCH 1) +set(META_VERSION_PATCH 2) set(META_APP_VERSION ${META_VERSION_MAJOR}.${META_VERSION_MINOR}.${META_VERSION_PATCH}) # add project files @@ -19,6 +19,7 @@ set(HEADER_FILES data/syncthingconnection.h data/syncthingdirectorymodel.h data/syncthingdevicemodel.h + data/syncthingdownloadmodel.h data/syncthingconfig.h data/syncthingprocess.h data/utils.h @@ -27,6 +28,7 @@ set(SRC_FILES data/syncthingconnection.cpp data/syncthingdirectorymodel.cpp data/syncthingdevicemodel.cpp + data/syncthingdownloadmodel.cpp data/syncthingconfig.cpp data/syncthingprocess.cpp data/utils.cpp @@ -43,8 +45,10 @@ set(WIDGETS_HEADER_FILES gui/webviewprovider.h gui/dirbuttonsitemdelegate.h gui/devbuttonsitemdelegate.h + gui/downloaditemdelegate.h gui/dirview.h gui/devview.h + gui/downloadview.h gui/textviewdialog.h ) set(WIDGETS_SRC_FILES @@ -58,8 +62,10 @@ set(WIDGETS_SRC_FILES gui/webviewdialog.cpp gui/dirbuttonsitemdelegate.cpp gui/devbuttonsitemdelegate.cpp + gui/downloaditemdelegate.cpp gui/dirview.cpp gui/devview.cpp + gui/downloadview.cpp gui/textviewdialog.cpp resources/icons.qrc ) @@ -100,6 +106,7 @@ set(REQUIRED_ICONS edit-copy edit-paste folder + folder-download folder-open folder-sync help-about diff --git a/README.md b/README.md index 73223ab..78f5137 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ support * Check current traffic statistics * Display further details about direcoties and devices, like last file, last scan, ... + * Display ongoing downloads * Trigger re-scan of a specific directory or all directories at once * Open a directory with the default file browser * Pause/resume a specific device or all devices at once @@ -33,7 +34,6 @@ support ## Planned features The tray is still under development; the following features are planned: -* Show currently processed items * Show recently processed items * Improve notification handling * Create Plasmoid for Plasma 5 desktop diff --git a/application/settings.cpp b/application/settings.cpp index 9f51f7a..2f49d8a 100644 --- a/application/settings.cpp +++ b/application/settings.cpp @@ -65,7 +65,7 @@ bool &showTraffic() } QSize &trayMenuSize() { - static QSize v(350, 300); + static QSize v(450, 400); return v; } int &frameStyle() diff --git a/data/syncthingconnection.cpp b/data/syncthingconnection.cpp index e17104f..0b00573 100644 --- a/data/syncthingconnection.cpp +++ b/data/syncthingconnection.cpp @@ -4,6 +4,7 @@ #include "../application/settings.h" #include +#include #include #include @@ -99,6 +100,27 @@ bool SyncthingDir::assignStatus(DirStatus newStatus, DateTime time) return false; } +SyncthingItemDownloadProgress::SyncthingItemDownloadProgress(const QString &containingDirPath, const QString &relativeItemPath, const QJsonObject &values) : + relativePath(relativeItemPath), + fileInfo(containingDirPath % QChar('/') % relativeItemPath), + blocksCurrentlyDownloading(values.value(QStringLiteral("Pulling")).toInt()), + blocksAlreadyDownloaded(values.value(QStringLiteral("Pulled")).toInt()), + totalNumberOfBlocks(values.value(QStringLiteral("Total")).toInt()), + downloadPercentage((blocksAlreadyDownloaded > 0 && totalNumberOfBlocks > 0) + ? (static_cast(blocksAlreadyDownloaded) * 100 / static_cast(totalNumberOfBlocks)) + : 0), + blocksCopiedFromOrigin(values.value(QStringLiteral("CopiedFromOrigin")).toInt()), + blocksCopiedFromElsewhere(values.value(QStringLiteral("CopiedFromElsewhere")).toInt()), + blocksReused(values.value(QStringLiteral("Reused")).toInt()), + bytesAlreadyHandled(values.value(QStringLiteral("BytesDone")).toInt()), + totalNumberOfBytes(values.value(QStringLiteral("BytesTotal")).toInt()), + label(QStringLiteral("%1 / %2 - %3 %").arg( + QString::fromLatin1(dataSizeToString(blocksAlreadyDownloaded > 0 ? static_cast(blocksAlreadyDownloaded) * syncthingBlockSize : 0).data()), + QString::fromLatin1(dataSizeToString(totalNumberOfBlocks > 0 ? static_cast(totalNumberOfBlocks) * syncthingBlockSize : 0).data()), + QString::number(downloadPercentage)) + ) +{} + /*! * \class SyncthingConnection * \brief The SyncthingConnection class allows Qt applications to access Syncthing. @@ -972,7 +994,7 @@ void SyncthingConnection::readEvents() } else if(eventType == QLatin1String("StateChanged")) { readStatusChangedEvent(eventTime, eventData); } else if(eventType == QLatin1String("DownloadProgress")) { - readDownloadProgressEvent(eventData); + readDownloadProgressEvent(eventTime, eventData); } else if(eventType.startsWith(QLatin1String("Folder"))) { readDirEvent(eventTime, eventType, eventData); } else if(eventType.startsWith(QLatin1String("Device"))) { @@ -1053,9 +1075,34 @@ void SyncthingConnection::readStatusChangedEvent(DateTime eventTime, const QJson * \brief Reads results of requestEvents(). * \remarks TODO */ -void SyncthingConnection::readDownloadProgressEvent(const QJsonObject &eventData) +void SyncthingConnection::readDownloadProgressEvent(DateTime eventTime, const QJsonObject &eventData) { - VAR_UNUSED(eventData) + VAR_UNUSED(eventTime) + for(SyncthingDir &dirInfo : m_dirs) { + // disappearing implies that the download has been finished so just wipe old entries + dirInfo.downloadingItems.clear(); + dirInfo.blocksAlreadyDownloaded = dirInfo.blocksToBeDownloaded = 0; + + // read progress of currently downloading items + const QJsonObject dirObj(eventData.value(dirInfo.id).toObject()); + if(!dirObj.isEmpty()) { + dirInfo.downloadingItems.reserve(static_cast(dirObj.size())); + for(auto filePair = dirObj.constBegin(), end = dirObj.constEnd(); filePair != end; ++filePair) { + dirInfo.downloadingItems.emplace_back(dirInfo.path, filePair.key(), filePair.value().toObject()); + const SyncthingItemDownloadProgress &itemProgress = dirInfo.downloadingItems.back(); + dirInfo.blocksAlreadyDownloaded += itemProgress.blocksAlreadyDownloaded; + dirInfo.blocksToBeDownloaded += itemProgress.totalNumberOfBlocks; + } + } + dirInfo.downloadPercentage = (dirInfo.blocksAlreadyDownloaded > 0 && dirInfo.blocksToBeDownloaded > 0) + ? (static_cast(dirInfo.blocksAlreadyDownloaded) * 100 / static_cast(dirInfo.blocksToBeDownloaded)) + : 0; + dirInfo.downloadLabel = QStringLiteral("%1 / %2 - %3 %").arg( + QString::fromLatin1(dataSizeToString(dirInfo.blocksAlreadyDownloaded > 0 ? static_cast(dirInfo.blocksAlreadyDownloaded) * SyncthingItemDownloadProgress::syncthingBlockSize : 0).data()), + QString::fromLatin1(dataSizeToString(dirInfo.blocksToBeDownloaded > 0 ? static_cast(dirInfo.blocksToBeDownloaded) * SyncthingItemDownloadProgress::syncthingBlockSize : 0).data()), + QString::number(dirInfo.downloadPercentage)); + } + emit downloadProgressChanged(); } /*! diff --git a/data/syncthingconnection.h b/data/syncthingconnection.h index fea25c7..ff09e18 100644 --- a/data/syncthingconnection.h +++ b/data/syncthingconnection.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -56,8 +57,27 @@ struct DirErrors QString path; }; +struct SyncthingItemDownloadProgress +{ + SyncthingItemDownloadProgress(const QString &containingDirPath, const QString &relativeItemPath, const QJsonObject &values); + QString relativePath; + QFileInfo fileInfo; + int blocksCurrentlyDownloading = 0; + int blocksAlreadyDownloaded = 0; + int totalNumberOfBlocks = 0; + unsigned int downloadPercentage = 0; + int blocksCopiedFromOrigin = 0; + int blocksCopiedFromElsewhere = 0; + int blocksReused = 0; + int bytesAlreadyHandled; + int totalNumberOfBytes = 0; + QString label; + ChronoUtilities::DateTime lastUpdate; + static constexpr unsigned int syncthingBlockSize = 128 * 1024; +}; + struct SyncthingDir -{ +{ QString id; QString label; QString path; @@ -79,6 +99,11 @@ struct SyncthingDir ChronoUtilities::DateTime lastFileTime; QString lastFileName; bool lastFileDeleted = false; + std::vector downloadingItems; + int blocksAlreadyDownloaded = 0; + int blocksToBeDownloaded = 0; + unsigned int downloadPercentage = 0; + QString downloadLabel; bool assignStatus(const QString &statusStr, ChronoUtilities::DateTime time); bool assignStatus(DirStatus newStatus, ChronoUtilities::DateTime time); @@ -215,6 +240,11 @@ Q_SIGNALS: */ void devStatusChanged(const SyncthingDev &dev, int index); + /*! + * \brief Indicates the download progress changed. + */ + void downloadProgressChanged(); + /*! * \brief Indicates a new Syncthing notification is available. */ @@ -266,7 +296,7 @@ private Q_SLOTS: void readEvents(); void readStartingEvent(const QJsonObject &eventData); void readStatusChangedEvent(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData); - void readDownloadProgressEvent(const QJsonObject &eventData); + void readDownloadProgressEvent(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData); void readDirEvent(ChronoUtilities::DateTime eventTime, 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); diff --git a/data/syncthingdevicemodel.cpp b/data/syncthingdevicemodel.cpp index 75b918f..8ccffd8 100644 --- a/data/syncthingdevicemodel.cpp +++ b/data/syncthingdevicemodel.cpp @@ -108,7 +108,7 @@ QVariant SyncthingDeviceModel::data(const QModelIndex &index, int role) const case Qt::ForegroundRole: switch(index.column()) { case 1: - const SyncthingDev &dev = m_devs[index.parent().row()]; + const SyncthingDev &dev = m_devs[static_cast(index.parent().row())]; switch(index.row()) { case 2: if(dev.lastSeen.isNull()) { @@ -128,7 +128,7 @@ QVariant SyncthingDeviceModel::data(const QModelIndex &index, int role) const case 1: switch(index.row()) { case 2: - const SyncthingDev &dev = m_devs[index.parent().row()]; + const SyncthingDev &dev = m_devs[static_cast(index.parent().row())]; if(!dev.lastSeen.isNull()) { return agoString(dev.lastSeen); } diff --git a/data/syncthingdevicemodel.h b/data/syncthingdevicemodel.h index d9ece90..55a89f7 100644 --- a/data/syncthingdevicemodel.h +++ b/data/syncthingdevicemodel.h @@ -34,7 +34,7 @@ public Q_SLOTS: int columnCount(const QModelIndex &parent) const; const SyncthingDev *devInfo(const QModelIndex &index) const; -private slots: +private Q_SLOTS: void newConfig(); void newDevices(); void devStatusChanged(const SyncthingDev &, int index); diff --git a/data/syncthingdirectorymodel.cpp b/data/syncthingdirectorymodel.cpp index c58479d..7e93dfa 100644 --- a/data/syncthingdirectorymodel.cpp +++ b/data/syncthingdirectorymodel.cpp @@ -35,12 +35,12 @@ QModelIndex SyncthingDirectoryModel::index(int row, int column, const QModelInde if(!parent.isValid()) { // top-level: all dir labels/IDs if(row < rowCount(parent)) { - return createIndex(row, column, -1); + return createIndex(row, column, static_cast(-1)); } } else if(!parent.parent().isValid()) { // dir-level: dir attributes if(row < rowCount(parent)) { - return createIndex(row, column, parent.row()); + return createIndex(row, column, static_cast(parent.row())); } } return QModelIndex(); @@ -48,7 +48,7 @@ QModelIndex SyncthingDirectoryModel::index(int row, int column, const QModelInde QModelIndex SyncthingDirectoryModel::parent(const QModelIndex &child) const { - return child.internalId() != static_cast(-1) ? index(child.internalId(), 0, QModelIndex()) : QModelIndex(); + return child.internalId() != static_cast(-1) ? index(static_cast(child.internalId()), 0, QModelIndex()) : QModelIndex(); } QVariant SyncthingDirectoryModel::headerData(int section, Qt::Orientation orientation, int role) const @@ -94,7 +94,7 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const } break; case 1: // attribute values - const SyncthingDir &dir = m_dirs[index.parent().row()]; + const SyncthingDir &dir = m_dirs[static_cast(index.parent().row())]; switch(index.row()) { case 0: return dir.id; case 1: return dir.path; @@ -110,7 +110,7 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const case Qt::ForegroundRole: switch(index.column()) { case 1: - const SyncthingDir &dir = m_dirs[index.parent().row()]; + const SyncthingDir &dir = m_dirs[static_cast(index.parent().row())]; switch(index.row()) { case 5: if(dir.lastScanTime.isNull()) { @@ -130,7 +130,7 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const case Qt::ToolTipRole: switch(index.column()) { case 1: - const SyncthingDir &dir = m_dirs[index.parent().row()]; + const SyncthingDir &dir = m_dirs[static_cast(index.parent().row())]; switch(index.row()) { case 5: if(!dir.lastScanTime.isNull()) { diff --git a/data/syncthingdirectorymodel.h b/data/syncthingdirectorymodel.h index 415590a..7c167eb 100644 --- a/data/syncthingdirectorymodel.h +++ b/data/syncthingdirectorymodel.h @@ -27,7 +27,7 @@ public Q_SLOTS: int columnCount(const QModelIndex &parent) const; const SyncthingDir *dirInfo(const QModelIndex &index) const; -private slots: +private Q_SLOTS: void newConfig(); void newDirs(); void dirStatusChanged(const SyncthingDir &, int index); diff --git a/data/syncthingdownloadmodel.cpp b/data/syncthingdownloadmodel.cpp new file mode 100644 index 0000000..20d6fd5 --- /dev/null +++ b/data/syncthingdownloadmodel.cpp @@ -0,0 +1,213 @@ +#include "./syncthingdownloadmodel.h" +#include "./syncthingconnection.h" +#include "./utils.h" + +#include + +using namespace ChronoUtilities; + +namespace Data { + +SyncthingDownloadModel::SyncthingDownloadModel(SyncthingConnection &connection, QObject *parent) : + QAbstractItemModel(parent), + m_connection(connection), + m_dirs(connection.dirInfo()), + m_unknownIcon(QIcon::fromTheme(QStringLiteral("text-x-generic"), QIcon(QStringLiteral(":/icons/hicolor/scalable/mimetypes/text-x-generic.svg")))), + m_pendingDirs(0) +{ + connect(&m_connection, &SyncthingConnection::newConfig, this, &SyncthingDownloadModel::newConfig); + connect(&m_connection, &SyncthingConnection::newDirs, this, &SyncthingDownloadModel::newDirs); + connect(&m_connection, &SyncthingConnection::downloadProgressChanged, this, &SyncthingDownloadModel::downloadProgressChanged); +} + +/*! + * \brief Returns the directory info for the spcified \a index. The returned object is not persistent. + */ +const SyncthingDir *SyncthingDownloadModel::dirInfo(const QModelIndex &index) const +{ + return (index.parent().isValid() ? dirInfo(index.parent()) : (static_cast(index.row()) < m_pendingDirs.size() ? m_pendingDirs[static_cast(index.row())] : nullptr)); +} + +const SyncthingItemDownloadProgress *SyncthingDownloadModel::progressInfo(const QModelIndex &index) const +{ + if(index.parent().isValid() + && static_cast(index.parent().row()) < m_pendingDirs.size() + && static_cast(index.row()) < m_pendingDirs[static_cast(index.parent().row())]->downloadingItems.size()) { + return &(m_pendingDirs[static_cast(index.parent().row())]->downloadingItems[static_cast(index.row())]); + } else { + return nullptr; + } +} + +QModelIndex SyncthingDownloadModel::index(int row, int column, const QModelIndex &parent) const +{ + if(!parent.isValid()) { + // top-level: all pending dir labels/IDs + if(row < rowCount(parent)) { + return createIndex(row, column, static_cast(-1)); + } + } else if(!parent.parent().isValid()) { + // dir-level: pending downloads + if(row < rowCount(parent)) { + return createIndex(row, column, static_cast(parent.row())); + } + } + return QModelIndex(); +} + +QModelIndex SyncthingDownloadModel::parent(const QModelIndex &child) const +{ + return child.internalId() != static_cast(-1) ? index(static_cast(child.internalId()), 0, QModelIndex()) : QModelIndex(); +} + +QVariant SyncthingDownloadModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch(orientation) { + case Qt::Horizontal: + switch(role) { + case Qt::DisplayRole: + switch(section) { + case 0: return tr("Dir/item"); + case 1: return tr("Progress"); + } + break; + default: + ; + } + break; + default: + ; + } + return QVariant(); +} + +QVariant SyncthingDownloadModel::data(const QModelIndex &index, int role) const +{ + if(index.isValid()) { + if(index.parent().isValid()) { + // dir attributes + if(static_cast(index.parent().row()) < m_pendingDirs.size()) { + const SyncthingDir &dir = *m_pendingDirs[static_cast(index.parent().row())]; + if(static_cast(index.row()) < dir.downloadingItems.size()) { + const SyncthingItemDownloadProgress &progress = dir.downloadingItems[static_cast(index.row())]; + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: // file names + return progress.relativePath; + case 1: // progress information + return progress.label; + } + break; + case Qt::ToolTipRole: + break; + case Qt::DecorationRole: + switch(index.column()) { + case 0: // file icon + return progress.fileInfo.exists() ? m_fileIconProvider.icon(progress.fileInfo) : m_unknownIcon; + default: + ; + } + break; + case ItemPercentage: + return progress.downloadPercentage; + default: + ; + } + } + } + } else if(static_cast(index.row()) < m_pendingDirs.size()) { + // dir IDs and overall dir progress + const SyncthingDir &dir = *m_pendingDirs[static_cast(index.row())]; + switch(role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) { + case 0: return QVariant((dir.label.isEmpty() ? dir.id : dir.label) % QChar(' ') % QChar('(') % QString::number(dir.downloadingItems.size()) % QChar(')')); + case 1: return dir.downloadLabel; + } + break; + case Qt::TextAlignmentRole: + switch(index.column()) { + case 0: break; + case 1: return static_cast(Qt::AlignRight | Qt::AlignVCenter); + } + break; + case ItemPercentage: + return dir.downloadPercentage; + default: + ; + } + } + } + return QVariant(); +} + +bool SyncthingDownloadModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_UNUSED(index) Q_UNUSED(value) Q_UNUSED(role) + return false; +} + +int SyncthingDownloadModel::rowCount(const QModelIndex &parent) const +{ + if(!parent.isValid()) { + return static_cast(m_pendingDirs.size()); + } else if(!parent.parent().isValid() && parent.row() >= 0 && static_cast(parent.row()) < m_pendingDirs.size()) { + return static_cast(m_pendingDirs[static_cast(parent.row())]->downloadingItems.size()); + } else { + return 0; + } +} + +int SyncthingDownloadModel::columnCount(const QModelIndex &parent) const +{ + if(!parent.isValid()) { + return 2; // label/ID, status/progress + } else if(!parent.parent().isValid()) { + return 2; // file, progress + } else { + return 0; + } +} + +void SyncthingDownloadModel::newConfig() +{ + beginResetModel(); + m_pendingDirs.clear(); + endResetModel(); +} + +void SyncthingDownloadModel::newDirs() +{ + m_pendingDirs.reserve(m_connection.dirInfo().size()); +} + +void SyncthingDownloadModel::downloadProgressChanged() +{ + int row = 0; + for(const SyncthingDir &dirInfo : m_connection.dirInfo()) { + auto pendingIterator = find(m_pendingDirs.begin(), m_pendingDirs.end(), &dirInfo); + if(dirInfo.downloadingItems.empty()) { + if(pendingIterator != m_pendingDirs.end()) { + beginRemoveRows(QModelIndex(), row, row); + m_pendingDirs.erase(pendingIterator); + endRemoveRows(); + } + } else { + if(pendingIterator != m_pendingDirs.end()) { + emit dataChanged(index(row, 0), index(row, 1), QVector() << Qt::DisplayRole << Qt::EditRole << Qt::DecorationRole << Qt::ForegroundRole << Qt::ToolTipRole); + } else { + beginInsertRows(QModelIndex(), row, row); + beginInsertRows(index(row, row), 0, static_cast(dirInfo.downloadingItems.size())); + m_pendingDirs.insert(pendingIterator, &dirInfo); + endInsertRows(); + endInsertRows(); + } + ++row; + } + } +} + +} // namespace Data diff --git a/data/syncthingdownloadmodel.h b/data/syncthingdownloadmodel.h new file mode 100644 index 0000000..2030de3 --- /dev/null +++ b/data/syncthingdownloadmodel.h @@ -0,0 +1,64 @@ +#ifndef DATA_SYNCTHINGDOWNLOADMODEL_H +#define DATA_SYNCTHINGDOWNLOADMODEL_H + +#include +#include +#include + +#include + +namespace Data { + +class SyncthingConnection; +struct SyncthingDir; +struct SyncthingItemDownloadProgress; + +class SyncthingDownloadModel : public QAbstractItemModel +{ + Q_OBJECT + Q_PROPERTY(unsigned int pendingDownloads READ pendingDownloads NOTIFY pendingDownloadsChanged) +public: + explicit SyncthingDownloadModel(SyncthingConnection &connection, QObject *parent = nullptr); + + enum SyncthingDownloadModelRole + { + ItemPercentage = Qt::UserRole + 1 + }; + +public Q_SLOTS: + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &child) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + const SyncthingDir *dirInfo(const QModelIndex &index) const; + const SyncthingItemDownloadProgress *progressInfo(const QModelIndex &index) const; + unsigned int pendingDownloads() const; + +Q_SIGNALS: + void pendingDownloadsChanged(unsigned int pendingDownloads); + +private Q_SLOTS: + void newConfig(); + void newDirs(); + void downloadProgressChanged(); + +private: + Data::SyncthingConnection &m_connection; + const std::vector &m_dirs; + const QIcon m_unknownIcon; + const QFileIconProvider m_fileIconProvider; + std::vector m_pendingDirs; + unsigned int m_pendingDownloads; +}; + +inline unsigned int SyncthingDownloadModel::pendingDownloads() const +{ + return m_pendingDownloads; +} + +} // namespace Data + +#endif // DATA_SYNCTHINGDOWNLOADMODEL_H diff --git a/gui/devview.cpp b/gui/devview.cpp index 7cc13a5..a36bbf8 100644 --- a/gui/devview.cpp +++ b/gui/devview.cpp @@ -1,6 +1,8 @@ #include "./devview.h" #include "./devbuttonsitemdelegate.h" +#include "../data/syncthingdevicemodel.h" + #include #include #include @@ -8,6 +10,8 @@ #include #include +using namespace Data; + namespace QtGui { DevView::DevView(QWidget *parent) : @@ -23,16 +27,16 @@ DevView::DevView(QWidget *parent) : void DevView::mouseReleaseEvent(QMouseEvent *event) { QTreeView::mouseReleaseEvent(event); - const QPoint pos(event->pos()); - const QModelIndex clickedIndex(indexAt(event->pos())); - if(clickedIndex.isValid() && clickedIndex.column() == 1 && !clickedIndex.parent().isValid()) { - const QRect itemRect(visualRect(clickedIndex)); - //if(pos.x() > itemRect.right() - 34) { - if(pos.x() > itemRect.right() - 17) { - emit pauseResumeDev(clickedIndex); - // } else { - // emit scanDir(clickedIndex); - // } + if(const auto *devModel = qobject_cast(model())) { + const QPoint pos(event->pos()); + const QModelIndex clickedIndex(indexAt(event->pos())); + if(clickedIndex.isValid() && clickedIndex.column() == 1 && !clickedIndex.parent().isValid()) { + if(const SyncthingDev *devInfo = devModel->devInfo(clickedIndex)) { + const QRect itemRect(visualRect(clickedIndex)); + if(pos.x() > itemRect.right() - 17) { + emit pauseResumeDev(*devInfo); + } + } } } } diff --git a/gui/devview.h b/gui/devview.h index 21b34f2..6c808da 100644 --- a/gui/devview.h +++ b/gui/devview.h @@ -3,6 +3,10 @@ #include +namespace Data { +struct SyncthingDev; +} + namespace QtGui { class DevView : public QTreeView @@ -12,7 +16,7 @@ public: DevView(QWidget *parent = nullptr); Q_SIGNALS: - void pauseResumeDev(const QModelIndex &index); + void pauseResumeDev(const Data::SyncthingDev &dev); protected: void mouseReleaseEvent(QMouseEvent *event); diff --git a/gui/dirview.cpp b/gui/dirview.cpp index f536bc5..e37d851 100644 --- a/gui/dirview.cpp +++ b/gui/dirview.cpp @@ -1,6 +1,8 @@ #include "./dirview.h" #include "./dirbuttonsitemdelegate.h" +#include "../data/syncthingdirectorymodel.h" + #include #include #include @@ -8,6 +10,8 @@ #include #include +using namespace Data; + namespace QtGui { DirView::DirView(QWidget *parent) : @@ -23,15 +27,19 @@ DirView::DirView(QWidget *parent) : void DirView::mouseReleaseEvent(QMouseEvent *event) { QTreeView::mouseReleaseEvent(event); - const QPoint pos(event->pos()); - const QModelIndex clickedIndex(indexAt(event->pos())); - if(clickedIndex.isValid() && clickedIndex.column() == 1 && !clickedIndex.parent().isValid()) { - const QRect itemRect(visualRect(clickedIndex)); - if(pos.x() > itemRect.right() - 34) { - if(pos.x() > itemRect.right() - 17) { - emit openDir(clickedIndex); - } else { - emit scanDir(clickedIndex); + if(const SyncthingDirectoryModel *dirModel = qobject_cast(model())) { + const QPoint pos(event->pos()); + const QModelIndex clickedIndex(indexAt(event->pos())); + if(clickedIndex.isValid() && clickedIndex.column() == 1 && !clickedIndex.parent().isValid()) { + if(const SyncthingDir *dir = dirModel->dirInfo(clickedIndex)) { + const QRect itemRect(visualRect(clickedIndex)); + if(pos.x() > itemRect.right() - 34) { + if(pos.x() > itemRect.right() - 17) { + emit openDir(*dir); + } else { + emit scanDir(*dir); + } + } } } } diff --git a/gui/dirview.h b/gui/dirview.h index 3093791..dd46aeb 100644 --- a/gui/dirview.h +++ b/gui/dirview.h @@ -3,6 +3,10 @@ #include +namespace Data { +struct SyncthingDir; +} + namespace QtGui { class DirView : public QTreeView @@ -12,8 +16,8 @@ public: DirView(QWidget *parent = nullptr); Q_SIGNALS: - void openDir(const QModelIndex &index); - void scanDir(const QModelIndex &index); + void openDir(const Data::SyncthingDir &dir); + void scanDir(const Data::SyncthingDir &dir); protected: void mouseReleaseEvent(QMouseEvent *event); diff --git a/gui/downloaditemdelegate.cpp b/gui/downloaditemdelegate.cpp new file mode 100644 index 0000000..1ff5e99 --- /dev/null +++ b/gui/downloaditemdelegate.cpp @@ -0,0 +1,63 @@ +#include "./downloaditemdelegate.h" + +#include "../data/syncthingdownloadmodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Data; + +namespace QtGui { + +inline int centerObj(int avail, int size) +{ + return (avail - size) / 2; +} + +DownloadItemDelegate::DownloadItemDelegate(QObject* parent) : + QStyledItemDelegate(parent), + m_folderIcon(QIcon::fromTheme(QStringLiteral("folder-open"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg"))).pixmap(QSize(16, 16))) +{} + +void DownloadItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // use the customization only on top-level rows + //if(!index.parent().isValid()) { + // QStyledItemDelegate::paint(painter, option, index); + //} else { + // init style options to use drawControl(), except for the text + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.text.clear(); + opt.features = QStyleOptionViewItem::None; + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter); + + // draw progress bar + const QAbstractItemModel *model = index.model(); + QStyleOptionProgressBar progressBarOption; + progressBarOption.state = QStyle::State_Enabled; + progressBarOption.direction = QApplication::layoutDirection(); + progressBarOption.rect = option.rect; + progressBarOption.rect.setWidth(option.rect.width() - 20); + progressBarOption.textAlignment = Qt::AlignCenter; + progressBarOption.textVisible = true; + progressBarOption.progress = model->data(index, SyncthingDownloadModel::ItemPercentage).toInt(); + progressBarOption.minimum = 0; + progressBarOption.maximum = 100; + progressBarOption.text = model->data(index, Qt::DisplayRole).toString(); + QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter); + + // draw buttons + const int buttonY = option.rect.y() + centerObj(option.rect.height(), 16); + painter->drawPixmap(option.rect.right() - 16, buttonY, 16, 16, m_folderIcon); + //} + +} + +} diff --git a/gui/downloaditemdelegate.h b/gui/downloaditemdelegate.h new file mode 100644 index 0000000..4471d97 --- /dev/null +++ b/gui/downloaditemdelegate.h @@ -0,0 +1,23 @@ +#ifndef DOWNLOADITEMDELEGATE_H +#define DOWNLOADITEMDELEGATE_H + +#include +#include + +namespace QtGui { + +class DownloadItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + DownloadItemDelegate(QObject *parent); + + void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const; + +private: + const QPixmap m_folderIcon; +}; + +} + +#endif // DOWNLOADITEMDELEGATE_H diff --git a/gui/downloadview.cpp b/gui/downloadview.cpp new file mode 100644 index 0000000..78e0ad3 --- /dev/null +++ b/gui/downloadview.cpp @@ -0,0 +1,79 @@ +#include "./downloadview.h" +#include "./downloaditemdelegate.h" + +#include "../data/syncthingdownloadmodel.h" + +#include +#include +#include +#include +#include +#include + +using namespace Data; + +namespace QtGui { + +DownloadView::DownloadView(QWidget *parent) : + QTreeView(parent) +{ + header()->setSectionResizeMode(QHeaderView::ResizeToContents); + header()->hide(); + setItemDelegateForColumn(1, new DownloadItemDelegate(this)); + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &DownloadView::customContextMenuRequested, this, &DownloadView::showContextMenu); +} + +void DownloadView::mouseReleaseEvent(QMouseEvent *event) +{ + QTreeView::mouseReleaseEvent(event); + if(const SyncthingDownloadModel *dlModel = qobject_cast(model())) { + const QPoint pos(event->pos()); + const QModelIndex clickedIndex(indexAt(event->pos())); + if(clickedIndex.isValid() && clickedIndex.column() == 1) { + const QRect itemRect(visualRect(clickedIndex)); + if(pos.x() > itemRect.right() - 17) { + if(clickedIndex.parent().isValid()) { + if(const SyncthingItemDownloadProgress *progress = dlModel->progressInfo(clickedIndex)) { + emit openItemDir(*progress); + } + } else if(const SyncthingDir *dir = dlModel->dirInfo(clickedIndex)) { + emit openDir(*dir); + } + } + } + } +} + +void DownloadView::showContextMenu() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + QMenu menu; + if(selectionModel()->selectedRows(0).at(0).parent().isValid()) { + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy value")), &QAction::triggered, this, &DownloadView::copySelectedItem); + } else { + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy label/ID")), &QAction::triggered, this, &DownloadView::copySelectedItem); + } + menu.exec(QCursor::pos()); + } +} + +void DownloadView::copySelectedItem() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0); + QString text; + if(selectedIndex.parent().isValid()) { + // dev attribute + text = model()->data(model()->index(selectedIndex.row(), 1, selectedIndex.parent())).toString(); + } else { + // dev label/id + text = model()->data(selectedIndex).toString(); + } + if(!text.isEmpty()) { + QGuiApplication::clipboard()->setText(text); + } + } +} + +} diff --git a/gui/downloadview.h b/gui/downloadview.h new file mode 100644 index 0000000..8f2c84c --- /dev/null +++ b/gui/downloadview.h @@ -0,0 +1,34 @@ +#ifndef DOWNLOADVIEW_H +#define DOWNLOADVIEW_H + +#include + +namespace Data { +struct SyncthingItemDownloadProgress; +struct SyncthingDir; +} + +namespace QtGui { + +class DownloadView : public QTreeView +{ + Q_OBJECT +public: + DownloadView(QWidget *parent = nullptr); + +Q_SIGNALS: + void openDir(const Data::SyncthingDir &dir); + void openItemDir(const Data::SyncthingItemDownloadProgress &dir); + +protected: + void mouseReleaseEvent(QMouseEvent *event); + +private Q_SLOTS: + void showContextMenu(); + void copySelectedItem(); + +}; + +} + +#endif // DOWNLOADVIEW_H diff --git a/gui/traywidget.cpp b/gui/traywidget.cpp index 0fbb76a..12f9fea 100644 --- a/gui/traywidget.cpp +++ b/gui/traywidget.cpp @@ -52,6 +52,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) : #endif m_dirModel(m_connection), m_devModel(m_connection), + m_dlModel(m_connection), m_selectedConnection(nullptr) { m_ui->setupUi(this); @@ -59,6 +60,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) : // setup model and view m_ui->dirsTreeView->setModel(&m_dirModel); m_ui->devsTreeView->setModel(&m_devModel); + m_ui->downloadsTreeView->setModel(&m_dlModel); // setup sync-all button m_cornerFrame = new QFrame(this); @@ -113,6 +115,8 @@ TrayWidget::TrayWidget(TrayMenu *parent) : 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); + connect(m_ui->downloadsTreeView, &DownloadView::openDir, this, &TrayWidget::openDir); + connect(m_ui->downloadsTreeView, &DownloadView::openItemDir, this, &TrayWidget::openItemDir); connect(scanAllButton, &QPushButton::clicked, &m_connection, &SyncthingConnection::rescanAllDirs); connect(viewIdButton, &QPushButton::clicked, this, &TrayWidget::showOwnDeviceId); connect(showLogButton, &QPushButton::clicked, this, &TrayWidget::showLog); @@ -307,32 +311,35 @@ void TrayWidget::applySettings() } } -void TrayWidget::openDir(const QModelIndex &dirIndex) +void TrayWidget::openDir(const SyncthingDir &dir) { - if(const SyncthingDir *dir = m_dirModel.dirInfo(dirIndex)) { - if(QDir(dir->path).exists()) { - DesktopUtils::openLocalFileOrDir(dir->path); - } else { - QMessageBox::warning(this, QCoreApplication::applicationName(), tr("The directory %1 does not exist on the local machine.").arg(dir->path)); - } + if(QDir(dir.path).exists()) { + DesktopUtils::openLocalFileOrDir(dir.path); + } else { + QMessageBox::warning(this, QCoreApplication::applicationName(), tr("The directory %1 does not exist on the local machine.").arg(dir.path)); } } -void TrayWidget::scanDir(const QModelIndex &dirIndex) +void TrayWidget::openItemDir(const SyncthingItemDownloadProgress &item) { - if(const SyncthingDir *dir = m_dirModel.dirInfo(dirIndex)) { - m_connection.rescan(dir->id); + if(item.fileInfo.exists()) { + DesktopUtils::openLocalFileOrDir(item.fileInfo.path()); + } else { + QMessageBox::warning(this, QCoreApplication::applicationName(), tr("The file %1 does not exist on the local machine.").arg(item.fileInfo.filePath())); } } -void TrayWidget::pauseResumeDev(const QModelIndex &devIndex) +void TrayWidget::scanDir(const SyncthingDir &dir) { - if(const SyncthingDev *dev = m_devModel.devInfo(devIndex)) { - if(dev->paused) { - m_connection.resume(dev->id); - } else { - m_connection.pause(dev->id); - } + m_connection.rescan(dir.id); +} + +void TrayWidget::pauseResumeDev(const SyncthingDev &dev) +{ + if(dev.paused) { + m_connection.resume(dev.id); + } else { + m_connection.pause(dev.id); } } diff --git a/gui/traywidget.h b/gui/traywidget.h index ede9807..8be0e19 100644 --- a/gui/traywidget.h +++ b/gui/traywidget.h @@ -6,6 +6,7 @@ #include "../data/syncthingconnection.h" #include "../data/syncthingdirectorymodel.h" #include "../data/syncthingdevicemodel.h" +#include "../data/syncthingdownloadmodel.h" #include "../data/syncthingprocess.h" #include "../application/settings.h" @@ -57,9 +58,10 @@ public slots: private slots: void handleStatusChanged(Data::SyncthingStatus status); void applySettings(); - void openDir(const QModelIndex &dirIndex); - void scanDir(const QModelIndex &dirIndex); - void pauseResumeDev(const QModelIndex &devIndex); + void openDir(const Data::SyncthingDir &dir); + void openItemDir(const Data::SyncthingItemDownloadProgress &item); + void scanDir(const Data::SyncthingDir &dir); + void pauseResumeDev(const Data::SyncthingDev &dev); void changeStatus(); void updateTraffic(); #ifndef SYNCTHINGTRAY_NO_WEBVIEW @@ -81,6 +83,7 @@ private: Data::SyncthingConnection m_connection; Data::SyncthingDirectoryModel m_dirModel; Data::SyncthingDeviceModel m_devModel; + Data::SyncthingDownloadModel m_dlModel; QMenu *m_connectionsMenu; QActionGroup *m_connectionsActionGroup; Settings::ConnectionSettings *m_selectedConnection; diff --git a/gui/traywidget.ui b/gui/traywidget.ui index 7149053..e7e277f 100644 --- a/gui/traywidget.ui +++ b/gui/traywidget.ui @@ -276,7 +276,8 @@ For <i>all</i> notifications, checkout the log New notifications - + + .. true @@ -375,6 +376,35 @@ For <i>all</i> notifications, checkout the log + + + + .. + + + Downloads + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + @@ -390,6 +420,11 @@ For <i>all</i> notifications, checkout the log QTreeView
./gui/devview.h
+ + QtGui::DownloadView + QTreeView +
./gui/downloadview.h
+
diff --git a/testdata/downloadprogressevent.json b/testdata/downloadprogressevent.json new file mode 100644 index 0000000..0839d74 --- /dev/null +++ b/testdata/downloadprogressevent.json @@ -0,0 +1,51 @@ +{ + "id": 221, + "type": "DownloadProgress", + "time": "2014-12-13T00:26:12.9876937Z", + "data": { + "repo": { + "file1": { + "Total": 800, + "Pulling": 2, + "CopiedFromOrigin": 0, + "Reused": 633, + "CopiedFromElsewhere": 0, + "Pulled": 38, + "BytesTotal": 104792064, + "BytesDone": 87883776 + }, + "dir\\file2": { + "Total": 80, + "Pulling": 2, + "CopiedFromOrigin": 0, + "Reused": 0, + "CopiedFromElsewhere": 0, + "Pulled": 32, + "BytesTotal": 10420224, + "BytesDone": 4128768 + } + }, + "projects": { + "file3": { + "Total": 800, + "Pulling": 2, + "CopiedFromOrigin": 0, + "Reused": 633, + "CopiedFromElsewhere": 0, + "Pulled": 38, + "BytesTotal": 104792064, + "BytesDone": 87883776 + }, + "dir\\file4": { + "Total": 80, + "Pulling": 2, + "CopiedFromOrigin": 0, + "Reused": 0, + "CopiedFromElsewhere": 0, + "Pulled": 32, + "BytesTotal": 10420224, + "BytesDone": 4128768 + } + } + } +} diff --git a/translations/syncthingtray_de_DE.ts b/translations/syncthingtray_de_DE.ts index ed7e8ba..d870716 100644 --- a/translations/syncthingtray_de_DE.ts +++ b/translations/syncthingtray_de_DE.ts @@ -4,145 +4,145 @@ Data::SyncthingConnection - + disconnected - + reconnecting - + connected - + connected, notifications available - + connected, paused - + connected, synchronizing - + unknown - - + + 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 errors: - + Unable to request errors: - + 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: @@ -361,6 +361,19 @@ + + Data::SyncthingDownloadModel + + + Dir/item + + + + + Progress + + + Data::Utils @@ -488,77 +501,77 @@ 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. + + It is possible to save multiple configurations. This allows switching quickly between multiple Syncthing instances using the connection button in the right corner of the tray menu. The config label is an arbitrary name to identify a configuration and does not have to match the name of the corresponding Syncthing device. - + HTTPS certificate - + Status - + API key - + Insert values from local Syncthing configuration - + Password @@ -591,17 +604,17 @@ QtGui::DevView - + Copy value - + Copy name - + Copy ID @@ -609,21 +622,34 @@ QtGui::DirView - + Copy value - + Copy label/ID - + Copy path + + QtGui::DownloadView + + + Copy value + + + + + Copy label/ID + + + QtGui::LauncherOptionPage @@ -753,67 +779,72 @@ - + + Rescan all + + + + About - + Close - + Error - + Syncthing notification - click to dismiss - + Not connected to Syncthing - + Disconnected from Syncthing - + Reconnecting ... - + Syncthing is idling - + Syncthing is scanning - + Notifications available - + At least one device is paused - + Synchronization is ongoing - + Synchronization complete @@ -827,7 +858,7 @@ - + Connect @@ -857,10 +888,15 @@ For <i>all</i> notifications, checkout the log + + + Downloads + + - + unknown @@ -875,18 +911,18 @@ For <i>all</i> notifications, checkout the log - + Directories - + Devices - + About @@ -901,83 +937,88 @@ For <i>all</i> notifications, checkout the log - + View own device ID - + Rescan all directories - + Show Syncthing log - + Restart Syncthing - + Connection - + device ID is unknown - + Copy to clipboard - + New notifications - + 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. - + + The file <i>%1</i> does not exist on the local machine. + + + + Continue - + Own device ID - + Log diff --git a/translations/syncthingtray_en_US.ts b/translations/syncthingtray_en_US.ts index 81c3fb6..270c4c5 100644 --- a/translations/syncthingtray_en_US.ts +++ b/translations/syncthingtray_en_US.ts @@ -4,145 +4,145 @@ Data::SyncthingConnection - + disconnected - + reconnecting - + connected - + connected, notifications available - + connected, paused - + connected, synchronizing - + unknown - - + + 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 errors: - + Unable to request errors: - + 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: @@ -361,6 +361,19 @@ + + Data::SyncthingDownloadModel + + + Dir/item + + + + + Progress + + + Data::Utils @@ -488,77 +501,77 @@ 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. + + It is possible to save multiple configurations. This allows switching quickly between multiple Syncthing instances using the connection button in the right corner of the tray menu. The config label is an arbitrary name to identify a configuration and does not have to match the name of the corresponding Syncthing device. - + HTTPS certificate - + Status - + API key - + Insert values from local Syncthing configuration - + Password @@ -591,17 +604,17 @@ QtGui::DevView - + Copy value - + Copy name - + Copy ID @@ -609,21 +622,34 @@ QtGui::DirView - + Copy value - + Copy label/ID - + Copy path + + QtGui::DownloadView + + + Copy value + + + + + Copy label/ID + + + QtGui::LauncherOptionPage @@ -753,67 +779,72 @@ - + + Rescan all + + + + About - + Close - + Error - + Syncthing notification - click to dismiss - + Not connected to Syncthing - + Disconnected from Syncthing - + Reconnecting ... - + Syncthing is idling - + Syncthing is scanning - + Notifications available - + At least one device is paused - + Synchronization is ongoing - + Synchronization complete @@ -827,7 +858,7 @@ - + Connect @@ -857,10 +888,15 @@ For <i>all</i> notifications, checkout the log + + + Downloads + + - + unknown @@ -875,18 +911,18 @@ For <i>all</i> notifications, checkout the log - + Directories - + Devices - + About @@ -901,83 +937,88 @@ For <i>all</i> notifications, checkout the log - + View own device ID - + Rescan all directories - + Show Syncthing log - + Restart Syncthing - + Connection - + device ID is unknown - + Copy to clipboard - + New notifications - + 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. - + + The file <i>%1</i> does not exist on the local machine. + + + + Continue - + Own device ID - + Log