Browse Source

Export group files through the clipboard and native file drags

master
evgeny 3 days ago
parent
commit
e8ca27b590
  1. 111
      tools/chatgui/src/member_files/browser.cpp
  2. 8
      tools/chatgui/src/member_files/browser.h

111
tools/chatgui/src/member_files/browser.cpp

@ -26,6 +26,14 @@
#include <QLocale>
#include <QDropEvent>
#include <QMimeData>
#include <QDrag>
#include <QClipboard>
#include <QGuiApplication>
#include <QAction>
#include <QDir>
#include <QTemporaryDir>
#include <QStandardPaths>
#include <functional>
#include <limits>
#include <cstring>
extern "C" {
@ -36,6 +44,15 @@ extern "C" {
}
static QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
// Внешний drag выдаёт только готовый локальный файл; обычный MIME дерева не подходит системному менеджеру.
class MemberFileTree : public QTreeWidget {
public:
using QTreeWidget::QTreeWidget;
std::function<void()> exportRequested;
protected:
void startDrag(Qt::DropActions) override { exportRequested(); }
};
static QString quotaSize(quint64 bytes) {
return bytes <= (quint64)std::numeric_limits<qint64>::max() ? QLocale().formattedDataSize((qint64)bytes, 2) :
text("%1 bytes").arg(QLocale().toString(bytes));
@ -64,7 +81,8 @@ MemberFileBrowser::MemberFileBrowser(DbManager* db, QWidget* parent) : QDialog(p
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"));
navigation->addWidget(up); navigation->addWidget(m_location, 1); navigation->addWidget(reload); layout->addWidget(m_navigation);
m_entries = new QTreeWidget(this); m_entries->setRootIsDecorated(false);
auto* tree = new MemberFileTree(this); m_entries = tree; m_entries->setRootIsDecorated(false);
tree->exportRequested = [this] { exportFile(false); }; m_entries->setDragEnabled(true);
m_entries->setHeaderLabels({text("Name"), text("Type"), text("Size"), text("Modified")});
m_entries->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_entries->setSelectionMode(QAbstractItemView::SingleSelection); layout->addWidget(m_entries, 1);
@ -77,6 +95,10 @@ MemberFileBrowser::MemberFileBrowser(DbManager* db, QWidget* parent) : QDialog(p
m_uploadStatus = new QLabel(this); m_uploadStatus->setObjectName("memberFilesUploadStatus");
m_uploadStatus->setWordWrap(true); layout->addWidget(m_uploadStatus);
m_progress = new QProgressBar(this); m_progress->setRange(0, 1000); m_progress->hide(); layout->addWidget(m_progress);
m_copyAction = new QAction(text("Copy file"), this); m_copyAction->setObjectName("memberFilesCopy");
m_copyAction->setShortcut(QKeySequence::Copy); m_copyAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
m_entries->addAction(m_copyAction);
connect(m_copyAction, &QAction::triggered, this, [this] { exportFile(true); });
auto dropTargets = findChildren<QWidget*>(); dropTargets.append(this);
for (auto* target : dropTargets) { target->setAcceptDrops(true); target->installEventFilter(this); }
m_dropHint = new QLabel(m_entries->viewport()); m_dropHint->setObjectName("memberFilesDropHint");
@ -105,6 +127,7 @@ MemberFileBrowser::MemberFileBrowser(DbManager* db, QWidget* parent) : QDialog(p
auto* item = m_entries->itemAt(point); if (!item) return;
m_entries->setCurrentItem(item); QMenu menu(this);
auto* open = menu.addAction(text("Open")); auto* info = menu.addAction(text("Properties"));
menu.addAction(m_copyAction);
auto* selected = menu.exec(m_entries->viewport()->mapToGlobal(point));
if (selected == open) enter(item);
else if (selected == info && owner()) {
@ -128,7 +151,8 @@ bool MemberFileBrowser::eventFilter(QObject* target, QEvent* event) {
return QDialog::eventFilter(target, event);
auto* drop = static_cast<QDropEvent*>(event);
QString reason;
if (m_dropId || m_uploadBatchId || m_transferId) reason = text("Finish or cancel the current transfer first");
if (drop->source() == m_entries) { m_dropHint->hide(); drop->ignore(); return true; }
if (m_dropId || m_uploadBatchId || m_transferId || m_exportId) reason = text("Finish or cancel the current transfer first");
else if (!m_group || !server() || !m_db || !owner() || owner() != m_db->myNodeId() || !gui_bridge_is_uasync_ready())
reason = text("Upload unavailable");
else if (!drop->mimeData()->hasUrls() || drop->mimeData()->urls().isEmpty() || !(drop->possibleActions() & Qt::CopyAction))
@ -160,7 +184,7 @@ bool MemberFileBrowser::eventFilter(QObject* target, QEvent* event) {
}
void MemberFileBrowser::setChannel(const QString& channel, quint64 selectedOwner) {
if (m_dropId || m_uploadBatchId || m_transferId) {
if (m_dropId || m_uploadBatchId || m_transferId || m_exportId) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: context change blocked during transfer channel=%s", qPrintable(channel));
m_status->setText(text("Finish or cancel the current transfer first")); return;
}
@ -203,11 +227,12 @@ void MemberFileBrowser::updateActions() {
auto* item = m_entries->currentItem(); bool own = owner() && owner() == m_db->myNodeId();
bool available = server() && gui_bridge_is_uasync_ready();
bool file = item && item->data(0, Qt::UserRole + 1).toInt() == MF_FILE;
bool busy = m_dropId || m_uploadBatchId || m_transferId;
bool busy = m_dropId || m_uploadBatchId || m_transferId || m_exportId;
m_servers->setEnabled(!busy); m_owners->setEnabled(!busy); m_navigation->setEnabled(!busy); m_entries->setEnabled(!busy);
m_upload->setEnabled(available && own && !busy); m_mkdir->setEnabled(available && own && !busy);
m_rename->setEnabled(available && own && item && !busy); m_remove->setEnabled(available && own && item && !busy);
m_download->setEnabled(available && owner() && file && !busy); m_cancel->setEnabled(busy);
m_copyAction->setEnabled(available && owner() && file && !busy);
}
bool MemberFileBrowser::post(mf_message& message) {
@ -414,7 +439,7 @@ void MemberFileBrowser::upload() {
}
void MemberFileBrowser::uploadOwnFiles(const QString& channel, const QStringList& paths) {
if (m_dropId || m_uploadBatchId || m_transferId) {
if (m_dropId || m_uploadBatchId || m_transferId || m_exportId) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: drop rejected while busy files=%d", int(paths.size()));
QMessageBox::warning(this, text("Group files"), text("Finish or cancel the current transfer first")); return;
}
@ -429,10 +454,11 @@ void MemberFileBrowser::uploadOwnFiles(const QString& channel, const QStringList
// Сервер выбирается до первого запроса; batch ID защищает вложенные циклы диалогов от отмены/закрытия окна.
void MemberFileBrowser::beginUploads(const QStringList& paths, bool chooseServer) {
if (paths.isEmpty()) return;
if (m_dropId || m_uploadBatchId || m_transferId || !server() || !owner() || owner() != m_db->myNodeId() || !gui_bridge_is_uasync_ready()) {
if (m_dropId || m_uploadBatchId || m_transferId || m_exportId || !server() || !owner() ||
owner() != m_db->myNodeId() || !gui_bridge_is_uasync_ready()) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: upload unavailable group=%llu server=%llu owner=%llu busy=%d ready=%d",
(unsigned long long)m_group, (unsigned long long)server(), (unsigned long long)owner(),
bool(m_dropId || m_uploadBatchId || m_transferId), gui_bridge_is_uasync_ready());
bool(m_dropId || m_uploadBatchId || m_transferId || m_exportId), gui_bridge_is_uasync_ready());
QMessageBox::warning(this, text("Group files"), !server() ? text("No media server in this group") : text("Upload unavailable"));
return;
}
@ -510,6 +536,60 @@ void MemberFileBrowser::download(bool open) {
m_openAfter = open; startTransfer(selectedPath(), local, false, selectedVersion());
}
void MemberFileBrowser::publishExport(const QString& local, bool clipboard) {
auto* mime = new QMimeData; QUrl url = QUrl::fromLocalFile(local); mime->setUrls({url});
// Nautilus и KDE также получают явное действие copy, без удаления исходного файла.
mime->setData("x-special/gnome-copied-files", QByteArray("copy\n") + url.toEncoded());
mime->setData("application/x-kde-cutselection", "0");
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file browser: exporting local=%s clipboard=%d", qPrintable(local), clipboard);
if (clipboard) {
QGuiApplication::clipboard()->setMimeData(mime);
m_status->setText(text("File copied. Paste it in your file manager."));
} else {
auto* drag = new QDrag(m_entries); drag->setMimeData(mime); drag->exec(Qt::CopyAction, Qt::CopyAction);
}
}
// Кеш сохраняется после закрытия окна: URI в системном буфере должен оставаться пригодным для вставки.
void MemberFileBrowser::exportFile(bool clipboard) {
if (!m_copyAction->isEnabled()) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: export unavailable group=%llu owner=%llu busy=%d",
(unsigned long long)m_group, (unsigned long long)owner(), bool(m_dropId || m_uploadBatchId || m_transferId || m_exportId));
m_status->setText(text("Select a file and finish the current transfer first")); return;
}
QString remote = selectedPath(); QByteArray version = selectedVersion();
if (version.size() != 16 || !mf_path_valid(remote.toUtf8().constData(), 0)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: invalid export entry path=%s version_bytes=%d", qPrintable(remote), int(version.size()));
m_status->setText(text("Invalid directory response")); return;
}
QString key = QString("%1:%2:%3:%4:%5").arg(m_group).arg(server()).arg(owner()).arg(remote, QString::fromLatin1(version.toHex()));
if (m_exportCache.contains(key)) { publishExport(m_exportCache.value(key), clipboard); return; }
QString base = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
if (base.isEmpty()) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: no cache location for export");
m_status->setText(text("Cannot prepare a local copy")); return;
}
m_exportId = requestId(); m_exportKey = key; m_exportClipboard = clipboard; m_openAfter = false; updateActions();
m_status->setText(text("Downloading a local copy for your file manager…"));
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file browser: preparing export group=%llu server=%llu owner=%llu path=%s clipboard=%d",
(unsigned long long)m_group, (unsigned long long)server(), (unsigned long long)owner(), qPrintable(remote), clipboard);
quint64 id = m_exportId; QString name = QFileInfo(remote).fileName();
m_uploadPool.start(QRunnable::create([this, id, base, name, remote, version] {
QString root = base + "/file-exports", local;
if (QDir().mkpath(root)) {
QTemporaryDir directory(root + "/XXXXXX");
if (directory.isValid()) { local = directory.filePath(name); directory.setAutoRemove(false); }
}
QMetaObject::invokeMethod(this, [this, id, local, remote, version] {
if (m_exportId != id) return;
if (local.isEmpty() || !startTransfer(remote, local, false, version)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: export could not start remote=%s local=%s", qPrintable(remote), qPrintable(local));
m_exportId = 0; updateActions(); m_status->setText(text("Cannot prepare a local copy"));
}
}, Qt::QueuedConnection);
}));
}
bool MemberFileBrowser::startTransfer(const QString& remote, const QString& local, bool uploading, const QByteArray& version) {
auto* q = (mf_transfer_request*)u_calloc(1, sizeof(mf_transfer_request));
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: transfer allocation failed"); return false; }
@ -534,6 +614,19 @@ void MemberFileBrowser::onTransfer(const uint8_t* data, int length) {
m_status->setText(text("Transferred %1 / %2 bytes").arg(done).arg(total));
if (data[50] != 2) return;
m_transferId = 0; m_progress->hide(); updateActions();
if (m_exportId) {
m_exportId = 0; updateActions();
if (status) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: export download failed local=%s status=%s", qPrintable(m_local), mf_status_text(status));
m_status->setText(QString::fromUtf8(mf_status_text(status)));
} else {
m_exportCache.insert(m_exportKey, m_local);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file browser: local copy ready path=%s", qPrintable(m_local));
if (m_exportClipboard) publishExport(m_local, true);
else m_status->setText(text("Local copy ready. Drag the file again, or use Copy file."));
}
return;
}
if (m_uploadBatchId) {
if (status) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: upload failed local=%s status=%s", qPrintable(m_local), mf_status_text(status));
@ -579,6 +672,10 @@ void MemberFileBrowser::mutate(uint8_t command) {
}
void MemberFileBrowser::cancel() {
if (m_exportId) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file browser: export cancelled id=%llu", (unsigned long long)m_exportId);
m_exportId = 0; m_status->setText(text("Copy cancelled")); updateActions();
}
if (m_dropId) { m_dropId = 0; m_uploadStatus->setText(text("Upload queue cancelled")); updateActions(); }
if (m_uploadBatchId) finishUploads(text("Upload queue cancelled"));
cancelRequest(m_transferId);

8
tools/chatgui/src/member_files/browser.h

@ -16,6 +16,7 @@ class QTreeWidgetItem;
class QPushButton;
class QLabel;
class QProgressBar;
class QAction;
struct mf_message;
class MemberFileBrowser : public QDialog {
@ -46,6 +47,8 @@ private:
void nextUpload();
void finishUploads(const QString& status);
void download(bool open);
void exportFile(bool clipboard);
void publishExport(const QString& local, bool clipboard);
bool startTransfer(const QString& remote, const QString& local, bool upload, const QByteArray& version);
void mutate(uint8_t command);
void cancel();
@ -58,6 +61,10 @@ private:
bool m_openAfter = false;
quint64 m_uploadBatchId = 0, m_uploadStatId = 0;
quint64 m_dropId = 0;
quint64 m_exportId = 0;
bool m_exportClipboard = false;
QString m_exportKey;
QHash<QString, QString> m_exportCache;
QStringList m_uploadQueue;
QThreadPool m_uploadPool;
int m_uploadDone = 0, m_uploadSkipped = 0, m_uploadTotal = 0;
@ -74,4 +81,5 @@ private:
QLabel* m_dropHint;
QLabel* m_quota;
QProgressBar* m_progress;
QAction* m_copyAction;
};

Loading…
Cancel
Save