diff --git a/connector/syncthingconnection.h b/connector/syncthingconnection.h index d654f51..4440d1d 100644 --- a/connector/syncthingconnection.h +++ b/connector/syncthingconnection.h @@ -218,6 +218,7 @@ Q_SIGNALS: void newEvents(const QJsonArray &events); void dirStatusChanged(const SyncthingDir &dir, int index); void devStatusChanged(const SyncthingDev &dev, int index); + void fileChanged(const SyncthingDir &dir, int index, const SyncthingFileChange &fileChange); void downloadProgressChanged(); void dirStatisticsChanged(); void dirCompleted(CppUtilities::DateTime when, const SyncthingDir &dir, int index, const SyncthingDev *remoteDev = nullptr); diff --git a/connector/syncthingconnection_requests.cpp b/connector/syncthingconnection_requests.cpp index fd20ebe..1ae4820 100644 --- a/connector/syncthingconnection_requests.cpp +++ b/connector/syncthingconnection_requests.cpp @@ -1488,6 +1488,7 @@ void SyncthingConnection::readChangeEvent(DateTime eventTime, const QString &eve change.path = eventData.value(QLatin1String("path")).toString(); dirInfo->recentChanges.emplace_back(move(change)); emit dirStatusChanged(*dirInfo, index); + emit fileChanged(*dirInfo, index, dirInfo->recentChanges.back()); } // events / long polling API diff --git a/model/CMakeLists.txt b/model/CMakeLists.txt index 9d95044..db66886 100644 --- a/model/CMakeLists.txt +++ b/model/CMakeLists.txt @@ -14,11 +14,18 @@ set(HEADER_FILES syncthingdirectorymodel.h syncthingdevicemodel.h syncthingdownloadmodel.h + syncthingrecentchangesmodel.h syncthingstatusselectionmodel.h syncthingicons.h colors.h) -set(SRC_FILES syncthingmodel.cpp syncthingdirectorymodel.cpp syncthingdevicemodel.cpp syncthingdownloadmodel.cpp - syncthingstatusselectionmodel.cpp syncthingicons.cpp) +set(SRC_FILES + syncthingmodel.cpp + syncthingdirectorymodel.cpp + syncthingdevicemodel.cpp + syncthingdownloadmodel.cpp + syncthingrecentchangesmodel.cpp + syncthingstatusselectionmodel.cpp + syncthingicons.cpp) set(RES_FILES resources/${META_PROJECT_NAME}icons.qrc) set(TS_FILES translations/${META_PROJECT_NAME}_cs_CZ.ts translations/${META_PROJECT_NAME}_de_DE.ts diff --git a/model/syncthingdirectorymodel.cpp b/model/syncthingdirectorymodel.cpp index ca11311..67f068d 100644 --- a/model/syncthingdirectorymodel.cpp +++ b/model/syncthingdirectorymodel.cpp @@ -14,7 +14,7 @@ using namespace CppUtilities; namespace Data { -int computeDirectoryRowCount(const SyncthingDir &dir) +static int computeDirectoryRowCount(const SyncthingDir &dir) { return dir.paused ? 8 : 10; } @@ -260,10 +260,10 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const if (!dir.lastFileTime.isNull()) { if (dir.lastFileDeleted) { return tr("Deleted at %1") - .arg(QString::fromLatin1(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true).data())); + .arg(QString::fromStdString(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true))); } else { return tr("Updated at %1") - .arg(QString::fromLatin1(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true).data())); + .arg(QString::fromStdString(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true))); } } break; diff --git a/model/syncthingrecentchangesmodel.cpp b/model/syncthingrecentchangesmodel.cpp new file mode 100644 index 0000000..d3aea4e --- /dev/null +++ b/model/syncthingrecentchangesmodel.cpp @@ -0,0 +1,205 @@ +#include "./syncthingrecentchangesmodel.h" +#include "./colors.h" +#include "./syncthingicons.h" + +#include "../connector/syncthingconnection.h" +#include "../connector/utils.h" + +#include + +#include + +using namespace std; +using namespace CppUtilities; + +namespace Data { + +SyncthingRecentChangesModel::SyncthingRecentChangesModel(SyncthingConnection &connection, QObject *parent) + : SyncthingModel(connection, parent) +{ + for (const auto &dir : connection.dirInfo()) { + for (const auto &fileChange : dir.recentChanges) { + fileChanged(dir, -1, fileChange); + } + } + connect(&m_connection, &SyncthingConnection::fileChanged, this, &SyncthingRecentChangesModel::fileChanged); +} + +QHash SyncthingRecentChangesModel::roleNames() const +{ + const static QHash roles{ + { Action, "action" }, + { ActionIcon, "actionIcon" }, + { ModifiedBy, "modifiedBy" }, + { DirectoryId, "directoryId" }, + { DirectoryName, "directoryName" }, + { Path, "path" }, + { EventTime, "eventTime" }, + { ExtendedAction, "extendedAction" }, + { ItemType, "itemType" }, + }; + return roles; +} + +const QVector &SyncthingRecentChangesModel::colorRoles() const +{ + static const QVector colorRoles({ Qt::DecorationRole, Qt::ForegroundRole }); + return colorRoles; +} + +QModelIndex SyncthingRecentChangesModel::index(int row, int column, const QModelIndex &parent) const +{ + if (static_cast(row) >= m_changes.size() || parent.isValid()) { + return QModelIndex(); + } + return createIndex(row, column, static_cast(-1)); +} + +QModelIndex SyncthingRecentChangesModel::parent(const QModelIndex &child) const +{ + return QModelIndex(); +} + +QVariant SyncthingRecentChangesModel::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("Action"); + case 1: + return tr("Device"); + case 2: + return tr("Directory"); + case 3: + return tr("Path"); + } + break; + default:; + } + break; + default:; + } + return QVariant(); +} + +QVariant SyncthingRecentChangesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.parent().isValid() || static_cast(index.row()) >= m_changes.size()) { + return QVariant(); + } + + const SyncthingRecentChange &change = m_changes[m_changes.size() - static_cast(index.row()) - 1]; + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch (index.column()) { + case 0: + return change.fileChange.action; + case 1: + return change.fileChange.modifiedBy; + case 2: + return change.directoryId; + case 3: + return change.fileChange.path; + } + break; + case Qt::DecorationRole: + case ActionIcon: + switch (index.column()) { + case 0: + if (change.fileChange.local) { + return m_brightColors ? fontAwesomeIconsForDarkTheme().home : fontAwesomeIconsForLightTheme().home; + } else { + return m_brightColors ? fontAwesomeIconsForDarkTheme().globe : fontAwesomeIconsForLightTheme().globe; + } + } + break; + case Qt::ToolTipRole: + switch (index.column()) { + case 0: + return QString((change.fileChange.local ? tr("Locally") : tr("Remotely")) % QChar(' ') % change.fileChange.action % QStringLiteral(", ") + % QString::fromStdString(change.fileChange.eventTime.toString(DateTimeOutputFormat::DateAndTime, true))); + case 3: + return change.fileChange.path; // usually too long so add a tooltip + } + break; + case Action: + return change.fileChange.action; + case ModifiedBy: + return change.fileChange.modifiedBy; + case DirectoryId: + return change.directoryId; + case DirectoryName: + return change.directoryName; + case Path: + return change.fileChange.path; + case EventTime: + return QString::fromStdString(change.fileChange.eventTime.toString(DateTimeOutputFormat::DateAndTime, true)); + case ExtendedAction: { + auto extendedAction = change.fileChange.action; + extendedAction[0] = extendedAction[0].toUpper(); + return QVariant(move(extendedAction)); + } + case ItemType: + return change.fileChange.type; + default:; + } + + return QVariant(); +} + +bool SyncthingRecentChangesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_UNUSED(index) + Q_UNUSED(value) + Q_UNUSED(role) + return false; +} + +int SyncthingRecentChangesModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return static_cast(m_changes.size()); + } else { + return 0; + } +} + +int SyncthingRecentChangesModel::columnCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return 4; // action, device, folder, path + } else { + return 0; + } +} + +void SyncthingRecentChangesModel::fileChanged(const SyncthingDir &dir, int index, const SyncthingFileChange &change) +{ + Q_UNUSED(index) + + if (index >= 0) { + beginInsertRows(QModelIndex(), 0, 0); + } + m_changes.emplace_back(SyncthingRecentChange{ + .directoryId = dir.id, + .directoryName = dir.displayName(), + .fileChange = change, + }); + if (index >= 0) { + endInsertRows(); + } +} + +void SyncthingRecentChangesModel::handleConfigInvalidated() +{ +} + +void SyncthingRecentChangesModel::handleNewConfigAvailable() +{ +} + +} // namespace Data diff --git a/model/syncthingrecentchangesmodel.h b/model/syncthingrecentchangesmodel.h new file mode 100644 index 0000000..9cde157 --- /dev/null +++ b/model/syncthingrecentchangesmodel.h @@ -0,0 +1,59 @@ +#ifndef DATA_SYNCTHINGRECENTCHANGESMODEL_H +#define DATA_SYNCTHINGRECENTCHANGESMODEL_H + +#include "./syncthingmodel.h" + +#include "../connector/syncthingdir.h" + +#include + +namespace Data { + +struct LIB_SYNCTHING_MODEL_EXPORT SyncthingRecentChange { + QString directoryId; + QString directoryName; + SyncthingFileChange fileChange; +}; + +class LIB_SYNCTHING_MODEL_EXPORT SyncthingRecentChangesModel : public SyncthingModel { + Q_OBJECT +public: + enum SyncthingRecentChangesModelRole { + Action = Qt::UserRole + 1, + ActionIcon, + ModifiedBy, + DirectoryId, + DirectoryName, + Path, + EventTime, + ExtendedAction, + ItemType, + }; + + explicit SyncthingRecentChangesModel(SyncthingConnection &connection, QObject *parent = nullptr); + +public Q_SLOTS: + QHash roleNames() const override; + const QVector &colorRoles() const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + +private Q_SLOTS: + void fileChanged(const SyncthingDir &dir, int index, const SyncthingFileChange &change); + void handleConfigInvalidated() override; + void handleNewConfigAvailable() override; + +private: + std::vector m_changes; +}; + +} // namespace Data + +Q_DECLARE_METATYPE(Data::SyncthingRecentChange) + +#endif // DATA_SYNCTHINGRECENTCHANGESMODEL_H diff --git a/plasmoid/lib/CMakeLists.txt b/plasmoid/lib/CMakeLists.txt index 227c72f..ad4d09c 100644 --- a/plasmoid/lib/CMakeLists.txt +++ b/plasmoid/lib/CMakeLists.txt @@ -9,6 +9,7 @@ set(PLASMOID_FILES ../package/contents/ui/DirectoriesPage.qml ../package/contents/ui/DevicesPage.qml ../package/contents/ui/DownloadsPage.qml + ../package/contents/ui/RecentChangesPage.qml ../package/contents/ui/TopLevelView.qml ../package/contents/ui/TopLevelItem.qml ../package/contents/ui/DetailView.qml diff --git a/plasmoid/lib/syncthingapplet.cpp b/plasmoid/lib/syncthingapplet.cpp index 69eff41..4622b56 100644 --- a/plasmoid/lib/syncthingapplet.cpp +++ b/plasmoid/lib/syncthingapplet.cpp @@ -55,6 +55,7 @@ SyncthingApplet::SyncthingApplet(QObject *parent, const QVariantList &data) , m_dirModel(m_connection) , m_devModel(m_connection) , m_downloadModel(m_connection) + , m_recentChangesModel(m_connection) , m_settingsDlg(nullptr) #ifndef SYNCTHINGWIDGETS_NO_WEBVIEW , m_webViewDlg(nullptr) diff --git a/plasmoid/lib/syncthingapplet.h b/plasmoid/lib/syncthingapplet.h index 4b97b8c..6ca7656 100644 --- a/plasmoid/lib/syncthingapplet.h +++ b/plasmoid/lib/syncthingapplet.h @@ -8,6 +8,7 @@ #include "../../model/syncthingdevicemodel.h" #include "../../model/syncthingdirectorymodel.h" #include "../../model/syncthingdownloadmodel.h" +#include "../../model/syncthingrecentchangesmodel.h" #include "../../model/syncthingstatusselectionmodel.h" #include "../../connector/syncthingconnection.h" @@ -22,13 +23,7 @@ #include namespace Data { -class SyncthingConnection; struct SyncthingConnectionSettings; -class SyncthingDirectoryModel; -class SyncthingDeviceModel; -class SyncthingDownloadModel; -class SyncthingService; -enum class SyncthingErrorCategory; } // namespace Data namespace QtGui { @@ -45,6 +40,7 @@ class SyncthingApplet : public Plasma::Applet { Q_PROPERTY(Data::SyncthingDirectoryModel *dirModel READ dirModel NOTIFY dirModelChanged) Q_PROPERTY(Data::SyncthingDeviceModel *devModel READ devModel NOTIFY devModelChanged) Q_PROPERTY(Data::SyncthingDownloadModel *downloadModel READ downloadModel NOTIFY downloadModelChanged) + Q_PROPERTY(Data::SyncthingRecentChangesModel *recentChangesModel READ recentChangesModel NOTIFY recentChangesModelChanged) Q_PROPERTY(Data::SyncthingStatusSelectionModel *passiveSelectionModel READ passiveSelectionModel NOTIFY passiveSelectionModelChanged) Q_PROPERTY(Data::SyncthingService *service READ service NOTIFY serviceChanged) Q_PROPERTY(bool local READ isLocal NOTIFY localChanged) @@ -76,6 +72,7 @@ public: Data::SyncthingDirectoryModel *dirModel() const; Data::SyncthingDeviceModel *devModel() const; Data::SyncthingDownloadModel *downloadModel() const; + Data::SyncthingRecentChangesModel *recentChangesModel() const; Data::SyncthingStatusSelectionModel *passiveSelectionModel() const; Data::SyncthingService *service() const; bool isLocal() const; @@ -128,6 +125,8 @@ Q_SIGNALS: /// \remarks Never emitted, just to silence "... depends on non-NOTIFYable ..." void downloadModelChanged(); /// \remarks Never emitted, just to silence "... depends on non-NOTIFYable ..." + void recentChangesModelChanged(); + /// \remarks Never emitted, just to silence "... depends on non-NOTIFYable ..." void passiveSelectionModelChanged(); /// \remarks Never emitted, just to silence "... depends on non-NOTIFYable ..." void serviceChanged(); @@ -172,6 +171,7 @@ private: Data::SyncthingDirectoryModel m_dirModel; Data::SyncthingDeviceModel m_devModel; Data::SyncthingDownloadModel m_downloadModel; + Data::SyncthingRecentChangesModel m_recentChangesModel; Data::SyncthingStatusSelectionModel m_passiveSelectionModel; SettingsDialog *m_settingsDlg; QtGui::DBusStatusNotifier m_dbusNotifier; @@ -204,6 +204,11 @@ inline Data::SyncthingDownloadModel *SyncthingApplet::downloadModel() const return const_cast(&m_downloadModel); } +inline Data::SyncthingRecentChangesModel *SyncthingApplet::recentChangesModel() const +{ + return const_cast(&m_recentChangesModel); +} + inline Data::SyncthingStatusSelectionModel *SyncthingApplet::passiveSelectionModel() const { return const_cast(&m_passiveSelectionModel); diff --git a/plasmoid/package/contents/ui/FullRepresentation.qml b/plasmoid/package/contents/ui/FullRepresentation.qml index a9991dc..676d9ce 100644 --- a/plasmoid/package/contents/ui/FullRepresentation.qml +++ b/plasmoid/package/contents/ui/FullRepresentation.qml @@ -523,6 +523,13 @@ ColumnLayout { iconSource: "folder-download-symbolic" tab: downloadsPage } + PlasmaComponents.TabButton { + id: recentChangesTabButton + //text: qsTr("Recent changes") + iconSource: "document-open-recent-symbolic" + tab: recentChangesPage + } + } Item { Layout.fillHeight: true @@ -577,6 +584,11 @@ ColumnLayout { when: mainTabGroup.currentTab === downloadsPage source: Qt.resolvedUrl("DownloadsPage.qml") } + PlasmaExtras.ConditionalLoader { + id: recentChangesPage + when: mainTabGroup.currentTab === recentChangesPage + source: Qt.resolvedUrl("RecentChangesPage.qml") + } } } } diff --git a/plasmoid/package/contents/ui/RecentChangesPage.qml b/plasmoid/package/contents/ui/RecentChangesPage.qml new file mode 100644 index 0000000..50f7f5b --- /dev/null +++ b/plasmoid/package/contents/ui/RecentChangesPage.qml @@ -0,0 +1,110 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.1 +import QtQml.Models 2.2 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + property alias view: recentChangesView + anchors.fill: parent + objectName: "RecentChangesPage" + + PlasmaExtras.ScrollArea { + anchors.fill: parent + + TopLevelView { + id: recentChangesView + model: plasmoid.nativeInterface.recentChangesModel + delegate: TopLevelItem { + ColumnLayout { + width: parent.width + spacing: 0 + RowLayout { + Layout.fillWidth: true + PlasmaCore.IconItem { + Layout.preferredWidth: units.iconSizes.small + Layout.preferredHeight: units.iconSizes.small + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + source: actionIcon + } + PlasmaComponents.Label { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillWidth: true + elide: Text.ElideRight + text: extendedAction + } + Item { + width: units.smallSpacing + } + PlasmaCore.IconItem { + Layout.preferredWidth: units.iconSizes.small + Layout.preferredHeight: units.iconSizes.small + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + source: "change-date-symbolic" + } + PlasmaComponents.Label { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + elide: Text.ElideRight + text: eventTime + } + Item { + width: units.smallSpacing + } + PlasmaCore.IconItem { + Layout.preferredWidth: units.iconSizes.small + Layout.preferredHeight: units.iconSizes.small + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + source: "network-server-symbolic" + } + PlasmaComponents.Label { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + elide: Text.ElideRight + text: modifiedBy + } + } + RowLayout { + Layout.fillWidth: true + PlasmaCore.IconItem { + Layout.preferredWidth: units.iconSizes.small + Layout.preferredHeight: units.iconSizes.small + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + source: itemType === "file" ? "view-refresh-symbolic" : "folder-sync" + } + PlasmaComponents.Label { + text: directoryId + ": " + font.weight: Font.DemiBold + } + PlasmaComponents.Label { + Layout.fillWidth: true + text: path + elide: Text.ElideRight + } + } + } + + function copyPath() { + plasmoid.nativeInterface.copyToClipboard(path) + } + function copyDeviceId() { + plasmoid.nativeInterface.copyToClipboard(modifiedBy) + } + } + + PlasmaComponents.Menu { + id: contextMenu + PlasmaComponents.MenuItem { + text: qsTr("Copy path") + icon: "edit-copy" + onClicked: recentChangesView.currentItem.copyPath() + } + PlasmaComponents.MenuItem { + text: qsTr("Copy device ID") + icon: "network-server-symbolic" + onClicked: recentChangesView.currentItem.copyDeviceId() + } + } + } + } +} diff --git a/tray/CMakeLists.txt b/tray/CMakeLists.txt index 6ebaaed..89fa70b 100644 --- a/tray/CMakeLists.txt +++ b/tray/CMakeLists.txt @@ -46,6 +46,7 @@ set(REQUIRED_ICONS dialog-cancel dialog-ok dialog-ok-apply + document-open-recent-symbolic edit-copy edit-clear edit-cut diff --git a/tray/gui/traywidget.cpp b/tray/gui/traywidget.cpp index 125a3b7..921feb5 100644 --- a/tray/gui/traywidget.cpp +++ b/tray/gui/traywidget.cpp @@ -76,6 +76,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) , m_dirModel(m_connection) , m_devModel(m_connection) , m_dlModel(m_connection) + , m_recentChangesModel(m_connection) , m_selectedConnection(nullptr) , m_startStopButtonTarget(StartStopButtonTarget::None) { @@ -87,6 +88,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) m_ui->dirsTreeView->setModel(&m_dirModel); m_ui->devsTreeView->setModel(&m_devModel); m_ui->downloadsTreeView->setModel(&m_dlModel); + m_ui->recentChangesTreeView->setModel(&m_recentChangesModel); // setup sync-all button m_cornerFrame = new QFrame(this); diff --git a/tray/gui/traywidget.h b/tray/gui/traywidget.h index 9c1691b..6332f33 100644 --- a/tray/gui/traywidget.h +++ b/tray/gui/traywidget.h @@ -7,6 +7,7 @@ #include "../../model/syncthingdevicemodel.h" #include "../../model/syncthingdirectorymodel.h" #include "../../model/syncthingdownloadmodel.h" +#include "../../model/syncthingrecentchangesmodel.h" #include "../../connector/syncthingconnection.h" #include "../../connector/syncthingnotifier.h" @@ -117,6 +118,7 @@ private: Data::SyncthingDirectoryModel m_dirModel; Data::SyncthingDeviceModel m_devModel; Data::SyncthingDownloadModel m_dlModel; + Data::SyncthingRecentChangesModel m_recentChangesModel; QMenu *m_connectionsMenu; QActionGroup *m_connectionsActionGroup; Data::SyncthingConnectionSettings *m_selectedConnection; diff --git a/tray/gui/traywidget.ui b/tray/gui/traywidget.ui index 31ae9bd..614501c 100644 --- a/tray/gui/traywidget.ui +++ b/tray/gui/traywidget.ui @@ -469,6 +469,35 @@ For <i>all</i> notifications, checkout the log + + + + .. + + + Recent changes + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + +