Compare commits

...

4 Commits

Author SHA1 Message Date
Martchus ffbff8ca96 WIP 2024-05-01 15:56:24 +02:00
Martchus 4f416eb6e2 WIP 2024-05-01 14:21:30 +02:00
Martchus 23425b6223 WIP 2024-05-01 14:21:30 +02:00
Martchus 18e30d8af0 WIP: Allow refreshing root in file browser 2024-05-01 14:21:30 +02:00
11 changed files with 286 additions and 79 deletions

View File

@ -53,10 +53,10 @@ enum class SyncthingItemType { Unknown, File, Directory };
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem { struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
QString name; QString name;
CppUtilities::DateTime modificationTime; CppUtilities::DateTime modificationTime = CppUtilities::DateTime();
std::size_t size = std::size_t(); std::size_t size = std::size_t();
SyncthingItemType type = SyncthingItemType::Unknown; SyncthingItemType type = SyncthingItemType::Unknown;
std::vector<SyncthingItem> children; 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) 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(); std::size_t index = std::size_t();
int level = 0; // the level of nesting, does *not* include levels of the prefix int level = 0; // the level of nesting, does *not* include levels of the prefix
@ -253,8 +253,8 @@ public Q_SLOTS:
public: public:
// methods to GET or POST information from/to Syncthing (non-slots) // methods to GET or POST information from/to Syncthing (non-slots)
QMetaObject::Connection browse( QMetaObject::Connection browse(const QString &dirId, const QString &prefix, int level,
const QString &dirId, const QString &prefix, int level, std::function<void(std::vector<SyncthingItem> &&)> &&callback); std::function<void(std::vector<std::unique_ptr<SyncthingItem>> &&, QString &&)> &&callback);
Q_SIGNALS: Q_SIGNALS:
void newConfig(const QJsonObject &rawConfig); void newConfig(const QJsonObject &rawConfig);
@ -369,7 +369,7 @@ private Q_SLOTS:
private: private:
// handler to evaluate results from request...() methods // handler to evaluate results from request...() methods
void readBrowse(const QString &dirId, int levels, std::function<void(std::vector<SyncthingItem> &&)> &&callback); void readBrowse(const QString &dirId, int levels, std::function<void(std::vector<std::unique_ptr<SyncthingItem>> &&, QString &&)> &&callback);
// internal helper methods // internal helper methods
struct Reply { struct Reply {

View File

@ -1590,8 +1590,8 @@ void SyncthingConnection::readRevert()
* consume results of a specific request. Errors are still reported via the error() signal so there's no extra error handling * 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. * required. Note that \a callback is *not* invoked in the error case.
*/ */
QMetaObject::Connection SyncthingConnection::browse( QMetaObject::Connection SyncthingConnection::browse(const QString &dirId, const QString &prefix, int levels,
const QString &dirId, const QString &prefix, int levels, std::function<void(std::vector<SyncthingItem> &&)> &&callback) std::function<void(std::vector<std::unique_ptr<SyncthingItem>> &&, QString &&)> &&callback)
{ {
auto query = QUrlQuery(); auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("folder"), formatQueryItem(dirId)); query.addQueryItem(QStringLiteral("folder"), formatQueryItem(dirId));
@ -1607,7 +1607,7 @@ QMetaObject::Connection SyncthingConnection::browse(
} }
/// \cond /// \cond
static void readSyncthingItems(const QJsonArray &array, std::vector<SyncthingItem> &into, int level, int levels) static void readSyncthingItems(const QJsonArray &array, std::vector<std::unique_ptr<SyncthingItem>> &into, int level, int levels)
{ {
into.reserve(static_cast<std::size_t>(array.size())); into.reserve(static_cast<std::size_t>(array.size()));
for (const auto &jsonItem : array) { for (const auto &jsonItem : array) {
@ -1618,57 +1618,66 @@ static void readSyncthingItems(const QJsonArray &array, std::vector<SyncthingIte
const auto type = jsonItemObj.value(QLatin1String("type")).toString(); const auto type = jsonItemObj.value(QLatin1String("type")).toString();
const auto index = into.size(); const auto index = into.size();
const auto children = jsonItemObj.value(QLatin1String("children")); const auto children = jsonItemObj.value(QLatin1String("children"));
auto &item = into.emplace_back(); auto &item = into.emplace_back(std::make_unique<SyncthingItem>());
item.name = jsonItemObj.value(QLatin1String("name")).toString(); item->name = jsonItemObj.value(QLatin1String("name")).toString();
item.modificationTime = CppUtilities::DateTime::fromIsoStringGmt(jsonItemObj.value(QLatin1String("modTime")).toString().toUtf8().data()); item->modificationTime = CppUtilities::DateTime::fromIsoStringGmt(jsonItemObj.value(QLatin1String("modTime")).toString().toUtf8().data());
item.size = static_cast<std::size_t>(jsonItemObj item->size = static_cast<std::size_t>(jsonItemObj
.value(QLatin1String("size")) .value(QLatin1String("size"))
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
.toInteger() .toInteger()
#else #else
.toDouble() .toDouble()
#endif #endif
); );
item.index = index; item->index = index;
item.level = level; item->level = level;
if (type == QLatin1String("FILE_INFO_TYPE_FILE")) { if (type == QLatin1String("FILE_INFO_TYPE_FILE")) {
item.type = SyncthingItemType::File; item->type = SyncthingItemType::File;
} else if (type == QLatin1String("FILE_INFO_TYPE_DIRECTORY")) { } else if (type == QLatin1String("FILE_INFO_TYPE_DIRECTORY")) {
item.type = SyncthingItemType::Directory; item->type = SyncthingItemType::Directory;
} }
readSyncthingItems(children.toArray(), item.children, level + 1, levels); readSyncthingItems(children.toArray(), item->children, level + 1, levels);
item.childrenPopulated = !levels || level < levels; item->childrenPopulated = !levels || level < levels;
} }
} }
/// \endcond /// \endcond
/*! /*!
* \brief Reads the response of browse() and reports results via the specified \a callback or emits error() in case of an error. * \brief Reads the response of browse() and reports results via the specified \a callback. Emits error() in case of an error.
* \remarks The \a callback is also emitted in the error case (with the error message as second parameter and an empty list of items).
*/ */
void SyncthingConnection::readBrowse(const QString &dirId, int levels, std::function<void(std::vector<SyncthingItem> &&)> &&callback) void SyncthingConnection::readBrowse(
const QString &dirId, int levels, std::function<void(std::vector<std::unique_ptr<SyncthingItem>> &&, QString &&)> &&callback)
{ {
auto const [reply, response] = prepareReply(); auto const [reply, response] = prepareReply();
if (!reply) { if (!reply) {
return; return;
} }
auto items = std::vector<SyncthingItem>(); auto items = std::vector<std::unique_ptr<SyncthingItem>>();
switch (reply->error()) { switch (reply->error()) {
case QNetworkReply::NoError: { case QNetworkReply::NoError: {
auto jsonError = QJsonParseError(); auto jsonError = QJsonParseError();
const auto replyDoc = QJsonDocument::fromJson(response, &jsonError); const auto replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) { if (jsonError.error != QJsonParseError::NoError) {
emit error(tr("Unable to parse response for browsing \"%1\": ").arg(dirId) + jsonError.errorString(), SyncthingErrorCategory::Parsing, auto errorMessage = tr("Unable to parse response for browsing \"%1\": ").arg(dirId) + jsonError.errorString();
QNetworkReply::NoError); emit error(errorMessage, SyncthingErrorCategory::Parsing, QNetworkReply::NoError);
if (callback) {
callback(std::move(items), std::move(errorMessage));
}
return; return;
} }
readSyncthingItems(replyDoc.array(), items, 0, levels); readSyncthingItems(replyDoc.array(), items, 0, levels);
if (callback) { if (callback) {
callback(std::move(items)); callback(std::move(items), QString());
} }
break; break;
} }
default: default:
emitError(tr("Unable to browse \"%1\": ").arg(dirId), SyncthingErrorCategory::SpecificRequest, reply); auto errorMessage = tr("Unable to browse \"%1\": ").arg(dirId);
emitError(errorMessage, SyncthingErrorCategory::SpecificRequest, reply);
if (callback) {
callback(std::move(items), std::move(errorMessage));
}
} }
} }

View File

@ -24,7 +24,7 @@ namespace Data {
*/ */
namespace TestData { namespace TestData {
static bool initialized = false; static bool initialized = false;
static std::string config, status, folderStats, deviceStats, errors, folderStatus, folderStatus2, folderStatus3, pullErrors, connections, version, empty; static std::string config, status, folderStats, deviceStats, errors, folderStatus, folderStatus2, folderStatus3, pullErrors, connections, version, empty, browse;
static std::string events[7]; static std::string events[7];
} // namespace TestData } // namespace TestData
@ -62,10 +62,10 @@ void setupTestData()
// read mock files for REST-API // read mock files for REST-API
const char *const fileNames[] = { "config", "status", "folderstats", "devicestats", "errors", "folderstatus-01", "folderstatus-02", const char *const fileNames[] = { "config", "status", "folderstats", "devicestats", "errors", "folderstatus-01", "folderstatus-02",
"folderstatus-03", "pullerrors-01", "connections", "version", "empty" }; "folderstatus-03", "pullerrors-01", "connections", "version", "empty", "browse" };
const char *const *fileName = fileNames; const char *const *fileName = fileNames;
for (auto *const testDataVariable : { &config, &status, &folderStats, &deviceStats, &errors, &folderStatus, &folderStatus2, &folderStatus3, for (auto *const testDataVariable : { &config, &status, &folderStats, &deviceStats, &errors, &folderStatus, &folderStatus2, &folderStatus3,
&pullErrors, &connections, &version, &empty }) { &pullErrors, &connections, &version, &empty, &browse }) {
*testDataVariable = readMockFile(testApp.testFilePath(argsToString("mocks/", *fileName, ".json"))); *testDataVariable = readMockFile(testApp.testFilePath(argsToString("mocks/", *fileName, ".json")));
++fileName; ++fileName;
} }
@ -171,6 +171,11 @@ MockedReply *MockedReply::forRequest(const QString &method, const QString &path,
} else if (folder == QLatin1String("forever-alone")) { } else if (folder == QLatin1String("forever-alone")) {
buffer = &folderStatus3; buffer = &folderStatus3;
} }
} else if (path == QLatin1String("db/browse") && !query.hasQueryItem(QStringLiteral("prefix"))) {
const auto folder = query.queryItemValue(QStringLiteral("folder"));
if (folder == QLatin1String("GXWxf-3zgnU")) {
buffer = &browse;
}
} else if (path == QLatin1String("folder/pullerrors")) { } else if (path == QLatin1String("folder/pullerrors")) {
const QString folder(query.queryItemValue(QStringLiteral("folder"))); const QString folder(query.queryItemValue(QStringLiteral("folder")));
if (folder == QLatin1String("GXWxf-3zgnU") && s_eventIndex >= 6) { if (folder == QLatin1String("GXWxf-3zgnU") && s_eventIndex >= 6) {

View File

@ -0,0 +1,46 @@
[
{
"modTime" : "2020-10-02T23:48:52.076996974+02:00",
"name" : "100ANDRO",
"size" : 128,
"type" : "FILE_INFO_TYPE_DIRECTORY"
},
{
"modTime" : "2020-10-09T13:04:42.4410738+02:00",
"name" : "Camera",
"size" : 128,
"type" : "FILE_INFO_TYPE_DIRECTORY",
"children" : [
{
"modTime" : "2020-12-16T23:31:34.5009668+01:00",
"name" : "IMG_20201114_124821.jpg",
"size" : 10682189,
"type" : "FILE_INFO_TYPE_FILE"
},
{
"modTime" : "2020-12-16T23:31:35.0106367+01:00",
"name" : "IMG_20201213_122451.jpg",
"size" : 7936351,
"type" : "FILE_INFO_TYPE_FILE"
},
{
"modTime" : "2020-12-13T12:25:05.017097469+01:00",
"name" : "IMG_20201213_122504.jpg",
"size" : 8406507,
"type" : "FILE_INFO_TYPE_FILE"
},
{
"modTime" : "2020-12-13T12:25:06.127097469+01:00",
"name" : "IMG_20201213_122505.jpg",
"size" : 8381931,
"type" : "FILE_INFO_TYPE_FILE"
},
{
"modTime" : "2020-12-13T12:53:29.707298401+01:00",
"name" : "IMG_20201213_125329.jpg",
"size" : 4388331,
"type" : "FILE_INFO_TYPE_FILE"
}
]
}
]

View File

@ -39,7 +39,8 @@ set(TS_FILES translations/${META_PROJECT_NAME}_zh_CN.ts translations/${META_PROJ
translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts) translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts)
set(QT_TESTS models) set(QT_TESTS models)
set(QT_TEST_SRC_FILES_models syncthingicons.cpp syncthingmodel.cpp syncthingdirectorymodel.cpp syncthingdevicemodel.cpp) set(QT_TEST_SRC_FILES_models syncthingicons.cpp syncthingmodel.cpp syncthingdirectorymodel.cpp syncthingdevicemodel.cpp
syncthingfilemodel.cpp)
# find c++utilities # find c++utilities
find_package(${PACKAGE_NAMESPACE_PREFIX}c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED) find_package(${PACKAGE_NAMESPACE_PREFIX}c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED)

View File

@ -13,15 +13,28 @@ using namespace CppUtilities;
namespace Data { namespace Data {
SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent) SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent)
: SyncthingModel(connection, parent) : SyncthingModel(connection, parent)
, m_connection(connection) , m_connection(connection)
, m_dirId(dirId) , m_dirId(dir.id)
, m_root(std::make_unique<SyncthingItem>())
{ {
m_connection.browse(m_dirId, QString(), 1, [this](std::vector<SyncthingItem> &&items) { m_root->name = dir.displayName();
m_root->modificationTime = dir.lastFileTime;
m_root->size = dir.globalStats.bytes;
m_root->type = SyncthingItemType::Directory;
m_fetchQueue.append(QString());
m_connection.browse(m_dirId, QString(), 1, [this](std::vector<std::unique_ptr<SyncthingItem>> &&items, QString &&errorMessage) {
Q_UNUSED(errorMessage)
m_fetchQueue.removeAll(QString());
if (items.empty()) {
return;
}
const auto last = items.size() - 1; const auto last = items.size() - 1;
beginInsertRows(QModelIndex(), 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max()); beginInsertRows(index(0, 0), 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
m_items = std::move(items); m_root->children = std::move(items);
m_root->childrenPopulated = true;
endInsertRows(); endInsertRows();
}); });
} }
@ -46,25 +59,48 @@ QHash<int, QByteArray> SyncthingFileModel::roleNames() const
QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const
{ {
if (row < 0 || column < 0 || column > 2) { if (row < 0 || column < 0 || column > 2 || parent.column() > 0) {
return QModelIndex(); return QModelIndex();
} }
if (!parent.isValid()) { if (!parent.isValid()) {
if (static_cast<std::size_t>(row) >= m_items.size()) { return static_cast<std::size_t>(row) ? QModelIndex() : createIndex(row, column, m_root.get());
return QModelIndex();
}
return createIndex(row, column, &m_items[static_cast<std::size_t>(row)]);
} }
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer()); auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
if (!parentItem) {
return QModelIndex();
}
auto &items = parentItem->children; auto &items = parentItem->children;
if (static_cast<std::size_t>(row) >= items.size()) { if (static_cast<std::size_t>(row) >= items.size()) {
return QModelIndex(); return QModelIndex();
} }
auto &item = items[static_cast<std::size_t>(row)]; auto &item = items[static_cast<std::size_t>(row)];
item.parent = parentItem; item->parent = parentItem;
return createIndex(row, column, &item); return createIndex(row, column, item.get());
}
QModelIndex SyncthingFileModel::index(const QString &path) const
{
auto parts = path.split(QChar('/'), Qt::SkipEmptyParts);
auto *parent = m_root.get();
auto res = createIndex(0, 0, parent);
for (const auto &part : parts) {
auto index = 0;
for (const auto &child : parent->children) {
if (child->name == part) {
child->parent = parent;
parent = child.get();
res = createIndex(index, 0, parent);
index = -1;
break;
}
++index;
}
if (index >= 0) {
res = QModelIndex();
return res;
}
}
return res;
} }
QString SyncthingFileModel::path(const QModelIndex &index) const QString SyncthingFileModel::path(const QModelIndex &index) const
@ -77,6 +113,10 @@ QString SyncthingFileModel::path(const QModelIndex &index) const
auto size = QString::size_type(); auto size = QString::size_type();
parts.reserve(reinterpret_cast<SyncthingItem *>(index.internalPointer())->level + 1); parts.reserve(reinterpret_cast<SyncthingItem *>(index.internalPointer())->level + 1);
for (auto i = index; i.isValid(); i = i.parent()) { for (auto i = index; i.isValid(); i = i.parent()) {
const auto *const item = reinterpret_cast<SyncthingItem *>(i.internalPointer());
if (item == m_root.get()) {
break;
}
parts.append(reinterpret_cast<SyncthingItem *>(i.internalPointer())->name); parts.append(reinterpret_cast<SyncthingItem *>(i.internalPointer())->name);
size += parts.back().size(); size += parts.back().size();
} }
@ -94,10 +134,10 @@ QModelIndex SyncthingFileModel::parent(const QModelIndex &child) const
return QModelIndex(); return QModelIndex();
} }
auto *const childItem = reinterpret_cast<SyncthingItem *>(child.internalPointer()); auto *const childItem = reinterpret_cast<SyncthingItem *>(child.internalPointer());
if (!childItem->parent) { if (!childItem) {
return QModelIndex(); return QModelIndex();
} }
return createIndex(static_cast<int>(childItem->index), 0, childItem->parent); return !childItem->parent ? QModelIndex() : createIndex(static_cast<int>(childItem->parent->index), 0, childItem->parent);
} }
QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation, int role) const QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation, int role) const
@ -192,7 +232,7 @@ int SyncthingFileModel::rowCount(const QModelIndex &parent) const
{ {
auto res = std::size_t(); auto res = std::size_t();
if (!parent.isValid()) { if (!parent.isValid()) {
res = m_items.size(); res = 1;
} else { } else {
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer()); auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer());
res = parentItem->childrenPopulated || parentItem->type != SyncthingItemType::Directory ? parentItem->children.size() : 1; res = parentItem->childrenPopulated || parentItem->type != SyncthingItemType::Directory ? parentItem->children.size() : 1;
@ -216,11 +256,11 @@ bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const
} }
/// \cond /// \cond
static void addLevel(std::vector<SyncthingItem> &items, int level) static void addLevel(std::vector<std::unique_ptr<SyncthingItem>> &items, int level)
{ {
for (auto &item : items) { for (auto &item : items) {
item.level += level; item->level += level;
addLevel(item.children, level); addLevel(item->children, level);
} }
} }
/// \endcond /// \endcond
@ -230,7 +270,7 @@ void SyncthingFileModel::fetchMore(const QModelIndex &parent)
if (!parent.isValid()) { if (!parent.isValid()) {
return; return;
} }
m_fetchQueue.append(parent); m_fetchQueue.append(path(parent));
if (m_fetchQueue.size() == 1) { if (m_fetchQueue.size() == 1) {
processFetchQueue(); processFetchQueue();
} }
@ -261,21 +301,36 @@ void SyncthingFileModel::processFetchQueue()
if (m_fetchQueue.isEmpty()) { if (m_fetchQueue.isEmpty()) {
return; return;
} }
const auto &parent = m_fetchQueue.front(); const auto &path = m_fetchQueue.front();
m_pendingRequest = m_connection.browse(m_dirId, path(parent), 1, [this, parent](std::vector<SyncthingItem> &&items) { m_pendingRequest = m_connection.browse(
auto *const parentItem = reinterpret_cast<SyncthingItem *>(parent.internalPointer()); m_dirId, path, 1, [this, p = path](std::vector<std::unique_ptr<SyncthingItem>> &&items, QString &&errorMessage) mutable {
addLevel(items, parentItem->level); Q_UNUSED(errorMessage)
beginRemoveRows(parent, 0, static_cast<int>(parentItem->children.size() - 1));
parentItem->children.clear(); m_fetchQueue.removeAll(p);
endRemoveRows(); const auto refreshedIndex = index(p);
const auto last = items.size() - 1; if (!refreshedIndex.isValid()) {
beginInsertRows(parent, 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max()); processFetchQueue();
parentItem->children = std::move(items); return;
parentItem->childrenPopulated = true; }
endInsertRows(); auto *const refreshedItem = reinterpret_cast<SyncthingItem *>(refreshedIndex.internalPointer());
m_fetchQueue.removeAll(parent); if (!refreshedItem->children.empty()) {
processFetchQueue(); beginRemoveRows(refreshedIndex, 0, static_cast<int>(refreshedItem->children.size() - 1));
}); refreshedItem->children.clear();
endRemoveRows();
}
if (!items.empty()) {
const auto last = items.size() - 1;
addLevel(items, refreshedItem->level);
for (auto &item : items) {
item->parent = refreshedItem;
}
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();
}
processFetchQueue();
});
} }
} // namespace Data } // namespace Data

