Show recent changes

This commit is contained in:
Martchus 2020-01-18 16:37:20 +01:00
parent e76d4afff5
commit 6026339c83
15 changed files with 447 additions and 11 deletions

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,205 @@
#include "./syncthingrecentchangesmodel.h"
#include "./colors.h"
#include "./syncthingicons.h"
#include "../connector/syncthingconnection.h"
#include "../connector/utils.h"
#include <c++utilities/conversion/stringconversion.h>
#include <QStringBuilder>
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<int, QByteArray> SyncthingRecentChangesModel::roleNames() const
{
const static QHash<int, QByteArray> roles{
{ Action, "action" },
{ ActionIcon, "actionIcon" },
{ ModifiedBy, "modifiedBy" },
{ DirectoryId, "directoryId" },
{ DirectoryName, "directoryName" },
{ Path, "path" },
{ EventTime, "eventTime" },
{ ExtendedAction, "extendedAction" },
{ ItemType, "itemType" },
};
return roles;
}
const QVector<int> &SyncthingRecentChangesModel::colorRoles() const
{
static const QVector<int> colorRoles({ Qt::DecorationRole, Qt::ForegroundRole });
return colorRoles;
}
QModelIndex SyncthingRecentChangesModel::index(int row, int column, const QModelIndex &parent) const
{
if (static_cast<size_t>(row) >= m_changes.size() || parent.isValid()) {
return QModelIndex();
}
return createIndex(row, column, static_cast<quintptr>(-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<size_t>(index.row()) >= m_changes.size()) {
return QVariant();
}
const SyncthingRecentChange &change = m_changes[m_changes.size() - static_cast<size_t>(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<int>(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

View File

@ -0,0 +1,59 @@
#ifndef DATA_SYNCTHINGRECENTCHANGESMODEL_H
#define DATA_SYNCTHINGRECENTCHANGESMODEL_H
#include "./syncthingmodel.h"
#include "../connector/syncthingdir.h"
#include <vector>
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<int, QByteArray> roleNames() const override;
const QVector<int> &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<SyncthingRecentChange> m_changes;
};
} // namespace Data
Q_DECLARE_METATYPE(Data::SyncthingRecentChange)
#endif // DATA_SYNCTHINGRECENTCHANGESMODEL_H

View File

@ -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

View File

@ -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)

View File

@ -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 <QSize>
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<Data::SyncthingDownloadModel *>(&m_downloadModel);
}
inline Data::SyncthingRecentChangesModel *SyncthingApplet::recentChangesModel() const
{
return const_cast<Data::SyncthingRecentChangesModel *>(&m_recentChangesModel);
}
inline Data::SyncthingStatusSelectionModel *SyncthingApplet::passiveSelectionModel() const
{
return const_cast<Data::SyncthingStatusSelectionModel *>(&m_passiveSelectionModel);

View File

@ -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")
}
}
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -46,6 +46,7 @@ set(REQUIRED_ICONS
dialog-cancel
dialog-ok
dialog-ok-apply
document-open-recent-symbolic
edit-copy
edit-clear
edit-cut

View File

@ -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);

View File

@ -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;

View File

@ -469,6 +469,35 @@ For &lt;i&gt;all&lt;/i&gt; notifications, checkout the log</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="recentChangesTab">
<attribute name="icon">
<iconset theme="document-open-recent-symbolic">
<normaloff>.</normaloff>.</iconset>
</attribute>
<attribute name="title">
<string>Recent changes</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTreeView" name="recentChangesTreeView"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>