Show local files in file model
This commit is contained in:
parent
8cc3ea7dd9
commit
cc1fef7365
|
@ -14,7 +14,7 @@ set(META_VERSION_MAJOR 1)
|
|||
set(META_VERSION_MINOR 5)
|
||||
set(META_VERSION_PATCH 4)
|
||||
set(META_RELEASE_DATE "2024-05-08")
|
||||
set(META_SOVERSION 14)
|
||||
set(META_SOVERSION 15)
|
||||
set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON)
|
||||
set(NETWORK_INFORMATION_SUPPORT ON)
|
||||
|
||||
|
|
|
@ -391,8 +391,8 @@ To avoid building c++utilities/qtutilities/qtforkawesome separately, follow the
|
|||
can be passed to CMake to influence the build.
|
||||
|
||||
### Further dependencies
|
||||
The following Qt modules are required (only the latest Qt 5 and Qt 6 version tested): `core`, `network`, `dbus`,
|
||||
`gui`, `widgets`, `svg`, `webenginewidgets`/`webkitwidgets`
|
||||
The following Qt modules are required (only the latest Qt 5 and Qt 6 version tested): `core`, `concurrent`,
|
||||
`network`, `dbus`, `gui`, `widgets`, `svg`, `webenginewidgets`/`webkitwidgets`
|
||||
|
||||
It is recommended to use at least Qt 5.14 to avoid limitations in previous versions (see *Known bugs* section).
|
||||
|
||||
|
|
|
@ -81,6 +81,10 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
|
|||
bool childrenPopulated = false;
|
||||
/// \brief Whether the item is "checked"; not set by default but might be set to flag an item for some mass-action.
|
||||
bool checked = false;
|
||||
/// \brief Whether the item is present in the Syncthing database.
|
||||
bool existsInDb = true;
|
||||
/// \brief Whether the item is present in the local file system.
|
||||
bool existsLocally = false;
|
||||
|
||||
bool isFilesystemItem() const;
|
||||
};
|
||||
|
|
|
@ -59,7 +59,7 @@ find_package(${PACKAGE_NAMESPACE_PREFIX}qtforkawesome${CONFIGURATION_PACKAGE_SUF
|
|||
use_qt_fork_awesome(VISIBILITY PUBLIC)
|
||||
|
||||
# link also explicitly against the following Qt modules
|
||||
list(APPEND ADDITIONAL_QT_MODULES Network Gui Widgets Svg)
|
||||
list(APPEND ADDITIONAL_QT_MODULES Concurrent Network Gui Widgets Svg)
|
||||
|
||||
# include modules to apply configuration
|
||||
include(BasicConfig)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "./syncthingfilemodel.h"
|
||||
#include "./colors.h"
|
||||
#include "./syncthingicons.h"
|
||||
|
||||
#include <syncthingconnector/syncthingconnection.h>
|
||||
|
@ -9,6 +10,7 @@
|
|||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QtConcurrent>
|
||||
#include <QGuiApplication>
|
||||
#include <QNetworkReply>
|
||||
#include <QStringBuilder>
|
||||
|
@ -63,13 +65,14 @@ SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const Sy
|
|||
{
|
||||
if (m_connection.isLocal()) {
|
||||
m_localPath = dir.pathWithoutTrailingSlash().toString();
|
||||
connect(&m_localItemLookup, &QFutureWatcherBase::finished, this, &SyncthingFileModel::handleLocalLookupFinished);
|
||||
}
|
||||
m_root->name = dir.displayName();
|
||||
m_root->modificationTime = dir.lastFileTime;
|
||||
m_root->size = dir.globalStats.bytes;
|
||||
m_root->type = SyncthingItemType::Directory;
|
||||
m_root->path = QStringLiteral(""); // assign an empty QString that is not null
|
||||
m_fetchQueue.append(QString());
|
||||
m_fetchQueue.append(m_root->path);
|
||||
processFetchQueue();
|
||||
}
|
||||
|
||||
|
@ -232,6 +235,11 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Qt::ForegroundRole:
|
||||
if (!item->existsInDb) {
|
||||
return Colors::gray(m_brightColors);
|
||||
}
|
||||
break;
|
||||
case Qt::ToolTipRole:
|
||||
switch (index.column()) {
|
||||
case 0:
|
||||
|
@ -365,16 +373,24 @@ void SyncthingFileModel::handleForkAwesomeIconsChanged()
|
|||
invalidateAllIndicies(QVector<int>({ Qt::DecorationRole }));
|
||||
}
|
||||
|
||||
void SyncthingFileModel::processFetchQueue()
|
||||
void SyncthingFileModel::handleBrightColorsChanged()
|
||||
{
|
||||
invalidateAllIndicies(QVector<int>({ Qt::ForegroundRole }));
|
||||
}
|
||||
|
||||
void SyncthingFileModel::processFetchQueue(const QString &lastItemPath)
|
||||
{
|
||||
if (!lastItemPath.isNull()) {
|
||||
m_fetchQueue.removeAll(lastItemPath);
|
||||
}
|
||||
if (m_fetchQueue.isEmpty()) {
|
||||
emit fetchQueueEmpty();
|
||||
return;
|
||||
}
|
||||
const auto &path = m_fetchQueue.front();
|
||||
const auto rootIndex = index(path);
|
||||
if (!rootIndex.isValid()) {
|
||||
m_fetchQueue.removeAll(path);
|
||||
processFetchQueue();
|
||||
processFetchQueue(path);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -386,41 +402,151 @@ void SyncthingFileModel::processFetchQueue()
|
|||
endInsertRows();
|
||||
}
|
||||
|
||||
m_pendingRequest = m_connection.browse(
|
||||
m_dirId, path, 1, [this, p = path](std::vector<std::unique_ptr<SyncthingItem>> &&items, QString &&errorMessage) mutable {
|
||||
m_pendingRequest.reply = nullptr;
|
||||
m_fetchQueue.removeAll(p);
|
||||
addErrorItem(items, std::move(errorMessage));
|
||||
// query directory entries from Syncthing database
|
||||
if (rootItem->existsInDb) {
|
||||
m_pendingRequest = m_connection.browse(
|
||||
m_dirId, path, 1, [this](std::vector<std::unique_ptr<SyncthingItem>> &&items, QString &&errorMessage) mutable {
|
||||
m_pendingRequest.reply = nullptr;
|
||||
addErrorItem(items, std::move(errorMessage));
|
||||
|
||||
const auto refreshedIndex = index(p);
|
||||
if (!refreshedIndex.isValid()) {
|
||||
processFetchQueue();
|
||||
return;
|
||||
}
|
||||
auto *const refreshedItem = reinterpret_cast<SyncthingItem *>(refreshedIndex.internalPointer());
|
||||
const auto previousChildCount = refreshedItem->children.size();
|
||||
if (previousChildCount) {
|
||||
beginRemoveRows(refreshedIndex, 0, static_cast<int>(refreshedItem->children.size() - 1));
|
||||
refreshedItem->children.clear();
|
||||
endRemoveRows();
|
||||
}
|
||||
if (!items.empty()) {
|
||||
const auto last = items.size() - 1;
|
||||
for (auto &item : items) {
|
||||
item->parent = refreshedItem;
|
||||
const auto refreshedIndex = index(m_pendingRequest.forPath);
|
||||
if (!refreshedIndex.isValid()) {
|
||||
processFetchQueue(m_pendingRequest.forPath);
|
||||
return;
|
||||
}
|
||||
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();
|
||||
auto *const refreshedItem = reinterpret_cast<SyncthingItem *>(refreshedIndex.internalPointer());
|
||||
const auto previousChildCount = refreshedItem->children.size();
|
||||
if (previousChildCount) {
|
||||
beginRemoveRows(refreshedIndex, 0, static_cast<int>(refreshedItem->children.size() - 1));
|
||||
refreshedItem->children.clear();
|
||||
endRemoveRows();
|
||||
}
|
||||
if (!items.empty()) {
|
||||
const auto last = items.size() - 1;
|
||||
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, QVector<int>{ Qt::DisplayRole });
|
||||
}
|
||||
if (!m_pendingRequest.localLookup.isCanceled()) {
|
||||
m_pendingRequest.refreshedIndex = refreshedIndex;
|
||||
m_localItemLookup.setFuture(m_pendingRequest.localLookup);
|
||||
} else {
|
||||
processFetchQueue(m_pendingRequest.forPath);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
m_pendingRequest = SyncthingConnection::QueryResult();
|
||||
}
|
||||
m_pendingRequest.forPath = path;
|
||||
|
||||
// lookup the directory entries locally to also show ignored files
|
||||
if (m_localPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_pendingRequest.localLookup = QtConcurrent::run([dir = QDir(m_localPath % QChar('/') % path)] {
|
||||
auto items = std::make_shared<std::map<QString, SyncthingItem>>();
|
||||
auto entries = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
|
||||
for (const auto &entry : entries) {
|
||||
const auto entryName = entry.fileName();
|
||||
auto &item = (*items)[entryName];
|
||||
item.name = entryName;
|
||||
item.existsInDb = false;
|
||||
item.existsLocally = true;
|
||||
item.size = static_cast<std::size_t>(entry.size());
|
||||
item.modificationTime = DateTime::unixEpochStart() + TimeSpan(TimeSpan::ticksPerMillisecond * entry.lastModified().toMSecsSinceEpoch());
|
||||
if (entry.isSymbolicLink()) {
|
||||
item.type = SyncthingItemType::Symlink;
|
||||
} else if (entry.isDir()) {
|
||||
item.type = SyncthingItemType::Directory;
|
||||
} else {
|
||||
item.type = SyncthingItemType::File;
|
||||
}
|
||||
if (refreshedItem->children.size() != previousChildCount) {
|
||||
const auto sizeIndex = refreshedIndex.siblingAtColumn(1);
|
||||
emit dataChanged(sizeIndex, sizeIndex, QVector<int>{ Qt::DisplayRole });
|
||||
}
|
||||
processFetchQueue();
|
||||
});
|
||||
}
|
||||
return items;
|
||||
});
|
||||
if (!rootItem->existsInDb) {
|
||||
m_pendingRequest.refreshedIndex = rootIndex;
|
||||
m_localItemLookup.setFuture(m_pendingRequest.localLookup);
|
||||
}
|
||||
}
|
||||
|
||||
void SyncthingFileModel::handleLocalLookupFinished()
|
||||
{
|
||||
// get refreshed index/item
|
||||
const auto &refreshedIndex = m_pendingRequest.refreshedIndex;
|
||||
if (!refreshedIndex.isValid()) {
|
||||
processFetchQueue(m_pendingRequest.forPath);
|
||||
return;
|
||||
}
|
||||
auto *const refreshedItem = reinterpret_cast<SyncthingItem *>(refreshedIndex.internalPointer());
|
||||
auto &items = refreshedItem->children;
|
||||
const auto previousChildCount = items.size();
|
||||
refreshedItem->childrenPopulated = true;
|
||||
|
||||
// clear loading item
|
||||
if (!refreshedItem->existsInDb && !items.empty()) {
|
||||
const auto last = items.size() - 1;
|
||||
beginRemoveRows(refreshedIndex, 0, last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max());
|
||||
items.clear();
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
// get result from local lookup
|
||||
auto res = m_pendingRequest.localLookup.result();
|
||||
if (!res || res->empty()) {
|
||||
processFetchQueue(m_pendingRequest.forPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// mark items from the database query as locally existing if they do; mark items from local lookup as existing in the db if they do
|
||||
auto &localItems = *res;
|
||||
for (auto &child : refreshedItem->children) {
|
||||
auto localItemIter = localItems.find(child->name);
|
||||
if (localItemIter == localItems.end()) {
|
||||
continue;
|
||||
}
|
||||
child->existsLocally = true;
|
||||
localItemIter->second.existsInDb = true;
|
||||
}
|
||||
|
||||
// insert items from local lookup that are not already present via the database query (probably ignored files)
|
||||
for (auto &[localItemName, localItem] : localItems) {
|
||||
if (localItem.existsInDb) {
|
||||
continue;
|
||||
}
|
||||
const auto last = items.size();
|
||||
const auto index = last < std::numeric_limits<int>::max() ? static_cast<int>(last) : std::numeric_limits<int>::max();
|
||||
beginInsertRows(refreshedIndex, index, index);
|
||||
auto &item = items.emplace_back(std::make_unique<SyncthingItem>(std::move(localItem)));
|
||||
item->parent = refreshedItem;
|
||||
item->index = last;
|
||||
populatePath(item->path = refreshedItem->path % QChar('/') % item->name, item->children);
|
||||
endInsertRows();
|
||||
}
|
||||
if (refreshedItem->children.size() != previousChildCount) {
|
||||
const auto sizeIndex = refreshedIndex.sibling(refreshedIndex.row(), 1);
|
||||
emit dataChanged(sizeIndex, sizeIndex, QVector<int>{ Qt::DisplayRole });
|
||||
}
|
||||
|
||||
processFetchQueue(m_pendingRequest.forPath);
|
||||
}
|
||||
|
||||
SyncthingFileModel::QueryResult &SyncthingFileModel::QueryResult::operator=(SyncthingConnection::QueryResult &&other)
|
||||
{
|
||||
reply = other.reply;
|
||||
connection = std::move(other.connection);
|
||||
localLookup = QFuture<LocalLookupRes>();
|
||||
refreshedIndex = QModelIndex();
|
||||
return *this;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
#include <syncthingconnector/syncthingconnection.h>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
namespace Data {
|
||||
|
@ -42,20 +46,35 @@ public Q_SLOTS:
|
|||
public:
|
||||
QString path(const QModelIndex &path) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void fetchQueueEmpty();
|
||||
|
||||
private Q_SLOTS:
|
||||
void handleConfigInvalidated() override;
|
||||
void handleNewConfigAvailable() override;
|
||||
void handleForkAwesomeIconsChanged() override;
|
||||
void handleBrightColorsChanged() override;
|
||||
void handleLocalLookupFinished();
|
||||
|
||||
private:
|
||||
void processFetchQueue();
|
||||
void processFetchQueue(const QString &lastItemPath = QString());
|
||||
|
||||
private:
|
||||
using SyncthingItems = std::vector<std::unique_ptr<SyncthingItem>>;
|
||||
using LocalLookupRes = std::shared_ptr<std::map<QString, SyncthingItem>>;
|
||||
struct QueryResult : SyncthingConnection::QueryResult {
|
||||
QString forPath;
|
||||
QFuture<LocalLookupRes> localLookup;
|
||||
QPersistentModelIndex refreshedIndex;
|
||||
QueryResult &operator=(SyncthingConnection::QueryResult &&);
|
||||
};
|
||||
|
||||
SyncthingConnection &m_connection;
|
||||
QString m_dirId;
|
||||
QString m_localPath;
|
||||
QStringList m_fetchQueue;
|
||||
SyncthingConnection::QueryResult m_pendingRequest;
|
||||
QueryResult m_pendingRequest;
|
||||
QFutureWatcher<LocalLookupRes> m_localItemLookup;
|
||||
std::unique_ptr<SyncthingItem> m_root;
|
||||
};
|
||||
|
||||
|
|
|
@ -63,10 +63,7 @@ void SyncthingModel::setBrightColors(bool brightColors)
|
|||
return;
|
||||
}
|
||||
m_brightColors = brightColors;
|
||||
|
||||
if (const QVector<int> &affectedRoles = colorRoles(); !affectedRoles.isEmpty()) {
|
||||
invalidateTopLevelIndicies(affectedRoles);
|
||||
}
|
||||
handleBrightColorsChanged();
|
||||
}
|
||||
|
||||
void SyncthingModel::handleConfigInvalidated()
|
||||
|
@ -87,4 +84,11 @@ void SyncthingModel::handleForkAwesomeIconsChanged()
|
|||
{
|
||||
}
|
||||
|
||||
void SyncthingModel::handleBrightColorsChanged()
|
||||
{
|
||||
if (const QVector<int> &affectedRoles = colorRoles(); !affectedRoles.isEmpty()) {
|
||||
invalidateTopLevelIndicies(affectedRoles);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
|
|
|
@ -41,6 +41,7 @@ private Q_SLOTS:
|
|||
virtual void handleNewConfigAvailable();
|
||||
virtual void handleStatusIconsChanged();
|
||||
virtual void handleForkAwesomeIconsChanged();
|
||||
virtual void handleBrightColorsChanged();
|
||||
|
||||
protected:
|
||||
Data::SyncthingConnection &m_connection;
|
||||
|
|
|
@ -117,13 +117,9 @@ void ModelTests::testFileModel()
|
|||
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();
|
||||
}
|
||||
connect(&model, &Data::SyncthingFileModel::fetchQueueEmpty, this, [this]() {
|
||||
m_timeout.stop();
|
||||
m_loop.quit();
|
||||
});
|
||||
m_timeout.start();
|
||||
m_loop.exec();
|
||||
|
|
Loading…
Reference in New Issue