View File

@ -3,23 +3,25 @@
#include "./syncthingmodel.h" #include "./syncthingmodel.h"
#include <syncthingconnector/syncthingconnection.h>
#include <memory>
#include <vector> #include <vector>
namespace Data { namespace Data {
struct SyncthingItem;
class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel { class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel {
Q_OBJECT Q_OBJECT
public: public:
enum SyncthingFileModelRole { NameRole = SyncthingModelUserRole + 1, SizeRole, ModificationTimeRole, Actions, ActionNames, ActionIcons }; enum SyncthingFileModelRole { NameRole = SyncthingModelUserRole + 1, SizeRole, ModificationTimeRole, Actions, ActionNames, ActionIcons };
explicit SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent = nullptr); explicit SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent = nullptr);
~SyncthingFileModel() override; ~SyncthingFileModel() override;
public Q_SLOTS: public Q_SLOTS:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex index(const QString &path) const;
QModelIndex parent(const QModelIndex &child) const override; QModelIndex parent(const QModelIndex &child) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) 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; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@ -44,9 +46,9 @@ private:
private: private:
SyncthingConnection &m_connection; SyncthingConnection &m_connection;
QString m_dirId; QString m_dirId;
QModelIndexList m_fetchQueue; QStringList m_fetchQueue;
QMetaObject::Connection m_pendingRequest; QMetaObject::Connection m_pendingRequest;
mutable std::vector<SyncthingItem> m_items; std::unique_ptr<SyncthingItem> m_root;
}; };
} // namespace Data } // namespace Data

