Show what changed between entry history items

* Also show what is changed on the current state
* Closes #2621
This commit is contained in:
osx user 2021-08-01 00:01:45 +03:00 committed by Jonathan White
parent 12990e59ad
commit 15d1b2f0ab
6 changed files with 280 additions and 34 deletions

View file

@ -3614,6 +3614,62 @@ Would you like to overwrite the existing attachment?</source>
<source>URL</source> <source>URL</source>
<translation>URL</translation> <translation>URL</translation>
</message> </message>
<message>
<source>Age</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Difference</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Size</source>
<translation type="unfinished">Size</translation>
</message>
<message>
<source>Password</source>
<translation type="unfinished">Password</translation>
</message>
<message>
<source>Notes</source>
<translation type="unfinished">Notes</translation>
</message>
<message>
<source>Custom Attributes</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Icon</source>
<translation type="unfinished">Icon</translation>
</message>
<message>
<source>Color</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Expiration</source>
<translation type="unfinished">Expiration</translation>
</message>
<message>
<source>TOTP</source>
<translation type="unfinished">TOTP</translation>
</message>
<message>
<source>Custom Data</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attachments</source>
<translation type="unfinished">Attachments</translation>
</message>
<message>
<source>Auto-Type</source>
<translation type="unfinished">Auto-Type</translation>
</message>
<message>
<source>Current (%1)</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>EntryModel</name> <name>EntryModel</name>
@ -7633,6 +7689,48 @@ Please consider generating a new key file.</source>
<source>KeeShare</source> <source>KeeShare</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message numerus="yes">
<source>over %1 year(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<source>about %1 month(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<source>%1 week(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<source>%1 day(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<source>%1 hour(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<source>%1 minute(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context> </context>
<context> <context>
<name>QtIOCompressor</name> <name>QtIOCompressor</name>

View file

@ -35,6 +35,7 @@
#include <QStringList> #include <QStringList>
#include <QUrl> #include <QUrl>
#include <QUuid> #include <QUuid>
#include <cmath>
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
#include <windows.h> // for Sleep() #include <windows.h> // for Sleep()
@ -133,6 +134,37 @@ namespace Tools
return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i)); 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) bool readFromDevice(QIODevice* device, QByteArray& data, int size)
{ {
QByteArray buffer; QByteArray buffer;

View file

@ -21,6 +21,7 @@
#include "core/Global.h" #include "core/Global.h"
#include <QDateTime>
#include <QProcessEnvironment> #include <QProcessEnvironment>
class QIODevice; class QIODevice;
@ -30,6 +31,7 @@ namespace Tools
{ {
QString debugInfo(); QString debugInfo();
QString humanReadableFileSize(qint64 bytes, quint32 precision = 2); QString humanReadableFileSize(qint64 bytes, quint32 precision = 2);
QString humanReadableTimeDifference(qint64 seconds);
bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384); bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384);
bool readAllFromDevice(QIODevice* device, QByteArray& data); bool readAllFromDevice(QIODevice* device, QByteArray& data);
bool isHex(const QByteArray& ba); bool isHex(const QByteArray& ba);

View file

@ -504,7 +504,9 @@ void EditEntryWidget::emitHistoryEntryActivated(const QModelIndex& index)
Q_ASSERT(!m_history); Q_ASSERT(!m_history);
Entry* entry = m_historyModel->entryFromIndex(index); Entry* entry = m_historyModel->entryFromIndex(index);
emit historyEntryActivated(entry); if (entry) {
emit historyEntryActivated(entry);
}
} }
void EditEntryWidget::histEntryActivated(const QModelIndex& index) void EditEntryWidget::histEntryActivated(const QModelIndex& index)
@ -521,7 +523,7 @@ void EditEntryWidget::updateHistoryButtons(const QModelIndex& current, const QMo
{ {
Q_UNUSED(previous); Q_UNUSED(previous);
if (current.isValid()) { if (m_historyModel->entryFromIndex(current)) {
m_historyUi->showButton->setEnabled(true); m_historyUi->showButton->setEnabled(true);
m_historyUi->restoreButton->setEnabled(true); m_historyUi->restoreButton->setEnabled(true);
m_historyUi->deleteButton->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()); m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid());
if (!m_history && !restore) { if (!m_history && !restore) {
m_historyModel->setEntries(entry->historyItems()); m_historyModel->setEntries(entry->historyItems(), entry);
m_historyUi->historyView->sortByColumn(0, Qt::DescendingOrder); m_historyUi->historyView->sortByColumn(0, Qt::DescendingOrder);
} }
if (m_historyModel->rowCount() > 0) { if (m_historyModel->rowCount() > 0) {
@ -1129,7 +1131,8 @@ bool EditEntryWidget::commitEntry()
m_entry->endUpdate(); 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); showMessage(tr("Entry updated successfully."), MessageWidget::Positive);
setModified(false); setModified(false);
@ -1538,8 +1541,9 @@ void EditEntryWidget::showHistoryEntry()
void EditEntryWidget::restoreHistoryEntry() void EditEntryWidget::restoreHistoryEntry()
{ {
QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex()); QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex());
if (index.isValid()) { auto entry = m_historyModel->entryFromIndex(index);
setForms(m_historyModel->entryFromIndex(index), true); if (entry) {
setForms(entry, true);
setModified(true); setModified(true);
} }
} }
@ -1547,7 +1551,7 @@ void EditEntryWidget::restoreHistoryEntry()
void EditEntryWidget::deleteHistoryEntry() void EditEntryWidget::deleteHistoryEntry()
{ {
QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex()); QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex());
if (index.isValid()) { if (m_historyModel->entryFromIndex(index)) {
m_historyModel->deleteIndex(index); m_historyModel->deleteIndex(index);
if (m_historyModel->rowCount() > 0) { if (m_historyModel->rowCount() > 0) {
m_historyUi->deleteAllButton->setEnabled(true); m_historyUi->deleteAllButton->setEnabled(true);

View file

@ -17,8 +17,12 @@
#include "EntryHistoryModel.h" #include "EntryHistoryModel.h"
#include "core/Clock.h"
#include "core/Entry.h" #include "core/Entry.h"
#include "core/Global.h" #include "core/Global.h"
#include "core/Tools.h"
#include <QFont>
EntryHistoryModel::EntryHistoryModel(QObject* parent) EntryHistoryModel::EntryHistoryModel(QObject* parent)
: QAbstractTableModel(parent) : QAbstractTableModel(parent)
@ -27,8 +31,11 @@ EntryHistoryModel::EntryHistoryModel(QObject* parent)
Entry* EntryHistoryModel::entryFromIndex(const QModelIndex& index) const Entry* EntryHistoryModel::entryFromIndex(const QModelIndex& index) const
{ {
Q_ASSERT(index.isValid() && index.row() < m_historyEntries.size()); if (!index.isValid() || index.row() >= m_historyEntries.size()) {
return m_historyEntries.at(index.row()); return nullptr;
}
auto entry = m_historyEntries.at(index.row());
return entry == m_parentEntry ? nullptr : entry;
} }
int EntryHistoryModel::columnCount(const QModelIndex& parent) const 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 QVariant EntryHistoryModel::data(const QModelIndex& index, int role) const
{ {
if (!index.isValid()) { if (index.row() >= m_historyEntries.size()) {
return QVariant(); return {};
} }
const auto entry = m_historyEntries[index.row()];
if (role == Qt::DisplayRole || role == Qt::UserRole) { if (role == Qt::DisplayRole || role == Qt::UserRole) {
Entry* entry = entryFromIndex(index); QDateTime lastModified = entry->timeInfo().lastModificationTime().toLocalTime();
const TimeInfo& timeInfo = entry->timeInfo(); QDateTime now = Clock::currentDateTime();
QDateTime lastModificationLocalTime = timeInfo.lastModificationTime().toLocalTime();
switch (index.column()) { switch (index.column()) {
case 0: case 0:
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
return lastModificationLocalTime.toString(Qt::SystemLocaleShortDate); return lastModified.toString(Qt::SystemLocaleShortDate);
} else { } else {
return lastModificationLocalTime; return lastModified;
} }
case 1: case 1: {
return entry->title(); const auto seconds = lastModified.secsTo(now);
case 2: if (role == Qt::DisplayRole) {
return entry->username(); if (entry == m_parentEntry) {
case 3: return tr("Current (%1)").arg(Tools::humanReadableTimeDifference(seconds));
return entry->url(); }
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 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: case 0:
return tr("Last modified"); return tr("Last modified");
case 1: case 1:
return tr("Title"); return tr("Age");
case 2: case 2:
return tr("Username"); return tr("Difference");
case 3: case 3:
return tr("URL"); return tr("Size");
} }
} }
return QVariant(); return {};
} }
void EntryHistoryModel::setEntries(const QList<Entry*>& entries) void EntryHistoryModel::setEntries(const QList<Entry*>& entries, Entry* parentEntry)
{ {
beginResetModel(); beginResetModel();
m_parentEntry = parentEntry;
m_historyEntries = entries; 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(); m_deletedHistoryEntries.clear();
calculateHistoryModifications();
endResetModel(); endResetModel();
} }
@ -125,8 +156,8 @@ QList<Entry*> EntryHistoryModel::deletedEntries()
void EntryHistoryModel::deleteIndex(QModelIndex index) void EntryHistoryModel::deleteIndex(QModelIndex index)
{ {
if (index.isValid()) { auto entry = entryFromIndex(index);
Entry* entry = entryFromIndex(index); if (entry) {
beginRemoveRows(QModelIndex(), m_historyEntries.indexOf(entry), m_historyEntries.indexOf(entry)); beginRemoveRows(QModelIndex(), m_historyEntries.indexOf(entry), m_historyEntries.indexOf(entry));
m_historyEntries.removeAll(entry); m_historyEntries.removeAll(entry);
m_deletedHistoryEntries << entry; m_deletedHistoryEntries << entry;
@ -141,8 +172,83 @@ void EntryHistoryModel::deleteAll()
beginRemoveRows(QModelIndex(), 0, m_historyEntries.size() - 1); beginRemoveRows(QModelIndex(), 0, m_historyEntries.size() - 1);
for (Entry* entry : asConst(m_historyEntries)) { for (Entry* entry : asConst(m_historyEntries)) {
m_deletedHistoryEntries << entry; if (entry != m_parentEntry) {
m_deletedHistoryEntries << entry;
}
} }
m_historyEntries.clear(); m_historyEntries.clear();
endRemoveRows(); 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;
}
}

View file

@ -35,7 +35,7 @@ public:
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
void setEntries(const QList<Entry*>& entries); void setEntries(const QList<Entry*>& entries, Entry* parentEntry);
void clear(); void clear();
void clearDeletedEntries(); void clearDeletedEntries();
QList<Entry*> deletedEntries(); QList<Entry*> deletedEntries();
@ -43,8 +43,12 @@ public:
void deleteAll(); void deleteAll();
private: private:
void calculateHistoryModifications();
QList<Entry*> m_historyEntries; QList<Entry*> m_historyEntries;
QList<Entry*> m_deletedHistoryEntries; QList<Entry*> m_deletedHistoryEntries;
QStringList m_historyModifications;
const Entry* m_parentEntry;
}; };
#endif // KEEPASSX_ENTRYHISTORYMODEL_H #endif // KEEPASSX_ENTRYHISTORYMODEL_H