Improve file browser

This commit is contained in:
Martchus 2024-05-04 22:37:31 +02:00
parent 93c9ffe5ca
commit 83e1dd6d8a
5 changed files with 114 additions and 17 deletions

View File

@ -49,7 +49,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingLogEntry {
QString message;
};
enum class SyncthingItemType { Unknown, File, Directory };
enum class SyncthingItemType { Unknown, File, Directory, Symlink };
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
QString name;
@ -58,6 +58,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
SyncthingItemType type = SyncthingItemType::Unknown;
std::vector<std::unique_ptr<SyncthingItem>> children;
SyncthingItem *parent = nullptr; // not populated but might be set as needed (take care in case the pointer gets invalidated)
QString path; // not populated but might be set as needed
std::size_t index = std::size_t();
int level = 0; // the level of nesting, does *not* include levels of the prefix
bool childrenPopulated = false; // populated depending on requested level

View File

@ -1635,6 +1635,8 @@ static void readSyncthingItems(const QJsonArray &array, std::vector<std::unique_
item->type = SyncthingItemType::File;
} else if (type == QLatin1String("FILE_INFO_TYPE_DIRECTORY")) {
item->type = SyncthingItemType::Directory;
} else if (type == QLatin1String("FILE_INFO_TYPE_SYMLINK")) {
item->type = SyncthingItemType::Symlink;
}
readSyncthingItems(children.toArray(), item->children, level + 1, levels);
item->childrenPopulated = !levels || level < levels;

View File

@ -4,8 +4,12 @@
#include <syncthingconnector/syncthingconnection.h>
#include <syncthingconnector/utils.h>
#include <qtutilities/misc/desktoputils.h>
#include <c++utilities/conversion/stringconversion.h>
#include <QClipboard>
#include <QGuiApplication>
#include <QStringBuilder>
using namespace std;
@ -13,12 +17,30 @@ using namespace CppUtilities;
namespace Data {
/// \cond
static void populatePath(const QString &root, std::vector<std::unique_ptr<SyncthingItem>> &items)
{
if (root.isEmpty()) {
for (auto &item : items) {
populatePath(item->path = item->name, item->children);
}
} else {
for (auto &item : items) {
populatePath(item->path = root % QChar('/') % item->name, item->children);
}
}
}
/// \endcond
SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent)
: SyncthingModel(connection, parent)
, m_connection(connection)
, m_dirId(dir.id)
, m_root(std::make_unique<SyncthingItem>())
{
if (m_connection.isLocal()) {
m_localPath = dir.pathWithoutTrailingSlash().toString();
}
m_root->name = dir.displayName();
m_root->modificationTime = dir.lastFileTime;
m_root->size = dir.globalStats.bytes;
@ -33,6 +55,7 @@ SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const Sy
}
const auto last = items.size() - 1;
beginInsertRows(index(0, 0), 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
populatePath(QString(), items);
m_root->children = std::move(items);
m_root->childrenPopulated = true;
endInsertRows();
@ -50,6 +73,7 @@ QHash<int, QByteArray> SyncthingFileModel::roleNames() const
{ NameRole, "name" },
{ SizeRole, "size" },
{ ModificationTimeRole, "modificationTime" },
{ PathRole, "path" },
{ Actions, "actions" },
{ ActionNames, "actionNames" },
{ ActionIcons, "actionIcons" },
@ -175,9 +199,22 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
case 0:
return item->name;
case 1:
return QString::fromStdString(CppUtilities::dataSizeToString(item->size));
switch (item->type) {
case SyncthingItemType::File:
return QString::fromStdString(CppUtilities::dataSizeToString(item->size));
case SyncthingItemType::Directory:
return item->childrenPopulated ? tr("%1 elements").arg(item->children.size()) : QString();
default:
return QString();
}
case 2:
return QString::fromStdString(item->modificationTime.toString());
switch (item->type) {
case SyncthingItemType::File:
case SyncthingItemType::Directory:
return QString::fromStdString(item->modificationTime.toString());
default:
return QString();
}
}
break;
case Qt::DecorationRole: {
@ -189,33 +226,64 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
return icons.file;
case SyncthingItemType::Directory:
return icons.folder;
case SyncthingItemType::Symlink:
return icons.link;
default:
return icons.cogs;
}
}
break;
}
case Qt::ToolTipRole:
switch (index.column()) {
case 0:
return item->path;
case 2:
return agoString(item->modificationTime);
}
break;
case NameRole:
return item->name;
case SizeRole:
return static_cast<qsizetype>(item->size);
case ModificationTimeRole:
return QString::fromStdString(item->modificationTime.toString());
case Actions:
case PathRole:
return item->path;
case Actions: {
auto res = QStringList();
res.reserve(3);
if (item->type == SyncthingItemType::Directory) {
return QStringList({ QStringLiteral("refresh") });
res << QStringLiteral("refresh");
}
break;
case ActionNames:
if (!m_localPath.isEmpty()) {
res << QStringLiteral("open") << QStringLiteral("copy-path");
}
return res;
}
case ActionNames: {
auto res = QStringList();
res.reserve(3);
if (item->type == SyncthingItemType::Directory) {
return QStringList({ tr("Refresh") });
res << tr("Refresh");
}
break;
case ActionIcons:
if (!m_localPath.isEmpty()) {
res << (item->type == SyncthingItemType::Directory ? tr("Browse locally") : tr("Open local version")) << tr("Copy local path");
}
return res;
}
case ActionIcons: {
auto res = QVariantList();
res.reserve(3);
if (item->type == SyncthingItemType::Directory) {
return QStringList({ QStringLiteral("view-refresh") });
res << QIcon::fromTheme(QStringLiteral("view-refresh"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/view-refresh.svg")));
}
break;
if (!m_localPath.isEmpty()) {
res << QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg")));
res << QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/edit-copy.svg")));
}
return res;
}
}
return QVariant();
}
@ -281,6 +349,18 @@ void SyncthingFileModel::triggerAction(const QString &action, const QModelIndex
if (action == QLatin1String("refresh")) {
fetchMore(index);
}
if (m_localPath.isEmpty()) {
return;
}
const auto relPath = index.data(PathRole).toString();
const auto path = relPath.isEmpty() ? m_localPath : QString(m_localPath % QChar('/') % relPath);
if (action == QLatin1String("open")) {
QtUtilities::openLocalFileOrDir(path);
} else if (action == QLatin1String("copy-path")) {
if (auto *const clipboard = QGuiApplication::clipboard()) {
clipboard->setText(path);
}
}
}
void SyncthingFileModel::handleConfigInvalidated()
@ -313,7 +393,8 @@ void SyncthingFileModel::processFetchQueue()
return;
}
auto *const refreshedItem = reinterpret_cast<SyncthingItem *>(refreshedIndex.internalPointer());
if (!refreshedItem->children.empty()) {
const auto previousChildCount = refreshedItem->children.size();
if (previousChildCount) {
beginRemoveRows(refreshedIndex, 0, static_cast<int>(refreshedItem->children.size() - 1));
refreshedItem->children.clear();
endRemoveRows();
@ -324,11 +405,16 @@ void SyncthingFileModel::processFetchQueue()
for (auto &item : items) {
item->parent = refreshedItem;
}
populatePath(refreshedItem->path, items);
beginInsertRows(refreshedIndex, 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
refreshedItem->children = std::move(items);
refreshedItem->childrenPopulated = true;
endInsertRows();
}
if (refreshedItem->children.size() != previousChildCount) {
const auto sizeIndex = refreshedIndex.siblingAtColumn(1);
emit dataChanged(sizeIndex, sizeIndex, QList<int>{ Qt::DisplayRole });
}
processFetchQueue();
});
}

