Allow browsing remote/global files
This commit is contained in:
parent
1b9450fc62
commit
93b5d66875
|
@ -19,7 +19,6 @@
|
|||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
using namespace Data;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
K_PLUGIN_CLASS_WITH_JSON(SyncthingFileItemAction, "metadata.json");
|
||||
|
@ -28,13 +27,13 @@ K_PLUGIN_FACTORY(SyncthingFileItemActionFactory, registerPlugin<SyncthingFileIte
|
|||
#endif
|
||||
|
||||
struct SyncthingItem {
|
||||
SyncthingItem(const SyncthingDir *dir, const QString &path);
|
||||
const SyncthingDir *dir;
|
||||
SyncthingItem(const Data::SyncthingDir *dir, const QString &path);
|
||||
const Data::SyncthingDir *dir;
|
||||
QString path;
|
||||
QString name;
|
||||
};
|
||||
|
||||
SyncthingItem::SyncthingItem(const SyncthingDir *dir, const QString &path)
|
||||
SyncthingItem::SyncthingItem(const Data::SyncthingDir *dir, const QString &path)
|
||||
: dir(dir)
|
||||
, path(path)
|
||||
{
|
||||
|
@ -78,17 +77,17 @@ QList<QAction *> SyncthingFileItemAction::actions(const KFileItemListProperties
|
|||
}
|
||||
|
||||
struct DirStats {
|
||||
explicit DirStats(const QList<const SyncthingDir *> &dirs);
|
||||
explicit DirStats(const QList<const Data::SyncthingDir *> &dirs);
|
||||
|
||||
QStringList ids;
|
||||
bool anyPaused = false;
|
||||
bool allPaused = true;
|
||||
};
|
||||
|
||||
DirStats::DirStats(const QList<const SyncthingDir *> &dirs)
|
||||
DirStats::DirStats(const QList<const Data::SyncthingDir *> &dirs)
|
||||
{
|
||||
ids.reserve(dirs.size());
|
||||
for (const SyncthingDir *const dir : dirs) {
|
||||
for (const Data::SyncthingDir *const dir : dirs) {
|
||||
ids << dir->id;
|
||||
if (dir->paused) {
|
||||
anyPaused = true;
|
||||
|
@ -124,11 +123,11 @@ QList<QAction *> SyncthingFileItemAction::createActions(const KFileItemListPrope
|
|||
}
|
||||
|
||||
// determine relevant Syncthing dirs
|
||||
QList<const SyncthingDir *> detectedDirs;
|
||||
QList<const SyncthingDir *> containingDirs;
|
||||
QList<const Data::SyncthingDir *> detectedDirs;
|
||||
QList<const Data::SyncthingDir *> containingDirs;
|
||||
QList<SyncthingItem> detectedItems;
|
||||
const SyncthingDir *lastDir = nullptr;
|
||||
for (const SyncthingDir &dir : dirs) {
|
||||
const Data::SyncthingDir *lastDir = nullptr;
|
||||
for (const Data::SyncthingDir &dir : dirs) {
|
||||
auto dirPath = QDir::cleanPath(dir.path);
|
||||
auto dirPathWithSlash = dirPath + QChar('/');
|
||||
for (const QString &path : std::as_const(paths)) {
|
||||
|
@ -177,7 +176,7 @@ QList<QAction *> SyncthingFileItemAction::createActions(const KFileItemListPrope
|
|||
actions << new QAction(QIcon::fromTheme(QStringLiteral("folder-sync")),
|
||||
detectedDirs.size() == 1 ? tr("Rescan \"%1\"").arg(detectedDirs.front()->displayName()) : tr("Rescan selected folders"), parent);
|
||||
if (connection.isConnected() && !detectedDirsStats.allPaused) {
|
||||
for (const SyncthingDir *dir : std::as_const(detectedDirs)) {
|
||||
for (const Data::SyncthingDir *dir : std::as_const(detectedDirs)) {
|
||||
connect(actions.back(), &QAction::triggered, bind(&SyncthingFileItemActionStaticData::rescanDir, &data, dir->id, QString()));
|
||||
containingDirs.removeAll(dir);
|
||||
}
|
||||
|
@ -195,8 +194,8 @@ QList<QAction *> SyncthingFileItemAction::createActions(const KFileItemListPrope
|
|||
}
|
||||
if (connection.isConnected()) {
|
||||
connect(actions.back(), &QAction::triggered,
|
||||
bind(detectedDirsStats.anyPaused ? &SyncthingConnection::resumeDirectories : &SyncthingConnection::pauseDirectories, &connection,
|
||||
detectedDirsStats.ids));
|
||||
bind(detectedDirsStats.anyPaused ? &Data::SyncthingConnection::resumeDirectories : &Data::SyncthingConnection::pauseDirectories,
|
||||
&connection, detectedDirsStats.ids));
|
||||
} else {
|
||||
actions.back()->setEnabled(false);
|
||||
}
|
||||
|
@ -208,7 +207,7 @@ QList<QAction *> SyncthingFileItemAction::createActions(const KFileItemListPrope
|
|||
actions << new QAction(QIcon::fromTheme(QStringLiteral("folder-sync")),
|
||||
containingDirs.size() == 1 ? tr("Rescan \"%1\"").arg(containingDirs.front()->displayName()) : tr("Rescan containing folders"), parent);
|
||||
if (connection.isConnected() && !containingDirsStats.allPaused) {
|
||||
for (const SyncthingDir *dir : std::as_const(containingDirs)) {
|
||||
for (const Data::SyncthingDir *dir : std::as_const(containingDirs)) {
|
||||
connect(actions.back(), &QAction::triggered, bind(&SyncthingFileItemActionStaticData::rescanDir, &data, dir->id, QString()));
|
||||
}
|
||||
} else {
|
||||
|
@ -226,8 +225,8 @@ QList<QAction *> SyncthingFileItemAction::createActions(const KFileItemListPrope
|
|||
}
|
||||
if (connection.isConnected()) {
|
||||
connect(actions.back(), &QAction::triggered,
|
||||
bind(containingDirsStats.anyPaused ? &SyncthingConnection::resumeDirectories : &SyncthingConnection::pauseDirectories, &connection,
|
||||
containingDirsStats.ids));
|
||||
bind(containingDirsStats.anyPaused ? &Data::SyncthingConnection::resumeDirectories : &Data::SyncthingConnection::pauseDirectories,
|
||||
&connection, containingDirsStats.ids));
|
||||
} else {
|
||||
actions.back()->setEnabled(false);
|
||||
}
|
||||
|
@ -236,10 +235,10 @@ QList<QAction *> SyncthingFileItemAction::createActions(const KFileItemListPrope
|
|||
// add actions to show further information about directory if the selection is only about one particular Syncthing dir
|
||||
if (lastDir && detectedDirs.size() + containingDirs.size() == 1) {
|
||||
auto *statusActions = new SyncthingDirActions(*lastDir, &data, parent);
|
||||
connect(&connection, &SyncthingConnection::newDirs, statusActions,
|
||||
static_cast<void (SyncthingDirActions::*)(const vector<SyncthingDir> &)>(&SyncthingDirActions::updateStatus));
|
||||
connect(&connection, &SyncthingConnection::dirStatusChanged, statusActions,
|
||||
static_cast<bool (SyncthingDirActions::*)(const SyncthingDir &)>(&SyncthingDirActions::updateStatus));
|
||||
connect(&connection, &Data::SyncthingConnection::newDirs, statusActions,
|
||||
static_cast<void (SyncthingDirActions::*)(const std::vector<Data::SyncthingDir> &)>(&SyncthingDirActions::updateStatus));
|
||||
connect(&connection, &Data::SyncthingConnection::dirStatusChanged, statusActions,
|
||||
static_cast<bool (SyncthingDirActions::*)(const Data::SyncthingDir &)>(&SyncthingDirActions::updateStatus));
|
||||
actions << *statusActions;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,21 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingLogEntry {
|
|||
QString message;
|
||||
};
|
||||
|
||||
enum class SyncthingItemType { Unknown, File, Directory };
|
||||
|
||||
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
|
||||
QString name;
|
||||
CppUtilities::DateTime modificationTime;
|
||||
std::size_t size = std::size_t();
|
||||
SyncthingItemType type = SyncthingItemType::Unknown;
|
||||
std::vector<SyncthingItem> children;
|
||||
SyncthingItem *parent = nullptr; // not populated but might be set as needed (take care in case the pointer gets invalidated)
|
||||
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
|
||||
bool checked = false; // not populated but might be set to flag an item for some mass-action
|
||||
};
|
||||
|
||||
class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingConnection : public QObject {
|
||||
friend ConnectionTests;
|
||||
friend MiscTests;
|
||||
|
@ -240,6 +255,11 @@ public Q_SLOTS:
|
|||
void postConfigFromJsonObject(const QJsonObject &rawConfig);
|
||||
void postConfigFromByteArray(const QByteArray &rawConfig);
|
||||
|
||||
public:
|
||||
// methods to GET or POST information from/to Syncthing (non-slots)
|
||||
QMetaObject::Connection browse(
|
||||
const QString &dirId, const QString &prefix, int level, std::function<void(std::vector<SyncthingItem> &&)> &&callback);
|
||||
|
||||
Q_SIGNALS:
|
||||
void newConfig(const QJsonObject &rawConfig);
|
||||
void newDirs(const std::vector<SyncthingDir> &dirs);
|
||||
|
@ -352,6 +372,9 @@ private Q_SLOTS:
|
|||
void recalculateStatus();
|
||||
|
||||
private:
|
||||
// handler to evaluate results from request...() methods
|
||||
void readBrowse(const QString &dirId, int levels, std::function<void(std::vector<SyncthingItem> &&)> &&callback);
|
||||
|
||||
// internal helper methods
|
||||
struct Reply {
|
||||
QNetworkReply *reply;
|
||||
|
|
|
@ -1582,6 +1582,89 @@ void SyncthingConnection::readRevert()
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Lists items in the directory with the specified \a dirId down to \a levels (or fully if \a levels is 0) as of \a prefix.
|
||||
* \sa https://docs.syncthing.net/rest/db-browse-get.html
|
||||
* \remarks
|
||||
* In contrast to other functions, this one uses a \a callback to return results (instead of a signal). This makes it easier to
|
||||
* consume results of a specific request. Errors are still reported via the error() signal so there's no extra error handling
|
||||
* required. Note that \a callback is *not* invoked in the error case.
|
||||
*/
|
||||
QMetaObject::Connection SyncthingConnection::browse(
|
||||
const QString &dirId, const QString &prefix, int levels, std::function<void(std::vector<SyncthingItem> &&)> &&callback)
|
||||
{
|
||||
auto query = QUrlQuery();
|
||||
query.addQueryItem(QStringLiteral("folder"), formatQueryItem(dirId));
|
||||
if (!prefix.isEmpty()) {
|
||||
query.addQueryItem(QStringLiteral("prefix"), formatQueryItem(prefix));
|
||||
}
|
||||
if (levels > 0) {
|
||||
query.addQueryItem(QStringLiteral("levels"), QString::number(levels));
|
||||
}
|
||||
return QObject::connect(
|
||||
requestData(QStringLiteral("db/browse"), query), &QNetworkReply::finished, this,
|
||||
[this, id = dirId, l = levels, cb = std::move(callback)]() mutable { readBrowse(id, l, std::move(cb)); }, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
/// \cond
|
||||
static void readSyncthingItems(const QJsonArray &array, std::vector<SyncthingItem> &into, int level, int levels)
|
||||
{
|
||||
into.reserve(static_cast<std::size_t>(array.size()));
|
||||
for (const auto &jsonItem : array) {
|
||||
if (!jsonItem.isObject()) {
|
||||
continue;
|
||||
}
|
||||
const auto jsonItemObj = jsonItem.toObject();
|
||||
const auto type = jsonItemObj.value(QLatin1String("type")).toString();
|
||||
const auto index = into.size();
|
||||
const auto children = jsonItemObj.value(QLatin1String("children"));
|
||||
auto &item = into.emplace_back();
|
||||
item.name = jsonItemObj.value(QLatin1String("name")).toString();
|
||||
item.modificationTime = CppUtilities::DateTime::fromIsoStringGmt(jsonItemObj.value(QLatin1String("modTime")).toString().toUtf8().data());
|
||||
item.size = static_cast<std::size_t>(jsonItemObj.value(QLatin1String("size")).toInteger());
|
||||
item.index = index;
|
||||
item.level = level;
|
||||
if (type == QLatin1String("FILE_INFO_TYPE_FILE")) {
|
||||
item.type = SyncthingItemType::File;
|
||||
} else if (type == QLatin1String("FILE_INFO_TYPE_DIRECTORY")) {
|
||||
item.type = SyncthingItemType::Directory;
|
||||
}
|
||||
readSyncthingItems(children.toArray(), item.children, level + 1, levels);
|
||||
item.childrenPopulated = !levels || level < levels;
|
||||
}
|
||||
}
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \brief Reads the response of browse() and reports results via the specified \a callback or emits error() in case of an error.
|
||||
*/
|
||||
void SyncthingConnection::readBrowse(const QString &dirId, int levels, std::function<void(std::vector<SyncthingItem> &&)> &&callback)
|
||||
{
|
||||
auto const [reply, response] = prepareReply();
|
||||
if (!reply) {
|
||||
return;
|
||||
}
|
||||
auto items = std::vector<SyncthingItem>();
|
||||
switch (reply->error()) {
|
||||
case QNetworkReply::NoError: {
|
||||
auto jsonError = QJsonParseError();
|
||||
const auto replyDoc = QJsonDocument::fromJson(response, &jsonError);
|
||||
if (jsonError.error != QJsonParseError::NoError) {
|
||||
emit error(tr("Unable to parse response for browsing \"%1\": ").arg(dirId) + jsonError.errorString(), SyncthingErrorCategory::Parsing,
|
||||
QNetworkReply::NoError);
|
||||
return;
|
||||
}
|
||||
readSyncthingItems(replyDoc.array(), items, 0, levels);
|
||||
if (callback) {
|
||||
callback(std::move(items));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
emitError(tr("Unable to browse \"%1\": ").arg(dirId), SyncthingErrorCategory::SpecificRequest, reply);
|
||||
}
|
||||
}
|
||||
|
||||
// post config
|
||||
|
||||
/*!
|
||||
|
|
|
@ -14,6 +14,7 @@ set(HEADER_FILES
|
|||
syncthingdirectorymodel.h
|
||||
syncthingdevicemodel.h
|
||||
syncthingdownloadmodel.h
|
||||
syncthingfilemodel.h
|
||||
syncthingrecentchangesmodel.h
|
||||
syncthingsortfiltermodel.h
|
||||
syncthingstatuscomputionmodel.h
|
||||
|
@ -25,6 +26,7 @@ set(SRC_FILES
|
|||
syncthingdirectorymodel.cpp
|
||||
syncthingdevicemodel.cpp
|
||||
syncthingdownloadmodel.cpp
|
||||
syncthingfilemodel.cpp
|
||||
syncthingrecentchangesmodel.cpp
|
||||
syncthingsortfiltermodel.cpp
|
||||
syncthingstatuscomputionmodel.cpp
|
||||
|
|
|
@ -0,0 +1,280 @@
|
|||
#include "./syncthingfilemodel.h"
|
||||
#include "./syncthingicons.h"
|
||||
|
||||
#include <syncthingconnector/syncthingconnection.h>
|
||||
#include <syncthingconnector/utils.h>
|
||||
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <QStringBuilder>
|
||||
|
||||
using namespace std;
|
||||
using namespace CppUtilities;
|
||||
|
||||
namespace Data {
|
||||
|
||||
SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent)
|
||||
: SyncthingModel(connection, parent)
|
||||
, m_connection(connection)
|
||||
, m_dirId(dirId)
|
||||
{
|
||||
m_connection.browse(m_dirId, QString(), 1, [this](std::vector<SyncthingItem> &&items) {
|
||||
const auto last = items.size() - 1;
|
||||
beginInsertRows(QModelIndex(), 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
|
||||
m_items = std::move(items);
|
||||
endInsertRows();
|
||||
});
|
||||
}
|
||||
|
||||
SyncthingFileModel::~SyncthingFileModel()
|
||||
{
|
||||
QObject::disconnect(m_pendingRequest);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> SyncthingFileModel::roleNames() const
|
||||
{
|
||||
const static auto roles = QHash<int, QByteArray>{
|
||||
{ NameRole, "name" },
|
||||
{ SizeRole, "size" },
|
||||
{ ModificationTimeRole, "modificationTime" },
|
||||
{ Actions, "actions" },
|
||||
{ ActionNames, "actionNames" },
|
||||
{ ActionIcons, "actionIcons" },
|
||||
};
|
||||
return roles;
|
||||
}
|
||||
|
||||
QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (row < 0 || column < 0 || column > 2) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
if (!parent.isValid()) {
|
||||
if (static_cast<std::size_t>(row) >= m_items.size()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
return createIndex(row, column, &m_items[static_cast<std::size_t>(row)]);
|
||||
}
|
||||
|
||||
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
|
||||
auto &items = parentItem->children;
|
||||
if (static_cast<std::size_t>(row) >= items.size()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
auto &item = items[static_cast<std::size_t>(row)];
|
||||
item.parent = parentItem;
|
||||
return createIndex(row, column, &item);
|
||||
}
|
||||
|
||||
QString SyncthingFileModel::path(const QModelIndex &index) const
|
||||
{
|
||||
auto res = QString();
|
||||
if (!index.isValid()) {
|
||||
return res;
|
||||
}
|
||||
auto parts = QStringList();
|
||||
auto size = QString::size_type();
|
||||
parts.reserve(reinterpret_cast<SyncthingItem *>(index.internalPointer())->level + 1);
|
||||
for (auto i = index; i.isValid(); i = i.parent()) {
|
||||
size += parts.emplace_back(reinterpret_cast<SyncthingItem *>(i.internalPointer())->name).size();
|
||||
}
|
||||
res.reserve(size + parts.size());
|
||||
for (auto i = parts.rbegin(), end = parts.rend(); i != end; ++i) {
|
||||
res += *i;
|
||||
res += QChar('/');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
QModelIndex SyncthingFileModel::parent(const QModelIndex &child) const
|
||||
{
|
||||
if (!child.isValid()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
auto *const childItem = reinterpret_cast<SyncthingItem *>(child.internalPointer());
|
||||
if (!childItem->parent) {
|
||||
return QModelIndex();
|
||||
}
|
||||
return createIndex(static_cast<int>(childItem->index), 0, childItem->parent);
|
||||
}
|
||||
|
||||
QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
switch (orientation) {
|
||||
case Qt::Horizontal:
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
switch (section) {
|
||||
case 0:
|
||||
return tr("Name");
|
||||
case 1:
|
||||
return tr("Size");
|
||||
case 2:
|
||||
return tr("Last modified");
|
||||
}
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
auto *const item = reinterpret_cast<SyncthingItem *>(index.internalPointer());
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
switch (index.column()) {
|
||||
case 0:
|
||||
return item->name;
|
||||
case 1:
|
||||
return QString::fromStdString(CppUtilities::dataSizeToString(item->size));
|
||||
case 2:
|
||||
return QString::fromStdString(item->modificationTime.toString());
|
||||
}
|
||||
break;
|
||||
case Qt::DecorationRole: {
|
||||
const auto &icons = commonForkAwesomeIcons();
|
||||
switch (index.column()) {
|
||||
case 0:
|
||||
switch (item->type) {
|
||||
case SyncthingItemType::File:
|
||||
return icons.file;
|
||||
case SyncthingItemType::Directory:
|
||||
return icons.folder;
|
||||
default:
|
||||
return icons.cogs;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NameRole:
|
||||
return item->name;
|
||||
case SizeRole:
|
||||
return static_cast<qsizetype>(item->size);
|
||||
case ModificationTimeRole:
|
||||
return QString::fromStdString(item->modificationTime.toString());
|
||||
case Actions:
|
||||
if (item->type == SyncthingItemType::Directory) {
|
||||
return QStringList({ QStringLiteral("refresh") });
|
||||
}
|
||||
break;
|
||||
case ActionNames:
|
||||
if (item->type == SyncthingItemType::Directory) {
|
||||
return QStringList({ tr("Refresh") });
|
||||
}
|
||||
break;
|
||||
case ActionIcons:
|
||||
if (item->type == SyncthingItemType::Directory) {
|
||||
return QStringList({ QStringLiteral("view-refresh") });
|
||||
}
|
||||
break;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool SyncthingFileModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
Q_UNUSED(index)
|
||||
Q_UNUSED(value)
|
||||
Q_UNUSED(role)
|
||||
return false;
|
||||
}
|
||||
|
||||
int SyncthingFileModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
auto res = std::size_t();
|
||||
if (!parent.isValid()) {
|
||||
res = m_items.size();
|
||||
} else {
|
||||
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
|
||||
res = parentItem->childrenPopulated || parentItem->type != SyncthingItemType::Directory ? parentItem->children.size() : 1;
|
||||
}
|
||||
return res < std::numeric_limits<int>::max() ? static_cast<int>(res) : std::numeric_limits<int>::max();
|
||||
}
|
||||
|
||||
int SyncthingFileModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return 3;
|
||||
}
|
||||
|
||||
bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const
|
||||
{
|
||||
if (!parent.isValid()) {
|
||||
return false;
|
||||
}
|
||||
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
|
||||
return !parentItem->childrenPopulated && parentItem->type == SyncthingItemType::Directory;
|
||||
}
|
||||
|
||||
/// \cond
|
||||
static void addLevel(std::vector<SyncthingItem> &items, int level)
|
||||
{
|
||||
for (auto &item : items) {
|
||||
item.level += level;
|
||||
addLevel(item.children, level);
|
||||
}
|
||||
}
|
||||
/// \endcond
|
||||
|
||||
void SyncthingFileModel::fetchMore(const QModelIndex &parent)
|
||||
{
|
||||
if (!parent.isValid()) {
|
||||
return;
|
||||
}
|
||||
m_fetchQueue.append(parent);
|
||||
if (m_fetchQueue.size() == 1) {
|
||||
processFetchQueue();
|
||||
}
|
||||
}
|
||||
|
||||
void SyncthingFileModel::triggerAction(const QString &action, const QModelIndex &index)
|
||||
{
|
||||
if (action == QLatin1String("refresh")) {
|
||||
fetchMore(index);
|
||||
}
|
||||
}
|
||||
|
||||
void SyncthingFileModel::handleConfigInvalidated()
|
||||
{
|
||||
}
|
||||
|
||||
void SyncthingFileModel::handleNewConfigAvailable()
|
||||
{
|
||||
}
|
||||
|
||||
void SyncthingFileModel::handleForkAwesomeIconsChanged()
|
||||
{
|
||||
invalidateAllIndicies(QVector<int>({ Qt::DecorationRole }));
|
||||
}
|
||||
|
||||
void SyncthingFileModel::processFetchQueue()
|
||||
{
|
||||
if (m_fetchQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto &parent = m_fetchQueue.front();
|
||||
m_pendingRequest = m_connection.browse(m_dirId, path(parent), 1, [this, parent](std::vector<SyncthingItem> &&items) {
|
||||
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
|
||||
addLevel(items, parentItem->level);
|
||||
beginRemoveRows(parent, 0, static_cast<int>(parentItem->children.size() - 1));
|
||||
parentItem->children.clear();
|
||||
endRemoveRows();
|
||||
const auto last = items.size() - 1;
|
||||
beginInsertRows(parent, 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
|
||||
parentItem->children = std::move(items);
|
||||
parentItem->childrenPopulated = true;
|
||||
endInsertRows();
|
||||
m_fetchQueue.removeAll(parent);
|
||||
processFetchQueue();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Data
|
|
@ -0,0 +1,54 @@
|
|||
#ifndef DATA_SYNCTHINGFILEMODEL_H
|
||||
#define DATA_SYNCTHINGFILEMODEL_H
|
||||
|
||||
#include "./syncthingmodel.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct SyncthingItem;
|
||||
|
||||
class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum SyncthingFileModelRole { NameRole = SyncthingModelUserRole + 1, SizeRole, ModificationTimeRole, Actions, ActionNames, ActionIcons };
|
||||
|
||||
explicit SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent = nullptr);
|
||||
~SyncthingFileModel() override;
|
||||
|
||||
public Q_SLOTS:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &child) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
int columnCount(const QModelIndex &parent) const override;
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
void triggerAction(const QString &action, const QModelIndex &index);
|
||||
|
||||
public:
|
||||
QString path(const QModelIndex &path) const;
|
||||
|
||||
private Q_SLOTS:
|
||||
void handleConfigInvalidated() override;
|
||||
void handleNewConfigAvailable() override;
|
||||
void handleForkAwesomeIconsChanged() override;
|
||||
|
||||
private:
|
||||
void processFetchQueue();
|
||||
|
||||
private:
|
||||
SyncthingConnection &m_connection;
|
||||
QString m_dirId;
|
||||
QModelIndexList m_fetchQueue;
|
||||
QMetaObject::Connection m_pendingRequest;
|
||||
mutable std::vector<SyncthingItem> m_items;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
|
||||
#endif // DATA_SYNCTHINGFILEMODEL_H
|
|
@ -304,6 +304,7 @@ ForkAwesomeIcons::ForkAwesomeIcons(QtForkAwesome::Renderer &renderer, const QCol
|
|||
, cogs(renderer.pixmap(QtForkAwesome::Icon::Cogs, size, color))
|
||||
, link(renderer.pixmap(QtForkAwesome::Icon::Link, size, color))
|
||||
, eye(renderer.pixmap(QtForkAwesome::Icon::Eye, size, color))
|
||||
, file(renderer.pixmap(QtForkAwesome::Icon::FileO, size, color))
|
||||
, fileArchive(renderer.pixmap(QtForkAwesome::Icon::FileArchiveO, size, color))
|
||||
, folder(renderer.pixmap(QtForkAwesome::Icon::Folder, size, color))
|
||||
, certificate(renderer.pixmap(QtForkAwesome::Icon::Certificate, size, color))
|
||||
|
@ -366,7 +367,10 @@ QImage aboutDialogImage()
|
|||
void setForkAwesomeThemeOverrides()
|
||||
{
|
||||
auto &renderer = QtForkAwesome::Renderer::global();
|
||||
renderer.addThemeOverride(QtForkAwesome::Icon::File, QStringLiteral("text-plain"));
|
||||
renderer.addThemeOverride(QtForkAwesome::Icon::Folder, QStringLiteral("folder-symbolic"));
|
||||
renderer.addThemeOverride(QtForkAwesome::Icon::FileO, QStringLiteral("text-plain"));
|
||||
renderer.addThemeOverride(QtForkAwesome::Icon::FolderO, QStringLiteral("folder-symbolic"));
|
||||
renderer.addThemeOverride(QtForkAwesome::Icon::Sitemap, QStringLiteral("network-server-symbolic"));
|
||||
renderer.addThemeOverride(QtForkAwesome::Icon::Download, QStringLiteral("folder-download-symbolic"));
|
||||
renderer.addThemeOverride(QtForkAwesome::Icon::History, QStringLiteral("shallow-history"));
|
||||
|
|
|
@ -138,6 +138,7 @@ struct LIB_SYNCTHING_MODEL_EXPORT ForkAwesomeIcons {
|
|||
QIcon cogs;
|
||||
QIcon link;
|
||||
QIcon eye;
|
||||
QIcon file;
|
||||
QIcon fileArchive;
|
||||
QIcon folder;
|
||||
QIcon certificate;
|
||||
|
|
|
@ -40,6 +40,23 @@ void SyncthingModel::invalidateNestedIndicies(const QVector<int> &affectedRoles)
|
|||
}
|
||||
}
|
||||
|
||||
void SyncthingModel::invalidateAllIndicies(const QVector<int> &affectedRoles, const QModelIndex &parentIndex)
|
||||
{
|
||||
const auto rows = rowCount(parentIndex);
|
||||
const auto columns = columnCount(parentIndex);
|
||||
if (rows <= 0 || columns <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto topLeftIndex = index(0, 0, parentIndex);
|
||||
const auto bottomRightIndex = index(rows - 1, columns - 1, parentIndex);
|
||||
emit dataChanged(topLeftIndex, bottomRightIndex, affectedRoles);
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
if (const auto idx = index(row, 0, parentIndex); idx.isValid()) {
|
||||
invalidateAllIndicies(affectedRoles, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SyncthingModel::setBrightColors(bool brightColors)
|
||||
{
|
||||
if (m_brightColors == brightColors) {
|
||||
|
|
|
@ -34,6 +34,7 @@ protected:
|
|||
virtual const QVector<int> &colorRoles() const;
|
||||
void invalidateTopLevelIndicies(const QVector<int> &affectedRoles);
|
||||
void invalidateNestedIndicies(const QVector<int> &affectedRoles);
|
||||
void invalidateAllIndicies(const QVector<int> &affectedRoles, const QModelIndex &parentIndex = QModelIndex());
|
||||
|
||||
private Q_SLOTS:
|
||||
virtual void handleConfigInvalidated();
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include <syncthingconnector/syncthingconnection.h>
|
||||
#include <syncthingconnector/syncthingdir.h>
|
||||
|
||||
#include <syncthingmodel/syncthingfilemodel.h>
|
||||
|
||||
// use meta-data of syncthingtray application here
|
||||
#include "resources/../../tray/resources/config.h"
|
||||
|
||||
|
@ -12,8 +14,10 @@
|
|||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
using namespace std;
|
||||
|
@ -74,4 +78,51 @@ QWidget *ownDeviceIdWidget(Data::SyncthingConnection &connection, int size, QWid
|
|||
setupOwnDeviceIdDialog(connection, size, widget);
|
||||
return widget;
|
||||
}
|
||||
|
||||
QDialog *browseRemoteFilesDialog(Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent)
|
||||
{
|
||||
auto dlg = new QDialog(parent);
|
||||
dlg->setWindowTitle(QCoreApplication::translate("QtGui::OtherDialogs", "Remote/global tree of folder \"%1\"").arg(dir.displayName())
|
||||
+ QStringLiteral(" - " APP_NAME));
|
||||
dlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg")));
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
// setup model/view
|
||||
auto model = new Data::SyncthingFileModel(connection, dir.id, &connection);
|
||||
auto view = new QTreeView(dlg);
|
||||
view->setModel(model);
|
||||
view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
QObject::connect(view, &QTreeView::customContextMenuRequested, view, [view, model](const QPoint &pos) {
|
||||
const auto index = view->indexAt(pos);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
const auto actions = model->data(index, SyncthingFileModel::Actions).toStringList();
|
||||
if (actions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto actionNames = model->data(index, SyncthingFileModel::ActionNames).toStringList();
|
||||
const auto actionIcons = model->data(index, SyncthingFileModel::ActionIcons).toStringList();
|
||||
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(),
|
||||
actionIndex < actionNames.size() ? actionNames.at(actionIndex) : action),
|
||||
&QAction::triggered, model, [model, action, index]() { model->triggerAction(action, index); });
|
||||
++actionIndex;
|
||||
}
|
||||
menu.exec(pos);
|
||||
});
|
||||
|
||||
// setup layout
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->setAlignment(Qt::AlignCenter);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(QMargins());
|
||||
layout->addWidget(view);
|
||||
dlg->setLayout(layout);
|
||||
|
||||
return dlg;
|
||||
}
|
||||
|
||||
} // namespace QtGui
|
||||
|
|
|
@ -10,12 +10,15 @@ QT_FORWARD_DECLARE_CLASS(QWidget)
|
|||
|
||||
namespace Data {
|
||||
class SyncthingConnection;
|
||||
}
|
||||
struct SyncthingDir;
|
||||
} // namespace Data
|
||||
|
||||
namespace QtGui {
|
||||
|
||||
SYNCTHINGWIDGETS_EXPORT QDialog *ownDeviceIdDialog(Data::SyncthingConnection &connection);
|
||||
SYNCTHINGWIDGETS_EXPORT QWidget *ownDeviceIdWidget(Data::SyncthingConnection &connection, int size, QWidget *parent = nullptr);
|
||||
SYNCTHINGWIDGETS_EXPORT QDialog *browseRemoteFilesDialog(
|
||||
Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent = nullptr);
|
||||
} // namespace QtGui
|
||||
|
||||
#endif // SYNCTHINGWIDGETS_OTHERDIALOGS_H
|
||||
|
|
|
@ -51,6 +51,7 @@ set(REQUIRED_ICONS
|
|||
dialog-ok
|
||||
dialog-ok-apply
|
||||
document-open
|
||||
document-open-remote
|
||||
download
|
||||
edit-copy
|
||||
edit-clear
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <syncthingmodel/syncthingdirectorymodel.h>
|
||||
#include <syncthingmodel/syncthingsortfiltermodel.h>
|
||||
#include <syncthingwidgets/misc/direrrorsdialog.h>
|
||||
#include <syncthingwidgets/settings/settings.h>
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
@ -102,6 +103,12 @@ void DirView::showContextMenu(const QPoint &position)
|
|||
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg"))),
|
||||
tr("Open in file browser")),
|
||||
&QAction::triggered, triggerActionForSelectedRow(this, &DirView::openDir));
|
||||
if (Settings::values().enableWipFeatures) {
|
||||
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("document-open-remote"),
|
||||
QIcon(QStringLiteral(":/icons/hicolor/scalable/places/document-open-remote.svg"))),
|
||||
tr("Browse remote files")),
|
||||
&QAction::triggered, triggerActionForSelectedRow(this, &DirView::browseRemoteFiles));
|
||||
}
|
||||
}
|
||||
showViewMenu(position, *this, menu);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ Q_SIGNALS:
|
|||
void openDir(const Data::SyncthingDir &dir);
|
||||
void scanDir(const Data::SyncthingDir &dir);
|
||||
void pauseResumeDir(const Data::SyncthingDir &dir);
|
||||
void browseRemoteFiles(const Data::SyncthingDir &dir);
|
||||
|
||||
protected:
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
|
|
|
@ -203,6 +203,7 @@ TrayWidget::TrayWidget(TrayMenu *parent)
|
|||
connect(m_ui->dirsTreeView, &DirView::scanDir, this, &TrayWidget::scanDir);
|
||||
connect(m_ui->dirsTreeView, &DirView::pauseResumeDir, this, &TrayWidget::pauseResumeDir);
|
||||
connect(m_ui->devsTreeView, &DevView::pauseResumeDev, this, &TrayWidget::pauseResumeDev);
|
||||
connect(m_ui->dirsTreeView, &DirView::browseRemoteFiles, this, &TrayWidget::browseRemoteFiles);
|
||||
connect(m_ui->downloadsTreeView, &DownloadView::openDir, this, &TrayWidget::openDir);
|
||||
connect(m_ui->downloadsTreeView, &DownloadView::openItemDir, this, &TrayWidget::openItemDir);
|
||||
connect(m_ui->recentChangesTreeView, &QTreeView::customContextMenuRequested, this, &TrayWidget::showRecentChangesContextMenu);
|
||||
|
@ -708,6 +709,14 @@ void TrayWidget::pauseResumeDir(const SyncthingDir &dir)
|
|||
}
|
||||
}
|
||||
|
||||
void TrayWidget::browseRemoteFiles(const Data::SyncthingDir &dir)
|
||||
{
|
||||
auto *const dlg = browseRemoteFilesDialog(m_connection, dir, this);
|
||||
dlg->resize(600, 500);
|
||||
centerWidget(this);
|
||||
dlg->show();
|
||||
}
|
||||
|
||||
void TrayWidget::showRecentChangesContextMenu(const QPoint &position)
|
||||
{
|
||||
const auto *const selectionModel = m_ui->recentChangesTreeView->selectionModel();
|
||||
|
|
|
@ -95,6 +95,7 @@ private Q_SLOTS:
|
|||
void scanDir(const Data::SyncthingDir &dir);
|
||||
void pauseResumeDev(const Data::SyncthingDev &dev);
|
||||
void pauseResumeDir(const Data::SyncthingDir &dir);
|
||||
void browseRemoteFiles(const Data::SyncthingDir &dir);
|
||||
void showRecentChangesContextMenu(const QPoint &position);
|
||||
void changeStatus();
|
||||
void updateTraffic();
|
||||
|
|
Loading…
Reference in New Issue