View File

@ -278,7 +278,7 @@ QString StatusIconSettings::toString() const
StatusIcons::StatusIcons(const StatusIconSettings &settings) StatusIcons::StatusIcons(const StatusIconSettings &settings)
: disconnected( : disconnected(
QIcon(renderSvgImage(makeSyncthingIcon(settings.disconnectedColor, StatusEmblem::None, settings.strokeWidth), settings.renderSize))) QIcon(renderSvgImage(makeSyncthingIcon(settings.disconnectedColor, StatusEmblem::None, settings.strokeWidth), settings.renderSize)))
, idling(QIcon(renderSvgImage(makeSyncthingIcon(settings.idleColor, StatusEmblem::None, settings.strokeWidth), settings.renderSize))) , idling(QIcon(renderSvgImage(makeSyncthingIcon(settings.idleColor, StatusEmblem::None, settings.strokeWidth), settings.renderSize)))
, scanninig(QIcon(renderSvgImage(makeSyncthingIcon(settings.scanningColor, StatusEmblem::Scanning, settings.strokeWidth), settings.renderSize))) , scanninig(QIcon(renderSvgImage(makeSyncthingIcon(settings.scanningColor, StatusEmblem::Scanning, settings.strokeWidth), settings.renderSize)))
, notify(QIcon(renderSvgImage(makeSyncthingIcon(settings.warningColor, StatusEmblem::Alert, settings.strokeWidth), settings.renderSize))) , notify(QIcon(renderSvgImage(makeSyncthingIcon(settings.warningColor, StatusEmblem::Alert, settings.strokeWidth), settings.renderSize)))

