diff --git a/tools/chatgui/src/member_files/browser.cpp b/tools/chatgui/src/member_files/browser.cpp index a84256e6..73583131 100644 --- a/tools/chatgui/src/member_files/browser.cpp +++ b/tools/chatgui/src/member_files/browser.cpp @@ -20,6 +20,9 @@ #include #include #include +#include +#include +#include #include extern "C" { #include "member_files/member_files.h" @@ -29,6 +32,10 @@ extern "C" { } static QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); } +static QString quotaSize(quint64 bytes) { + return bytes <= (quint64)std::numeric_limits::max() ? QLocale().formattedDataSize((qint64)bytes, 2) : + text("%1 bytes").arg(QLocale().toString(bytes)); +} static quint64 requestId() { quint64 id; do { id = QRandomGenerator::global()->generate64(); } while (!id); return id; } static bool copyPath(char* out, const QString& value) { QByteArray bytes = value.toUtf8(); @@ -43,6 +50,10 @@ MemberFileBrowser::MemberFileBrowser(DbManager* db, QWidget* parent) : QDialog(p m_servers = new QComboBox(this); m_owners = new QComboBox(this); selectors->addWidget(new QLabel(text("Media server:"), this)); selectors->addWidget(m_servers, 1); selectors->addWidget(new QLabel(text("Member:"), this)); selectors->addWidget(m_owners, 1); layout->addLayout(selectors); + m_quota = new QLabel(text("Select a member to see storage quota"), this); + m_quota->setObjectName("memberFilesQuota"); layout->addWidget(m_quota); + auto* quotaTimer = new QTimer(this); quotaTimer->setInterval(2000); + connect(quotaTimer, &QTimer::timeout, this, [this]() { if (isVisible()) requestQuota(); }); quotaTimer->start(); auto* navigation = new QHBoxLayout; auto* up = new QPushButton(text("Up"), this); auto* reload = new QPushButton(text("Refresh"), this); m_location = new QLineEdit(this); m_location->setPlaceholderText(text("Path inside the member directory")); @@ -91,6 +102,7 @@ MemberFileBrowser::MemberFileBrowser(DbManager* db, QWidget* parent) : QDialog(p MemberFileBrowser::~MemberFileBrowser() { cancel(); cancelRequest(m_listId); + cancelRequest(m_quotaId); for (auto id : m_actions.keys()) cancelRequest(id); } @@ -162,11 +174,25 @@ void MemberFileBrowser::cancelRequest(quint64 id) { q->inst = gui_bridge_get_inst(); q->id = id; gui_bridge_post_uasync_fn(member_files_gui_cancel, q); } +void MemberFileBrowser::requestQuota() { + if (m_quotaId || !owner() || !server() || !gui_bridge_is_uasync_ready()) return; + mf_message q{}; q.command = MF_QUOTA; q.owner = owner(); + if (post(q)) m_quotaId = q.request_id; + else { + m_quota->setText(text("Quota unavailable")); + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: GUI quota request failed owner=%llu", (unsigned long long)owner()); + } +} + void MemberFileBrowser::refresh() { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: GUI refresh old_id=%llu group=%llx server=%llx owner=%llx path=%s", (unsigned long long)m_listId, (unsigned long long)m_group, (unsigned long long)server(), (unsigned long long)owner(), qPrintable(m_path)); cancelRequest(m_listId); m_listId = 0; m_entries->clear(); m_location->setText(m_path); updateActions(); + cancelRequest(m_quotaId); m_quotaId = 0; m_refreshGeneration++; + m_quota->setToolTip(QString()); + m_quota->setText(owner() ? text("Loading quota…") : text("Select a member to see storage quota")); if (!server()) { m_status->setText(text("No media server in this group")); return; } + requestQuota(); mf_message q{}; q.command = MF_LIST; q.owner = owner(); if (!copyPath(q.path, m_path) || !post(q)) { m_status->setText(text("Unable to request directory")); return; } m_listId = q.request_id; m_entries->setSortingEnabled(false); m_status->setText(text("Loading directory…")); @@ -200,9 +226,41 @@ void MemberFileBrowser::onResponse(const uint8_t* data, int length) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: GUI response id=%llu list_id=%llu cmd=%s status=%s bytes=%u owner=%llx/%llx path=%s", (unsigned long long)r.request_id, (unsigned long long)m_listId, mf_command_text(r.command), mf_status_text(r.status), r.data_len, (unsigned long long)r.owner, (unsigned long long)owner(), r.path); + if (r.command == MF_QUOTA) { + if (r.request_id != m_quotaId || r.owner != owner()) { + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: GUI stale quota response id=%llu", (unsigned long long)r.request_id); + return; + } + m_quotaId = 0; + if (r.status) { + m_quota->setText(r.status == MF_BUSY ? text("Loading quota…") : text("Quota unavailable")); + m_quota->setToolTip(QString::fromUtf8(mf_status_text(r.status))); + if (r.status != MF_BUSY) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: GUI quota failed status=%s", mf_status_text(r.status)); + return; + } + if (r.data_len != MF_QUOTA_BYTES || !mf_get64(r.data + 8)) { + m_quota->setText(text("Quota unavailable")); + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: GUI malformed quota response bytes=%u", r.data_len); + return; + } + quint64 used = mf_get64(r.data), limit = mf_get64(r.data + 8), reserved = mf_get64(r.data + 16); + QString label = text("Storage: %1 used / %2 total").arg(quotaSize(used), quotaSize(limit)); + if (reserved) label += text(" · Reserved for uploads: %1").arg(quotaSize(reserved)); + m_quota->setText(label); + m_quota->setToolTip(text("Used: %1 bytes\nLimit: %2 bytes\nReserved: %3 bytes").arg(used).arg(limit).arg(reserved)); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: GUI quota owner=%llu used=%llu limit=%llu reserved=%llu", + (unsigned long long)owner(), (unsigned long long)used, (unsigned long long)limit, (unsigned long long)reserved); + return; + } if (r.request_id == m_listId && r.owner == owner() && QString::fromUtf8(r.path) == m_path) { if (r.status) { m_listId = 0; + if (r.status == MF_BUSY) { + quint64 generation = m_refreshGeneration; + m_status->setText(text("Loading directory…")); + QTimer::singleShot(1000, this, [this, generation]() { if (generation == m_refreshGeneration) refresh(); }); + return; + } if (r.status == MF_STALE && m_staleRetries++ < 1) { refresh(); return; } m_entries->clear(); m_status->setText(QString::fromUtf8(mf_status_text(r.status))); return; } @@ -300,6 +358,7 @@ void MemberFileBrowser::onTransfer(const uint8_t* data, int length) { if (m_group == m_transferGroup && server() == m_transferServer) refresh(); } m_openAfter = false; + if (!m_quotaId) requestQuota(); } void MemberFileBrowser::mutate(uint8_t command) { diff --git a/tools/chatgui/src/member_files/browser.h b/tools/chatgui/src/member_files/browser.h index f4e40309..fb87c0f5 100644 --- a/tools/chatgui/src/member_files/browser.h +++ b/tools/chatgui/src/member_files/browser.h @@ -26,6 +26,7 @@ public: void onTransfer(const uint8_t* data, int length); private: void refresh(); + void requestQuota(); void navigate(const QString& path); void enter(QTreeWidgetItem* item); void updateActions(); @@ -42,6 +43,7 @@ private: void cancelRequest(quint64 id); DbManager* m_db; quint64 m_group = 0, m_listId = 0, m_transferId = 0; + quint64 m_quotaId = 0, m_refreshGeneration = 0; quint64 m_transferGroup = 0, m_transferServer = 0; QString m_path, m_local; bool m_openAfter = false; @@ -53,5 +55,6 @@ private: QTreeWidget* m_entries; QPushButton *m_upload, *m_download, *m_mkdir, *m_rename, *m_remove, *m_cancel; QLabel* m_status; + QLabel* m_quota; QProgressBar* m_progress; };