#include "models/DownloadTableModel.hpp" #include #include namespace velox::gui { namespace { QString humanBytes(qint64 n) { if (n < 0) { return QStringLiteral("—"); } return QLocale().formattedDataSize(n, 2, QLocale::DataSizeIecFormat); } QString humanRate(qint64 bps) { if (bps <= 0) { return QString(); } return DownloadTableModel::tr("%1/s").arg( QLocale().formattedDataSize(bps, 1, QLocale::DataSizeIecFormat)); } QString humanEta(qint64 secs) { if (secs < 0) { return QString(); } const qint64 h = secs / 3600; const qint64 m = (secs % 3600) / 60; const qint64 s = secs % 60; return QStringLiteral("%1:%2:%3") .arg(h, 2, 10, QLatin1Char('0')) .arg(m, 2, 10, QLatin1Char('0')) .arg(s, 2, 10, QLatin1Char('0')); } QString stateLabel(const QString &state) { static const QHash kLabels = { {QStringLiteral("new"), DownloadTableModel::tr("New")}, {QStringLiteral("probing"), DownloadTableModel::tr("Probing")}, {QStringLiteral("queued"), DownloadTableModel::tr("Queued")}, {QStringLiteral("connecting"), DownloadTableModel::tr("Connecting")}, {QStringLiteral("downloading"), DownloadTableModel::tr("Downloading")}, {QStringLiteral("paused"), DownloadTableModel::tr("Paused")}, {QStringLiteral("retry_wait"), DownloadTableModel::tr("Retry wait")}, {QStringLiteral("assembling"), DownloadTableModel::tr("Assembling")}, {QStringLiteral("verifying"), DownloadTableModel::tr("Verifying")}, {QStringLiteral("complete"), DownloadTableModel::tr("Complete")}, {QStringLiteral("failed"), DownloadTableModel::tr("Failed")}, {QStringLiteral("cancelled"), DownloadTableModel::tr("Cancelled")}, }; return kLabels.value(state, state); } qint64 optInt(const QJsonObject &o, const char *key, qint64 fallback) { const QJsonValue v = o.value(QLatin1String(key)); return (v.isDouble()) ? static_cast(v.toDouble()) : fallback; } } // namespace DownloadTableModel::DownloadTableModel(QObject *parent) : QAbstractTableModel(parent) {} int DownloadTableModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : static_cast(rows_.size()); } int DownloadTableModel::columnCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : ColumnCount; } double DownloadTableModel::progressOf(const Row &r) const { if (r.sizeBytes <= 0) { return -1.0; } return std::clamp(static_cast(r.downloadedBytes) / static_cast(r.sizeBytes), 0.0, 1.0); } QVariant DownloadTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= rows_.size()) { return {}; } const Row &r = rows_.at(index.row()); switch (role) { case TaskIdRole: return r.taskId; case StateRole: return r.state; case CategoryIdRole: return r.categoryId; case QueueIdRole: return r.queueId; case ProgressRole: return progressOf(r); case Qt::TextAlignmentRole: switch (index.column()) { case ColSize: case ColSpeed: case ColTimeLeft: case ColQueue: return static_cast(Qt::AlignRight | Qt::AlignVCenter); default: return static_cast(Qt::AlignLeft | Qt::AlignVCenter); } default: break; } if (role != Qt::DisplayRole && role != Qt::ToolTipRole) { return {}; } switch (index.column()) { case ColName: return r.filename; case ColQueue: return r.queuePosition >= 0 ? QString::number(r.queuePosition) : QString(); case ColSize: return humanBytes(r.sizeBytes); case ColStatus: if (r.state == QLatin1String("downloading")) { const double p = progressOf(r); return p < 0 ? stateLabel(r.state) : QStringLiteral("%1 %").arg(p * 100.0, 0, 'f', 1); } return stateLabel(r.state); case ColTimeLeft: return r.state == QLatin1String("downloading") ? humanEta(r.etaSeconds) : QString(); case ColSpeed: return r.state == QLatin1String("downloading") ? humanRate(r.speedBps) : QString(); case ColLastTry: return r.lastTryAt; case ColDescription: return r.description.isEmpty() ? r.url : r.description; default: return {}; } } QVariant DownloadTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || role != Qt::DisplayRole) { return QAbstractTableModel::headerData(section, orientation, role); } switch (section) { case ColName: return tr("File Name"); case ColQueue: return tr("Q"); case ColSize: return tr("Size"); case ColStatus: return tr("Status"); case ColTimeLeft: return tr("Time Left"); case ColSpeed: return tr("Transfer Rate"); case ColLastTry: return tr("Last Try Date"); case ColDescription: return tr("Description"); default: return {}; } } Qt::ItemFlags DownloadTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) { return Qt::NoItemFlags; } return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } DownloadTableModel::Row DownloadTableModel::rowFromSummary(const QJsonObject &s) { Row r; r.taskId = s.value("taskId").toString(); r.filename = s.value("filename").toString(); r.url = s.value("url").toString(); r.description = s.value("description").toString(); r.state = s.value("state").toString(); r.lastTryAt = s.value("lastTryAt").toString(); r.categoryId = s.value("categoryId").toString(); r.queueId = s.value("queueId").toString(); r.sizeBytes = optInt(s, "sizeBytes", -1); r.downloadedBytes = optInt(s, "downloadedBytes", 0); r.speedBps = optInt(s, "speedBps", 0); r.etaSeconds = optInt(s, "etaSeconds", -1); r.queuePosition = optInt(s, "queuePosition", -1); r.resumable = s.value("resumable").toBool(); return r; } void DownloadTableModel::rebuildIndex() { indexByTaskId_.clear(); indexByTaskId_.reserve(rows_.size()); for (int i = 0; i < rows_.size(); ++i) { indexByTaskId_.insert(rows_.at(i).taskId, i); } } void DownloadTableModel::resetFromJson(const QJsonArray &items) { beginResetModel(); rows_.clear(); rows_.reserve(items.size()); for (const QJsonValue &v : items) { rows_.push_back(rowFromSummary(v.toObject())); } rebuildIndex(); endResetModel(); } void DownloadTableModel::applyProgress(const QJsonArray &tasks) { for (const QJsonValue &v : tasks) { const QJsonObject t = v.toObject(); const auto it = indexByTaskId_.constFind(t.value("taskId").toString()); if (it == indexByTaskId_.cend()) { continue; // progress for a row we are not showing — ignore, do not synthesize } const int row = it.value(); Row &r = rows_[row]; r.downloadedBytes = optInt(t, "downloadedBytes", r.downloadedBytes); r.speedBps = optInt(t, "speedBps", r.speedBps); r.etaSeconds = optInt(t, "etaSeconds", -1); if (r.state != QLatin1String("downloading")) { r.state = QStringLiteral("downloading"); } // Narrow patch: only the value columns move on a tick. emit dataChanged(index(row, ColSize), index(row, ColSpeed), {Qt::DisplayRole, ProgressRole}); } } void DownloadTableModel::applyTaskState(const QJsonObject ¶ms) { const QString taskId = params.value("taskId").toString(); const QJsonObject summary = params.value("summary").toObject(); const auto it = indexByTaskId_.constFind(taskId); if (it == indexByTaskId_.cend()) { if (!summary.isEmpty()) { applyTaskAdded(summary); } return; } const int row = it.value(); if (!summary.isEmpty()) { rows_[row] = rowFromSummary(summary); } else { rows_[row].state = params.value("state").toString(); } emit dataChanged(index(row, 0), index(row, ColumnCount - 1)); } void DownloadTableModel::applyTaskAdded(const QJsonObject &summary) { const QString taskId = summary.value("taskId").toString(); if (taskId.isEmpty()) { return; } const auto it = indexByTaskId_.constFind(taskId); if (it != indexByTaskId_.cend()) { const int row = it.value(); rows_[row] = rowFromSummary(summary); emit dataChanged(index(row, 0), index(row, ColumnCount - 1)); return; } const int row = static_cast(rows_.size()); beginInsertRows({}, row, row); rows_.push_back(rowFromSummary(summary)); indexByTaskId_.insert(taskId, row); endInsertRows(); } void DownloadTableModel::applyTaskRemoved(const QString &taskId) { const auto it = indexByTaskId_.constFind(taskId); if (it == indexByTaskId_.cend()) { return; } const int row = it.value(); beginRemoveRows({}, row, row); rows_.remove(row); endRemoveRows(); rebuildIndex(); } } // namespace velox::gui