diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 32029602..c69ebe60 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -3614,6 +3614,62 @@ Would you like to overwrite the existing attachment?
URL
URL
+
+ Age
+
+
+
+ Difference
+
+
+
+ Size
+ Size
+
+
+ Password
+ Password
+
+
+ Notes
+ Notes
+
+
+ Custom Attributes
+
+
+
+ Icon
+ Icon
+
+
+ Color
+
+
+
+ Expiration
+ Expiration
+
+
+ TOTP
+ TOTP
+
+
+ Custom Data
+
+
+
+ Attachments
+ Attachments
+
+
+ Auto-Type
+ Auto-Type
+
+
+ Current (%1)
+
+
EntryModel
@@ -7633,6 +7689,48 @@ Please consider generating a new key file.
KeeShare
+
+ over %1 year(s)
+
+
+
+
+
+
+ about %1 month(s)
+
+
+
+
+
+
+ %1 week(s)
+
+
+
+
+
+
+ %1 day(s)
+
+
+
+
+
+
+ %1 hour(s)
+
+
+
+
+
+
+ %1 minute(s)
+
+
+
+
+
QtIOCompressor
diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp
index 59ddf9e3..0cbe2b4f 100644
--- a/src/core/Tools.cpp
+++ b/src/core/Tools.cpp
@@ -35,6 +35,7 @@
#include
#include
#include
+#include
#ifdef Q_OS_WIN
#include // for Sleep()
@@ -133,6 +134,37 @@ namespace Tools
return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i));
}
+ QString humanReadableTimeDifference(qint64 seconds)
+ {
+ constexpr double secondsInHour = 3600;
+ constexpr double secondsInDay = secondsInHour * 24;
+ constexpr double secondsInWeek = secondsInDay * 7;
+ constexpr double secondsInMonth = secondsInDay * 30; // Approximation
+ constexpr double secondsInYear = secondsInDay * 365;
+
+ seconds = abs(seconds);
+
+ if (seconds >= secondsInYear) {
+ auto years = std::floor(seconds / secondsInYear);
+ return QObject::tr("over %1 year(s)", nullptr, years).arg(years);
+ } else if (seconds >= secondsInMonth) {
+ auto months = std::round(seconds / secondsInMonth);
+ return QObject::tr("about %1 month(s)", nullptr, months).arg(months);
+ } else if (seconds >= secondsInWeek) {
+ auto weeks = std::round(seconds / secondsInWeek);
+ return QObject::tr("%1 week(s)", nullptr, weeks).arg(weeks);
+ } else if (seconds >= secondsInDay) {
+ auto days = std::floor(seconds / secondsInDay);
+ return QObject::tr("%1 day(s)", nullptr, days).arg(days);
+ } else if (seconds >= secondsInHour) {
+ auto hours = std::floor(seconds / secondsInHour);
+ return QObject::tr("%1 hour(s)", nullptr, hours).arg(hours);
+ }
+
+ auto minutes = std::floor(seconds / 60);
+ return QObject::tr("%1 minute(s)", nullptr, minutes).arg(minutes);
+ }
+
bool readFromDevice(QIODevice* device, QByteArray& data, int size)
{
QByteArray buffer;
diff --git a/src/core/Tools.h b/src/core/Tools.h
index 2c22e742..c605143b 100644
--- a/src/core/Tools.h
+++ b/src/core/Tools.h
@@ -21,6 +21,7 @@
#include "core/Global.h"
+#include
#include
class QIODevice;
@@ -30,6 +31,7 @@ namespace Tools
{
QString debugInfo();
QString humanReadableFileSize(qint64 bytes, quint32 precision = 2);
+ QString humanReadableTimeDifference(qint64 seconds);
bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384);
bool readAllFromDevice(QIODevice* device, QByteArray& data);
bool isHex(const QByteArray& ba);
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index f9527966..78d8b48c 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -504,7 +504,9 @@ void EditEntryWidget::emitHistoryEntryActivated(const QModelIndex& index)
Q_ASSERT(!m_history);
Entry* entry = m_historyModel->entryFromIndex(index);
- emit historyEntryActivated(entry);
+ if (entry) {
+ emit historyEntryActivated(entry);
+ }
}
void EditEntryWidget::histEntryActivated(const QModelIndex& index)
@@ -521,7 +523,7 @@ void EditEntryWidget::updateHistoryButtons(const QModelIndex& current, const QMo
{
Q_UNUSED(previous);
- if (current.isValid()) {
+ if (m_historyModel->entryFromIndex(current)) {
m_historyUi->showButton->setEnabled(true);
m_historyUi->restoreButton->setEnabled(true);
m_historyUi->deleteButton->setEnabled(true);
@@ -1025,7 +1027,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid());
if (!m_history && !restore) {
- m_historyModel->setEntries(entry->historyItems());
+ m_historyModel->setEntries(entry->historyItems(), entry);
m_historyUi->historyView->sortByColumn(0, Qt::DescendingOrder);
}
if (m_historyModel->rowCount() > 0) {
@@ -1129,7 +1131,8 @@ bool EditEntryWidget::commitEntry()
m_entry->endUpdate();
}
- m_historyModel->setEntries(m_entry->historyItems());
+ m_historyModel->setEntries(m_entry->historyItems(), m_entry);
+ m_advancedUi->attachmentsWidget->linkAttachments(m_entry->attachments());
showMessage(tr("Entry updated successfully."), MessageWidget::Positive);
setModified(false);
@@ -1538,8 +1541,9 @@ void EditEntryWidget::showHistoryEntry()
void EditEntryWidget::restoreHistoryEntry()
{
QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex());
- if (index.isValid()) {
- setForms(m_historyModel->entryFromIndex(index), true);
+ auto entry = m_historyModel->entryFromIndex(index);
+ if (entry) {
+ setForms(entry, true);
setModified(true);
}
}
@@ -1547,7 +1551,7 @@ void EditEntryWidget::restoreHistoryEntry()
void EditEntryWidget::deleteHistoryEntry()
{
QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex());
- if (index.isValid()) {
+ if (m_historyModel->entryFromIndex(index)) {
m_historyModel->deleteIndex(index);
if (m_historyModel->rowCount() > 0) {
m_historyUi->deleteAllButton->setEnabled(true);
diff --git a/src/gui/entry/EntryHistoryModel.cpp b/src/gui/entry/EntryHistoryModel.cpp
index 2506e06d..beadce6f 100644
--- a/src/gui/entry/EntryHistoryModel.cpp
+++ b/src/gui/entry/EntryHistoryModel.cpp
@@ -17,8 +17,12 @@
#include "EntryHistoryModel.h"
+#include "core/Clock.h"
#include "core/Entry.h"
#include "core/Global.h"
+#include "core/Tools.h"
+
+#include
EntryHistoryModel::EntryHistoryModel(QObject* parent)
: QAbstractTableModel(parent)
@@ -27,8 +31,11 @@ EntryHistoryModel::EntryHistoryModel(QObject* parent)
Entry* EntryHistoryModel::entryFromIndex(const QModelIndex& index) const
{
- Q_ASSERT(index.isValid() && index.row() < m_historyEntries.size());
- return m_historyEntries.at(index.row());
+ if (!index.isValid() || index.row() >= m_historyEntries.size()) {
+ return nullptr;
+ }
+ auto entry = m_historyEntries.at(index.row());
+ return entry == m_parentEntry ? nullptr : entry;
}
int EntryHistoryModel::columnCount(const QModelIndex& parent) const
@@ -48,31 +55,50 @@ int EntryHistoryModel::rowCount(const QModelIndex& parent) const
QVariant EntryHistoryModel::data(const QModelIndex& index, int role) const
{
- if (!index.isValid()) {
- return QVariant();
+ if (index.row() >= m_historyEntries.size()) {
+ return {};
}
+ const auto entry = m_historyEntries[index.row()];
if (role == Qt::DisplayRole || role == Qt::UserRole) {
- Entry* entry = entryFromIndex(index);
- const TimeInfo& timeInfo = entry->timeInfo();
- QDateTime lastModificationLocalTime = timeInfo.lastModificationTime().toLocalTime();
+ QDateTime lastModified = entry->timeInfo().lastModificationTime().toLocalTime();
+ QDateTime now = Clock::currentDateTime();
+
switch (index.column()) {
case 0:
if (role == Qt::DisplayRole) {
- return lastModificationLocalTime.toString(Qt::SystemLocaleShortDate);
+ return lastModified.toString(Qt::SystemLocaleShortDate);
} else {
- return lastModificationLocalTime;
+ return lastModified;
}
- case 1:
- return entry->title();
- case 2:
- return entry->username();
- case 3:
- return entry->url();
+ case 1: {
+ const auto seconds = lastModified.secsTo(now);
+ if (role == Qt::DisplayRole) {
+ if (entry == m_parentEntry) {
+ return tr("Current (%1)").arg(Tools::humanReadableTimeDifference(seconds));
+ }
+ return Tools::humanReadableTimeDifference(seconds);
+ }
+ return seconds;
}
+ case 2:
+ if (index.row() < m_historyModifications.size()) {
+ return m_historyModifications[index.row()];
+ }
+ return {};
+ case 3:
+ if (role == Qt::DisplayRole) {
+ return Tools::humanReadableFileSize(entry->size(), 0);
+ }
+ return entry->size();
+ }
+ } else if (role == Qt::FontRole && entry == m_parentEntry) {
+ QFont font;
+ font.setBold(true);
+ return font;
}
- return QVariant();
+ return {};
}
QVariant EntryHistoryModel::headerData(int section, Qt::Orientation orientation, int role) const
@@ -82,24 +108,29 @@ QVariant EntryHistoryModel::headerData(int section, Qt::Orientation orientation,
case 0:
return tr("Last modified");
case 1:
- return tr("Title");
+ return tr("Age");
case 2:
- return tr("Username");
+ return tr("Difference");
case 3:
- return tr("URL");
+ return tr("Size");
}
}
- return QVariant();
+ return {};
}
-void EntryHistoryModel::setEntries(const QList& entries)
+void EntryHistoryModel::setEntries(const QList& entries, Entry* parentEntry)
{
beginResetModel();
-
+ m_parentEntry = parentEntry;
m_historyEntries = entries;
+ m_historyEntries << parentEntry;
+ // Sort the entries by last modified (newest -> oldest) so we can calculate the differences
+ std::sort(m_historyEntries.begin(), m_historyEntries.end(), [](const Entry* lhs, const Entry* rhs) {
+ return lhs->timeInfo().lastModificationTime() > rhs->timeInfo().lastModificationTime();
+ });
m_deletedHistoryEntries.clear();
-
+ calculateHistoryModifications();
endResetModel();
}
@@ -125,8 +156,8 @@ QList EntryHistoryModel::deletedEntries()
void EntryHistoryModel::deleteIndex(QModelIndex index)
{
- if (index.isValid()) {
- Entry* entry = entryFromIndex(index);
+ auto entry = entryFromIndex(index);
+ if (entry) {
beginRemoveRows(QModelIndex(), m_historyEntries.indexOf(entry), m_historyEntries.indexOf(entry));
m_historyEntries.removeAll(entry);
m_deletedHistoryEntries << entry;
@@ -141,8 +172,83 @@ void EntryHistoryModel::deleteAll()
beginRemoveRows(QModelIndex(), 0, m_historyEntries.size() - 1);
for (Entry* entry : asConst(m_historyEntries)) {
- m_deletedHistoryEntries << entry;
+ if (entry != m_parentEntry) {
+ m_deletedHistoryEntries << entry;
+ }
}
m_historyEntries.clear();
endRemoveRows();
}
+
+void EntryHistoryModel::calculateHistoryModifications()
+{
+ m_historyModifications.clear();
+
+ Entry* compare = nullptr;
+ for (const auto curr : m_historyEntries) {
+ if (!compare) {
+ compare = curr;
+ continue;
+ }
+
+ QStringList modifiedFields;
+
+ if (*curr->attributes() != *compare->attributes()) {
+ bool foundAttribute = false;
+
+ if (curr->title() != compare->title()) {
+ modifiedFields << tr("Title");
+ foundAttribute = true;
+ }
+ if (curr->username() != compare->username()) {
+ modifiedFields << tr("Username");
+ foundAttribute = true;
+ }
+ if (curr->password() != compare->password()) {
+ modifiedFields << tr("Password");
+ foundAttribute = true;
+ }
+ if (curr->url() != compare->url()) {
+ modifiedFields << tr("URL");
+ foundAttribute = true;
+ }
+ if (curr->notes() != compare->notes()) {
+ modifiedFields << tr("Notes");
+ foundAttribute = true;
+ }
+
+ if (!foundAttribute) {
+ modifiedFields << tr("Custom Attributes");
+ }
+ }
+ if (curr->iconNumber() != compare->iconNumber() || curr->iconUuid() != compare->iconUuid()) {
+ modifiedFields << tr("Icon");
+ }
+ if (curr->foregroundColor() != compare->foregroundColor()
+ || curr->backgroundColor() != compare->backgroundColor()) {
+ modifiedFields << tr("Color");
+ }
+ if (curr->timeInfo().expires() != compare->timeInfo().expires()
+ || curr->timeInfo().expiryTime() != compare->timeInfo().expiryTime()) {
+ modifiedFields << tr("Expiration");
+ }
+ if (curr->totpSettingsString() != compare->totpSettingsString()) {
+ modifiedFields << tr("TOTP");
+ }
+ if (*curr->customData() != *compare->customData()) {
+ modifiedFields << tr("Custom Data");
+ }
+ if (*curr->attachments() != *compare->attachments()) {
+ modifiedFields << tr("Attachments");
+ }
+ if (*curr->autoTypeAssociations() != *compare->autoTypeAssociations()
+ || curr->autoTypeEnabled() != compare->autoTypeEnabled()
+ || curr->defaultAutoTypeSequence() != compare->defaultAutoTypeSequence()) {
+ modifiedFields << tr("Auto-Type");
+ }
+
+ m_historyModifications << modifiedFields.join(", ");
+
+ compare = curr;
+ }
+}
diff --git a/src/gui/entry/EntryHistoryModel.h b/src/gui/entry/EntryHistoryModel.h
index 6d186f04..21897ec0 100644
--- a/src/gui/entry/EntryHistoryModel.h
+++ b/src/gui/entry/EntryHistoryModel.h
@@ -35,7 +35,7 @@ public:
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
- void setEntries(const QList& entries);
+ void setEntries(const QList& entries, Entry* parentEntry);
void clear();
void clearDeletedEntries();
QList deletedEntries();
@@ -43,8 +43,12 @@ public:
void deleteAll();
private:
+ void calculateHistoryModifications();
+
QList m_historyEntries;
QList m_deletedHistoryEntries;
+ QStringList m_historyModifications;
+ const Entry* m_parentEntry;
};
#endif // KEEPASSX_ENTRYHISTORYMODEL_H