View File

@ -1,13 +1,14 @@
#include "../syncthingdirectorymodel.h"
#include "../syncthingdevicemodel.h" #include "../syncthingdevicemodel.h"
#include "../syncthingdirectorymodel.h"
#include "../syncthingfilemodel.h"
#include <syncthingconnector/syncthingconnection.h> #include <syncthingconnector/syncthingconnection.h>
#include <QtTest/QtTest> #include <QtTest/QtTest>
#include <QEventLoop> #include <QEventLoop>
#include <QTimer>
#include <QLocale> #include <QLocale>
#include <QTimer>
#include <qtutilities/misc/compat.h> #include <qtutilities/misc/compat.h>
@ -20,6 +21,7 @@ private Q_SLOTS:
void testDirectoryModel(); void testDirectoryModel();
void testDevicesModel(); void testDevicesModel();
void testFileModel();
private: private:
QTimer m_timeout; QTimer m_timeout;
@ -38,7 +40,7 @@ void ModelTests::initTestCase()
m_timeout.start(); m_timeout.start();
connect(&m_timeout, &QTimer::timeout, this, [this] { connect(&m_timeout, &QTimer::timeout, this, [this] {
m_loop.quit(); m_loop.quit();
QFAIL("Timeout exceeded when loading mocked config/status for test"); QFAIL("Timeout exceeded");
}); });
// request config and status and wait until available // request config and status and wait until available
@ -97,5 +99,91 @@ void ModelTests::testDevicesModel()
QCOMPARE(model.index(1, 1, dev2Idx).data(), QStringLiteral("dynamic, tcp://192.168.1.3:22000")); QCOMPARE(model.index(1, 1, dev2Idx).data(), QStringLiteral("dynamic, tcp://192.168.1.3:22000"));
} }
void ModelTests::testFileModel()
{
auto row = 0;
const auto *dirInfo = m_connection.findDirInfo(QStringLiteral("GXWxf-3zgnU"), row);
QVERIFY(dirInfo);
// test behavior of empty/unpopulated model
auto model = Data::SyncthingFileModel(m_connection, *dirInfo);
QCOMPARE(model.rowCount(QModelIndex()), 1);
const auto rootIdx = QPersistentModelIndex(model.index(0, 0));
QVERIFY(rootIdx.isValid());
QVERIFY(!model.index(1, 0).isValid());
QCOMPARE(model.rowCount(rootIdx), 1);
QCOMPARE(model.index(0, 0, rootIdx).data(), QVariant());
QCOMPARE(model.index(1, 0, rootIdx).data(), QVariant());
QVERIFY(model.canFetchMore(rootIdx));
// wait until the root has been updated
connect(&model, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
Q_UNUSED(first)
Q_UNUSED(last)
if (!parent.parent().isValid() && parent.row() == 0 && parent.column() == 0) {
m_timeout.stop();
m_loop.quit();
}
});
m_timeout.start();
m_loop.exec();
QVERIFY(rootIdx.isValid());
QCOMPARE(model.rowCount(rootIdx), 2);
// test access to nested folders
const auto androidIdx = QPersistentModelIndex(model.index(0, 0, rootIdx));
const auto cameraIdx = QPersistentModelIndex(model.index(1, 0, rootIdx));
const auto nestedIdx = QPersistentModelIndex(model.index(0, 0, cameraIdx));
const auto initialAndroidPtr = androidIdx.constInternalPointer();
const auto initialCameraPtr = cameraIdx.constInternalPointer();
QVERIFY(androidIdx.isValid());
QVERIFY(cameraIdx.isValid());
QCOMPARE(androidIdx.parent(), rootIdx);
QCOMPARE(cameraIdx.parent(), rootIdx);
QCOMPARE(nestedIdx.parent(), cameraIdx);
QCOMPARE(model.rowCount(androidIdx), 0);
QCOMPARE(model.rowCount(cameraIdx), 5);
QCOMPARE(androidIdx.data(), QStringLiteral("100ANDRO"));
QCOMPARE(cameraIdx.data(), QStringLiteral("Camera"));
QCOMPARE(model.index(0, 0, cameraIdx).data(), QStringLiteral("IMG_20201114_124821.jpg"));
QCOMPARE(model.index(0, 1, cameraIdx).data(), QStringLiteral("10.19 MiB"));
QCOMPARE(model.index(0, 2, cameraIdx).data(), QStringLiteral("2020-12-16 22:31:34.500"));
QCOMPARE(model.index(1, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122451.jpg"));
QCOMPARE(model.index(2, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122504.jpg"));
QCOMPARE(model.index(3, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122505.jpg"));
QCOMPARE(model.index(4, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_125329.jpg"));
QCOMPARE(model.index(5, 0, cameraIdx).data(), QVariant());
QCOMPARE(model.index(5, 1, cameraIdx).data(), QVariant());
QCOMPARE(model.index(5, 2, cameraIdx).data(), QVariant());
QCOMPARE(model.index(5, 3, cameraIdx).data(), QVariant());
// test conversion of indexes to/from paths
const auto testPath = QStringLiteral("Camera/IMG_20201213_122504.jpg/");
const auto testPathIdx = model.index(2, 0, cameraIdx);
QCOMPARE(model.path(testPathIdx), testPath);
QCOMPARE(model.index(testPath), testPathIdx);
// re-load the data again and wait for the update
model.fetchMore(rootIdx);
m_timeout.start();
m_loop.exec();
// verify that only the root index is still valid (all other indexes have been invalidated)
QVERIFY(rootIdx.isValid());
QCOMPARE(model.rowCount(rootIdx), 2);
QVERIFY(androidIdx.constInternalPointer() != initialAndroidPtr);
QVERIFY(!androidIdx.isValid());
QVERIFY(cameraIdx.constInternalPointer() != initialCameraPtr);
QVERIFY(!cameraIdx.isValid());
QVERIFY(!nestedIdx.isValid());
// verify that data was re-loaded
const auto androidIdx2 = QPersistentModelIndex(model.index(0, 0, rootIdx));
const auto cameraIdx2 = QPersistentModelIndex(model.index(1, 0, rootIdx));
QCOMPARE(androidIdx2.data(), QStringLiteral("100ANDRO"));
QCOMPARE(cameraIdx2.data(), QStringLiteral("Camera"));
}
QTEST_MAIN(ModelTests) QTEST_MAIN(ModelTests)
#include "models.moc" #include "models.moc"

View File

@ -88,7 +88,7 @@ QDialog *browseRemoteFilesDialog(Data::SyncthingConnection &connection, const Da
dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setAttribute(Qt::WA_DeleteOnClose);
// setup model/view // setup model/view
auto model = new Data::SyncthingFileModel(connection, dir.id, &connection); auto model = new Data::SyncthingFileModel(connection, dir, &connection);
auto view = new QTreeView(dlg); auto view = new QTreeView(dlg);
view->setModel(model); view->setModel(model);
view->setContextMenuPolicy(Qt::CustomContextMenu); view->setContextMenuPolicy(Qt::CustomContextMenu);

View File

@ -178,7 +178,8 @@ public:
#endif #endif
} }
// register own handler to detect whether signal has been emitted // register own handler to detect whether signal has been emitted
m_emittedConnection = QObject::connect(sender, signal, sender, [this] { m_signalEmitted = true; }, Qt::DirectConnection); m_emittedConnection = QObject::connect(
sender, signal, sender, [this] { m_signalEmitted = true; }, Qt::DirectConnection);
#ifndef SYNCTHINGTESTHELPER_FOR_CLI #ifndef SYNCTHINGTESTHELPER_FOR_CLI
if (!m_emittedConnection) { if (!m_emittedConnection) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to check for signal emmitation")); CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to check for signal emmitation"));