|
|
|
|
@ -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); |
|
|
|
|
|