View File

@ -6,14 +6,21 @@
#include <syncthingconnector/syncthingconnection.h>
#include <memory>
#include <vector>
namespace Data {
class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel {
Q_OBJECT
public:
enum SyncthingFileModelRole { NameRole = SyncthingModelUserRole + 1, SizeRole, ModificationTimeRole, Actions, ActionNames, ActionIcons };
enum SyncthingFileModelRole {
NameRole = SyncthingModelUserRole + 1,
SizeRole,
ModificationTimeRole,
PathRole,
Actions,
ActionNames,
ActionIcons
};
explicit SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent = nullptr);
~SyncthingFileModel() override;
@ -46,6 +53,7 @@ private:
private:
SyncthingConnection &m_connection;
QString m_dirId;
QString m_localPath;
QStringList m_fetchQueue;
QMetaObject::Connection m_pendingRequest;
std::unique_ptr<SyncthingItem> m_root;

View File

@ -102,11 +102,11 @@ QDialog *browseRemoteFilesDialog(Data::SyncthingConnection &connection, const Da
return;
}
const auto actionNames = model->data(index, SyncthingFileModel::ActionNames).toStringList();
const auto actionIcons = model->data(index, SyncthingFileModel::ActionIcons).toStringList();
const auto actionIcons = model->data(index, SyncthingFileModel::ActionIcons).toList();
auto menu = QMenu(view);
auto actionIndex = qsizetype();
for (const auto &action : actions) {
QObject::connect(menu.addAction(actionIndex < actionIcons.size() ? QIcon::fromTheme(actionIcons.at(actionIndex)) : QIcon(),
QObject::connect(menu.addAction(actionIndex < actionIcons.size() ? actionIcons.at(actionIndex).value<QIcon>() : QIcon(),
actionIndex < actionNames.size() ? actionNames.at(actionIndex) : action),
&QAction::triggered, model, [model, action, index]() { model->triggerAction(action, index); });
++actionIndex;