#include "ui/DiffTableModel.h"
#include "ui/Palette.h"
#include <algorithm>
#include <cstdlib>
#include <QDateTime>
#include <QLocale>
#include <QStringList>
namespace whittle {
namespace {
QString humanSize(std::int64_t bytes) {
return QLocale().formattedDataSize(bytes, 1,
QLocale::DataSizeTraditionalFormat);
}
QString humanDuration(std::int64_t ns) {
ns = std::llabs(ns);
if (ns == 0) return QStringLiteral("0s");
const std::int64_t s = ns / 1'000'000'000LL;
if (s == 0) {
if (ns >= 1'000'000LL) return QStringLiteral("%1ms").arg(ns / 1'000'000LL);
if (ns >= 1'000LL) return QStringLiteral("%1us").arg(ns / 1'000LL);
return QStringLiteral("%1ns").arg(ns);
}
const std::int64_t d = s / 86400, h = (s % 86400) / 3600,
m = (s % 3600) / 60, sec = s % 60;
QStringList p;
if (d) p << QStringLiteral("%1d").arg(d);
if (h) p << QStringLiteral("%1h").arg(h);
if (m) p << QStringLiteral("%1m").arg(m);
if (sec || p.isEmpty()) p << QStringLiteral("%1s").arg(sec);
while (p.size() > 2) p.removeLast();
return p.join(' ');
}
QString displayName(const QString& relPath) {
const int last = relPath.lastIndexOf('/');
if (last <= 0) return relPath;
const int prev = relPath.lastIndexOf('/', last - 1);
return relPath.mid(prev + 1);
}
}
DiffTableModel::DiffTableModel(bool checkable, QObject* parent)
: QAbstractTableModel(parent), m_checkable(checkable) {}
void DiffTableModel::setRows(QList<FileRecord> rows,
QHash<QString, FileRecord> peer) {
beginResetModel();
m_rows = std::move(rows);
m_peer = std::move(peer);
m_skipped.clear();
m_checked.clear();
if (m_checkable)
for (const auto& r : m_rows) m_checked.insert(r.relPath);
applySort();
endResetModel();
}
void DiffTableModel::sort(int column, Qt::SortOrder order) {
if (column < 0 || column >= ColumnCount) return;
m_sortColumn = column;
m_sortOrder = order;
beginResetModel();
applySort();
endResetModel();
}
void DiffTableModel::applySort() {
const int col = m_sortColumn;
const bool asc = (m_sortOrder == Qt::AscendingOrder);
const auto delta = [this](const FileRecord& r) -> std::int64_t {
const auto it = m_peer.constFind(r.relPath);
if (it == m_peer.constEnd()) return -1;
return std::llabs(r.mtimeNs - it->mtimeNs);
};
std::stable_sort(m_rows.begin(), m_rows.end(),
[&](const FileRecord& a, const FileRecord& b) {
int cmp = 0;
switch (col) {
case Size:
cmp = (a.size < b.size) ? -1 : (a.size > b.size ? 1 : 0);
break;
case Modified:
cmp = (a.mtimeNs < b.mtimeNs) ? -1 : (a.mtimeNs > b.mtimeNs ? 1 : 0);
break;
case TimeDiff: {
const auto da = delta(a), db = delta(b);
cmp = (da < db) ? -1 : (da > db ? 1 : 0);
break;
}
case Name:
default:
cmp = displayName(a.relPath).compare(displayName(b.relPath),
Qt::CaseInsensitive);
break;
}
if (cmp == 0) cmp = a.relPath.compare(b.relPath);
return asc ? cmp < 0 : cmp > 0;
});
}
void DiffTableModel::setSkipped(const QSet<QString>& relPaths) {
m_skipped = relPaths;
if (!m_rows.isEmpty())
emit dataChanged(index(0, 0), index(m_rows.size() - 1, ColumnCount - 1));
}
const FileRecord* DiffTableModel::recordAt(int row) const {
if (row < 0 || row >= m_rows.size()) return nullptr;
return &m_rows[row];
}
void DiffTableModel::removeRecord(const QString& relPath) {
const QString prefix = relPath + '/';
for (int i = m_rows.size() - 1; i >= 0; --i) {
const QString& key = m_rows[i].relPath;
if (key != relPath && !key.startsWith(prefix)) continue;
beginRemoveRows({}, i, i);
m_checked.remove(key);
m_skipped.remove(key);
m_rows.removeAt(i);
endRemoveRows();
}
m_peer.remove(relPath);
for (auto it = m_peer.begin(); it != m_peer.end(); )
it = it.key().startsWith(prefix) ? m_peer.erase(it) : ++it;
}
int DiffTableModel::rowCount(const QModelIndex&) const { return m_rows.size(); }
int DiffTableModel::columnCount(const QModelIndex&) const { return ColumnCount; }
QVariant DiffTableModel::data(const QModelIndex& ix, int role) const {
if (!ix.isValid() || ix.row() >= m_rows.size()) return {};
const FileRecord& r = m_rows[ix.row()];
const bool inPeer = m_peer.contains(r.relPath);
if (role == Qt::CheckStateRole && m_checkable && ix.column() == Name)
return m_checked.contains(r.relPath) ? Qt::Checked : Qt::Unchecked;
if (role == Qt::ForegroundRole) {
if (m_skipped.contains(r.relPath)) return Palette::Skipped;
if (m_checkable && !inPeer) return Palette::NewOnUsb;
if (!m_checkable && !inPeer) return Palette::UsbOnly;
return {};
}
if (role != Qt::DisplayRole) return {};
switch (ix.column()) {
case Name: return displayName(r.relPath) + (r.isDir ? QStringLiteral("/") : QString());
case Size: return r.isDir ? QStringLiteral("--") : humanSize(r.size);
case Modified:
return QDateTime::fromMSecsSinceEpoch(r.mtimeNs / 1'000'000LL)
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
case TimeDiff:
if (inPeer)
return humanDuration(r.mtimeNs - m_peer.value(r.relPath).mtimeNs);
return m_checkable ? tr("new") : tr("no file");
}
return {};
}
QVariant DiffTableModel::headerData(int s, Qt::Orientation o, int role) const {
if (role != Qt::DisplayRole || o != Qt::Horizontal) return {};
switch (s) {
case Name: return QStringLiteral("File");
case Size: return QStringLiteral("Size");
case Modified: return QStringLiteral("Modified");
case TimeDiff: return QStringLiteral("Diff");
}
return {};
}
Qt::ItemFlags DiffTableModel::flags(const QModelIndex& ix) const {
Qt::ItemFlags f = Qt::ItemIsEnabled;
if (m_checkable && ix.column() == Name) f |= Qt::ItemIsUserCheckable;
return f;
}
bool DiffTableModel::setData(const QModelIndex& ix, const QVariant& v, int role) {
if (role != Qt::CheckStateRole || !m_checkable || ix.column() != Name)
return false;
const QString& key = m_rows[ix.row()].relPath;
if (v.toInt() == Qt::Checked) m_checked.insert(key);
else m_checked.remove(key);
emit dataChanged(ix, ix, {Qt::CheckStateRole});
return true;
}
}