Fix and refactor directory error handling
* Rely not only on the FolderErrors event * Request pull errors when opening the dialog * Use "pullErrors" from FolderSummary * Move code for directory error dialog into its own class This should fix that sometimes obsolete errors were still displayed or actually present errors missing.
This commit is contained in:
parent
dcbc19cf8c
commit
4a42a3f0c0
|
@ -203,12 +203,6 @@ void SyncthingConnection::connect()
|
|||
// reset status
|
||||
m_reconnecting = m_hasConfig = m_hasStatus = m_hasEvents = m_hasDiskEvents = false;
|
||||
|
||||
// remove error items (might have been invalidated)
|
||||
for (SyncthingDir &dir : m_dirs) {
|
||||
dir.itemErrors.swap(dir.previousItemErrors);
|
||||
dir.itemErrors.clear();
|
||||
}
|
||||
|
||||
// check configuration
|
||||
if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) {
|
||||
emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
|
||||
|
|
|
@ -151,9 +151,11 @@ public:
|
|||
std::vector<const SyncthingDev *> connectedDevices() const;
|
||||
const QJsonObject &rawConfig() const;
|
||||
SyncthingDir *findDirInfo(const QString &dirId, int &row);
|
||||
const SyncthingDir *findDirInfo(const QString &dirId, int &row) const;
|
||||
SyncthingDir *findDirInfo(QLatin1String key, const QJsonObject &object, int *row = nullptr);
|
||||
SyncthingDir *findDirInfoByPath(const QString &path, QString &relativePath, int &row);
|
||||
SyncthingDev *findDevInfo(const QString &devId, int &row);
|
||||
const SyncthingDev *findDevInfo(const QString &devId, int &row) const;
|
||||
SyncthingDev *findDevInfoByName(const QString &devName, int &row);
|
||||
|
||||
const QList<QSslError> &expectedSslErrors() const;
|
||||
|
@ -197,6 +199,7 @@ public Q_SLOTS:
|
|||
void requestClearingErrors();
|
||||
void requestDirStatistics();
|
||||
void requestDirStatus(const QString &dirId);
|
||||
void requestDirPullErrors(const QString &dirId, int page = 0, int perPage = 0);
|
||||
void requestCompletion(const QString &devId, const QString &dirId);
|
||||
void requestDeviceStatistics();
|
||||
void requestVersion();
|
||||
|
@ -273,6 +276,7 @@ private Q_SLOTS:
|
|||
void readRestart();
|
||||
void readShutdown();
|
||||
void readDirStatus();
|
||||
void readDirPullErrors();
|
||||
void readDirSummary(ChronoUtilities::DateTime eventTime, const QJsonObject &summary, SyncthingDir &dirInfo, int index);
|
||||
void readDirRejected(ChronoUtilities::DateTime eventTime, const QString &dirId, const QJsonObject &eventData);
|
||||
void readDevRejected(ChronoUtilities::DateTime eventTime, const QString &devId, const QJsonObject &eventData);
|
||||
|
@ -716,6 +720,27 @@ inline const QJsonObject &SyncthingConnection::rawConfig() const
|
|||
{
|
||||
return m_rawConfig;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the directory info object for the directory with the specified ID.
|
||||
* \returns Returns a pointer to the object or nullptr if not found.
|
||||
* \remarks The returned object becomes invalid when the newDirs() signal is emitted or the connection is destroyed.
|
||||
*/
|
||||
inline const SyncthingDir *SyncthingConnection::findDirInfo(const QString &dirId, int &row) const
|
||||
{
|
||||
return const_cast<SyncthingConnection *>(this)->findDirInfo(dirId, row);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the device info object for the device with the specified ID.
|
||||
* \returns Returns a pointer to the object or nullptr if not found.
|
||||
* \remarks The returned object becomes invalid when the newConfig() signal is emitted or the connection is destroyed.
|
||||
*/
|
||||
inline const SyncthingDev *SyncthingConnection::findDevInfo(const QString &devId, int &row) const
|
||||
{
|
||||
return const_cast<SyncthingConnection *>(this)->findDevInfo(devId, row);
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
|
||||
Q_DECLARE_METATYPE(Data::SyncthingLogEntry)
|
||||
|
|
|
@ -952,6 +952,62 @@ void SyncthingConnection::readDirStatus()
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Requests pull errors for \a dirId asynchronously.
|
||||
*
|
||||
* The dirStatusChanged() signal is emitted on success and error() in the error case.
|
||||
*/
|
||||
void SyncthingConnection::requestDirPullErrors(const QString &dirId, int page, int perPage)
|
||||
{
|
||||
QUrlQuery query;
|
||||
query.addQueryItem(QStringLiteral("folder"), dirId);
|
||||
if (page > 0 && perPage > 0) {
|
||||
query.addQueryItem(QStringLiteral("page"), QString::number(page));
|
||||
query.addQueryItem(QStringLiteral("perpage"), QString::number(perPage));
|
||||
}
|
||||
auto *const reply = requestData(QStringLiteral("folder/pullerrors"), query);
|
||||
reply->setProperty("dirId", dirId);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirPullErrors);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Reads data from requestDirPullErrors().
|
||||
*/
|
||||
void SyncthingConnection::readDirPullErrors()
|
||||
{
|
||||
auto *const reply = static_cast<QNetworkReply *>(sender());
|
||||
reply->deleteLater();
|
||||
|
||||
// determine relevant dir
|
||||
int index;
|
||||
const QString dirId(reply->property("dirId").toString());
|
||||
SyncthingDir *const dir = findDirInfo(dirId, index);
|
||||
if (!dir) {
|
||||
// discard errors for unknown dirs
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reply->error()) {
|
||||
case QNetworkReply::NoError: {
|
||||
// parse JSON
|
||||
const QByteArray response(reply->readAll());
|
||||
QJsonParseError jsonError;
|
||||
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
|
||||
if (jsonError.error != QJsonParseError::NoError) {
|
||||
emitError(tr("Unable to parse pull errors for directory %1: ").arg(dirId), jsonError, reply, response);
|
||||
return;
|
||||
}
|
||||
|
||||
readFolderErrors(DateTime::gmtNow(), replyDoc.object(), *dir, index);
|
||||
break;
|
||||
}
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
return;
|
||||
default:
|
||||
emitError(tr("Unable to request pull errors for directory %1: ").arg(dirId), SyncthingErrorCategory::SpecificRequest, reply);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Requests completion for \a devId and \a dirId asynchronously.
|
||||
*/
|
||||
|
@ -1279,6 +1335,7 @@ void SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &
|
|||
neededStats.files = jsonValueToInt(summary.value(QLatin1String("needFiles")));
|
||||
neededStats.dirs = jsonValueToInt(summary.value(QLatin1String("needDirectories")));
|
||||
neededStats.symlinks = jsonValueToInt(summary.value(QLatin1String("needSymlinks")));
|
||||
dir.pullErrorCount = jsonValueToInt(summary.value(QLatin1String("pullErrors")));
|
||||
|
||||
dir.ignorePatterns = summary.value(QLatin1String("ignorePatterns")).toBool();
|
||||
dir.lastStatisticsUpdate = eventTime;
|
||||
|
@ -1715,13 +1772,24 @@ void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject
|
|||
// handle unsuccessful operation
|
||||
const auto error(eventData.value(QLatin1String("error")).toString()), item(eventData.value(QLatin1String("item")).toString());
|
||||
if (!error.isEmpty()) {
|
||||
if (dirInfo->status == SyncthingDirStatus::OutOfSync) {
|
||||
// add error item if not already present
|
||||
if (dirInfo->status != SyncthingDirStatus::OutOfSync) {
|
||||
// FIXME: find better way to check whether the event is still relevant
|
||||
dirInfo->itemErrors.emplace_back(error, item);
|
||||
// emitNotification will trigger status update, so no need to call setStatus(status())
|
||||
emit dirStatusChanged(*dirInfo, index);
|
||||
emitNotification(eventTime, error);
|
||||
return;
|
||||
}
|
||||
for (const auto &itemError : dirInfo->itemErrors) {
|
||||
if (itemError.message == error && itemError.path == item) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
dirInfo->itemErrors.emplace_back(error, item);
|
||||
if (dirInfo->pullErrorCount < dirInfo->itemErrors.size()) {
|
||||
dirInfo->pullErrorCount = dirInfo->itemErrors.size();
|
||||
}
|
||||
|
||||
// emitNotification will trigger status update, so no need to call setStatus(status())
|
||||
emit dirStatusChanged(*dirInfo, index);
|
||||
emitNotification(eventTime, error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1740,31 +1808,38 @@ void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Reads results of requestEvents().
|
||||
* \brief Reads results of requestEvents() and requestDirPullErrors().
|
||||
*/
|
||||
void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
|
||||
{
|
||||
const QJsonArray errors(eventData.value(QLatin1String("errors")).toArray());
|
||||
if (errors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// ignore errors occurred before the last time the directory was in "sync" state (Syncthing re-emits recurring errors)
|
||||
if (dirInfo.lastSyncStarted > eventTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QJsonValue &errorVal : errors) {
|
||||
// clear previous errors (considering syncthing/lib/model/rwfolder.go it seems that also the event API always returns a
|
||||
// full list of events and not only new ones)
|
||||
dirInfo.itemErrors.clear();
|
||||
|
||||
// add errors
|
||||
for (const QJsonValueRef errorVal : eventData.value(QLatin1String("errors")).toArray()) {
|
||||
const QJsonObject error(errorVal.toObject());
|
||||
if (error.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
auto &errors = dirInfo.itemErrors;
|
||||
SyncthingItemError dirError(error.value(QLatin1String("error")).toString(), error.value(QLatin1String("path")).toString());
|
||||
if (find(errors.cbegin(), errors.cend(), dirError) != errors.cend()) {
|
||||
continue;
|
||||
}
|
||||
errors.emplace_back(move(dirError));
|
||||
dirInfo.itemErrors.emplace_back(error.value(QLatin1String("error")).toString(), error.value(QLatin1String("path")).toString());
|
||||
}
|
||||
|
||||
// set pullErrorCount in case it has not already been populated from the FolderSummary event
|
||||
if (dirInfo.pullErrorCount < dirInfo.itemErrors.size()) {
|
||||
dirInfo.pullErrorCount = dirInfo.itemErrors.size();
|
||||
}
|
||||
|
||||
// ensure the directory is considered out-of-sync
|
||||
if (dirInfo.pullErrorCount) {
|
||||
dirInfo.assignStatus(SyncthingDirStatus::OutOfSync, eventTime);
|
||||
}
|
||||
|
||||
emit dirStatusChanged(dirInfo, index);
|
||||
}
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ MockedReply *MockedReply::forRequest(const QString &method, const QString &path,
|
|||
}
|
||||
} else if (path == QLatin1String("folder/pullerrors")) {
|
||||
const QString folder(query.queryItemValue(QStringLiteral("folder")));
|
||||
if (folder == QLatin1String("GXWxf-3zgnU")) {
|
||||
if (folder == QLatin1String("GXWxf-3zgnU") && s_eventIndex >= 6) {
|
||||
buffer = &pullErrors;
|
||||
}
|
||||
} else if (path == QLatin1String("system/connections")) {
|
||||
|
|
|
@ -55,26 +55,15 @@ bool SyncthingDir::checkWhetherStatusUpdateRelevant(DateTime time)
|
|||
|
||||
bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, DateTime time)
|
||||
{
|
||||
// clear out-of-sync items
|
||||
switch (newStatus) {
|
||||
case SyncthingDirStatus::Unknown:
|
||||
case SyncthingDirStatus::OutOfSync:
|
||||
break;
|
||||
default:
|
||||
if (newStatus == SyncthingDirStatus::Synchronizing || lastSyncStarted.isNull()) {
|
||||
// errors become obsolete; however errors must be kept as previous errors to be able
|
||||
// to identify "new errors" as known errors
|
||||
previousItemErrors.clear();
|
||||
previousItemErrors.swap(itemErrors);
|
||||
}
|
||||
}
|
||||
|
||||
// set time of the last "sync" state (used internally and not displayed, hence keep it GMT)
|
||||
switch (newStatus) {
|
||||
case SyncthingDirStatus::Synchronizing:
|
||||
// handle obsoletion of out-of-sync items: no FolderErrors are accepted older than the last "sync" state are accepted
|
||||
if (newStatus == SyncthingDirStatus::Synchronizing) {
|
||||
// update time of last "sync" state and obsolete currently assigned errors
|
||||
lastSyncStarted = time; // used internally and not displayed, hence keep it GMT
|
||||
itemErrors.clear();
|
||||
pullErrorCount = 0;
|
||||
} else if (lastSyncStarted.isNull() && newStatus != SyncthingDirStatus::OutOfSync) {
|
||||
// prevent adding new errors from "before the first status" if the time of the last "sync" state is unknown
|
||||
lastSyncStarted = time;
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
|
||||
// clear global error if not out-of-sync anymore
|
||||
|
@ -86,14 +75,12 @@ bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, DateTime t
|
|||
return false;
|
||||
}
|
||||
|
||||
// update last scan time and status
|
||||
switch (status) {
|
||||
case SyncthingDirStatus::Scanning:
|
||||
// update last scan time if the previous status was scanning
|
||||
if (status == SyncthingDirStatus::Scanning) {
|
||||
// FIXME: better use \a time and convert it from GMT to local time
|
||||
lastScanTime = DateTime::now();
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
|
||||
status = newStatus;
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
namespace Data {
|
||||
|
||||
enum class SyncthingDirStatus { Unknown, Idle, Scanning, Synchronizing, OutOfSync };
|
||||
// note: update "visible: status === 4" in DirectoriesPage.qml (which references OutOfSync by
|
||||
// its raw value due to limitations of Qt/Qml) when updating this enum
|
||||
|
||||
QString LIB_SYNCTHING_CONNECTOR_EXPORT statusString(SyncthingDirStatus status);
|
||||
|
||||
|
@ -153,8 +151,8 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
|
|||
double fileSystemWatcherDelay = 0.0;
|
||||
std::map<QString, SyncthingCompletion> completionByDevice;
|
||||
QString globalError;
|
||||
quint64 pullErrorCount = 0;
|
||||
std::vector<SyncthingItemError> itemErrors;
|
||||
std::vector<SyncthingItemError> previousItemErrors;
|
||||
std::vector<SyncthingFileChange> recentChanges;
|
||||
SyncthingStatistics globalStats, localStats, neededStats;
|
||||
ChronoUtilities::DateTime lastStatisticsUpdate;
|
||||
|
|
|
@ -169,12 +169,12 @@ bool SyncthingDirActions::updateStatus(const SyncthingDir &dir)
|
|||
m_lastScanAction.setText(tr("Last scan time: ") + agoString(dir.lastScanTime));
|
||||
m_lastScanAction.setIcon(QIcon::fromTheme(QStringLiteral("accept_time_event")));
|
||||
m_rescanIntervalAction.setText(tr("Rescan interval: %1 seconds").arg(dir.rescanInterval));
|
||||
if (dir.itemErrors.empty()) {
|
||||
if (!dir.pullErrorCount) {
|
||||
m_errorsAction.setVisible(false);
|
||||
} else {
|
||||
m_errorsAction.setVisible(true);
|
||||
m_errorsAction.setIcon(QIcon::fromTheme(QStringLiteral("dialog-error")));
|
||||
m_errorsAction.setText(tr("%1 item(s) out-of-sync", nullptr, trQuandity(dir.itemErrors.size())).arg(dir.itemErrors.size()));
|
||||
m_errorsAction.setText(tr("%1 item(s) out-of-sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.pullErrorCount));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ QHash<int, QByteArray> SyncthingDirectoryModel::roleNames() const
|
|||
roles[DirectoryPaused] = "paused";
|
||||
roles[DirectoryId] = "dirId";
|
||||
roles[DirectoryPath] = "path";
|
||||
roles[DirectoryPullErrorCount] = "pullErrorCount";
|
||||
roles[DirectoryDetail] = "detail";
|
||||
return roles;
|
||||
}());
|
||||
|
@ -167,19 +168,18 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const
|
|||
case 8:
|
||||
return dir.lastFileName.isEmpty() ? tr("unknown") : dir.lastFileName;
|
||||
case 9:
|
||||
if (!dir.globalError.isEmpty() || !dir.itemErrors.empty()) {
|
||||
if (dir.itemErrors.empty()) {
|
||||
return dir.globalError;
|
||||
}
|
||||
if (dir.globalError.isEmpty()) {
|
||||
return tr("%1 item(s) out of sync", nullptr, static_cast<int>(dir.itemErrors.size())).arg(dir.itemErrors.size());
|
||||
}
|
||||
return tr("%1 and %2 item(s) out of sync", nullptr, static_cast<int>(dir.itemErrors.size()))
|
||||
.arg(dir.globalError)
|
||||
.arg(dir.itemErrors.size());
|
||||
} else {
|
||||
if (dir.globalError.isEmpty() && !dir.pullErrorCount) {
|
||||
return tr("none");
|
||||
}
|
||||
if (!dir.pullErrorCount) {
|
||||
return dir.globalError;
|
||||
}
|
||||
if (dir.globalError.isEmpty()) {
|
||||
return tr("%1 item(s) out of sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.pullErrorCount);
|
||||
}
|
||||
return tr("%1 and %2 item(s) out of sync", nullptr, trQuandity(dir.pullErrorCount))
|
||||
.arg(dir.globalError)
|
||||
.arg(dir.pullErrorCount);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -202,7 +202,7 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const
|
|||
return dir.lastFileName.isEmpty() ? Colors::gray(m_brightColors)
|
||||
: (dir.lastFileDeleted ? Colors::red(m_brightColors) : QVariant());
|
||||
case 9:
|
||||
return dir.globalError.isEmpty() && dir.itemErrors.empty() ? Colors::gray(m_brightColors) : Colors::red(m_brightColors);
|
||||
return dir.globalError.isEmpty() && !dir.pullErrorCount ? Colors::gray(m_brightColors) : Colors::red(m_brightColors);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -315,6 +315,8 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const
|
|||
return dir.id;
|
||||
case DirectoryPath:
|
||||
return dir.path;
|
||||
case DirectoryPullErrorCount:
|
||||
return dir.pullErrorCount;
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
@ -363,7 +365,7 @@ void SyncthingDirectoryModel::dirStatusChanged(const SyncthingDir &, int index)
|
|||
{
|
||||
const QModelIndex modelIndex1(this->index(index, 0, QModelIndex()));
|
||||
static const QVector<int> modelRoles1({ Qt::DisplayRole, Qt::EditRole, Qt::DecorationRole, DirectoryPaused, DirectoryStatus,
|
||||
DirectoryStatusString, DirectoryStatusColor, DirectoryId, DirectoryPath });
|
||||
DirectoryStatusString, DirectoryStatusColor, DirectoryId, DirectoryPath, DirectoryPullErrorCount });
|
||||
emit dataChanged(modelIndex1, modelIndex1, modelRoles1);
|
||||
const QModelIndex modelIndex2(this->index(index, 1, QModelIndex()));
|
||||
static const QVector<int> modelRoles2({ Qt::DisplayRole, Qt::EditRole, Qt::ForegroundRole });
|
||||
|
|
|
@ -21,6 +21,7 @@ public:
|
|||
DirectoryStatusColor,
|
||||
DirectoryId,
|
||||
DirectoryPath,
|
||||
DirectoryPullErrorCount,
|
||||
DirectoryDetail,
|
||||
};
|
||||
|
||||
|
|
|
@ -11,10 +11,13 @@ class SyncthingConnection;
|
|||
|
||||
class LIB_SYNCTHING_MODEL_EXPORT SyncthingModel : public QAbstractItemModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(SyncthingConnection *connection READ connection)
|
||||
Q_PROPERTY(bool brightColors READ brightColors WRITE setBrightColors)
|
||||
|
||||
public:
|
||||
explicit SyncthingModel(SyncthingConnection &connection, QObject *parent = nullptr);
|
||||
Data::SyncthingConnection *connection();
|
||||
const Data::SyncthingConnection *connection() const;
|
||||
bool brightColors() const;
|
||||
void setBrightColors(bool brightColors);
|
||||
|
||||
|
@ -25,6 +28,16 @@ protected:
|
|||
bool m_brightColors;
|
||||
};
|
||||
|
||||
inline SyncthingConnection *SyncthingModel::connection()
|
||||
{
|
||||
return &m_connection;
|
||||
}
|
||||
|
||||
inline const SyncthingConnection *SyncthingModel::connection() const
|
||||
{
|
||||
return &m_connection;
|
||||
}
|
||||
|
||||
inline bool SyncthingModel::brightColors() const
|
||||
{
|
||||
return m_brightColors;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "../../connector/syncthingservice.h"
|
||||
#include "../../connector/utils.h"
|
||||
|
||||
#include "../../widgets/misc/direrrorsdialog.h"
|
||||
#include "../../widgets/misc/internalerrorsdialog.h"
|
||||
#include "../../widgets/misc/otherdialogs.h"
|
||||
#include "../../widgets/misc/textviewdialog.h"
|
||||
|
@ -313,15 +314,20 @@ void SyncthingApplet::showInternalErrorsDialog()
|
|||
errorViewDlg->show();
|
||||
}
|
||||
|
||||
void SyncthingApplet::showDirectoryErrors(unsigned int directoryIndex) const
|
||||
void SyncthingApplet::showDirectoryErrors(unsigned int directoryIndex)
|
||||
{
|
||||
const auto &dirs = m_connection.dirInfo();
|
||||
if (directoryIndex < dirs.size()) {
|
||||
auto *const dlg = TextViewDialog::forDirectoryErrors(dirs[directoryIndex]);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
centerWidget(dlg);
|
||||
dlg->show();
|
||||
if (directoryIndex >= dirs.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &dir(dirs[directoryIndex]);
|
||||
m_connection.requestDirPullErrors(dir.id);
|
||||
|
||||
auto *const dlg = new DirectoryErrorsDialog(m_connection, dir);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
centerWidget(dlg);
|
||||
dlg->show();
|
||||
}
|
||||
|
||||
void SyncthingApplet::copyToClipboard(const QString &text)
|
||||
|
|
|
@ -104,7 +104,7 @@ public Q_SLOTS:
|
|||
void showNotificationsDialog();
|
||||
void dismissNotifications();
|
||||
void showInternalErrorsDialog();
|
||||
void showDirectoryErrors(unsigned int directoryIndex) const;
|
||||
void showDirectoryErrors(unsigned int directoryIndex);
|
||||
void copyToClipboard(const QString &text);
|
||||
void updateStatusIconAndTooltip();
|
||||
|
||||
|
|
|
@ -73,9 +73,7 @@ ColumnLayout {
|
|||
id: errorsButton
|
||||
iconSource: "emblem-important"
|
||||
tooltip: qsTr("Show errors")
|
||||
// 4 stands for SyncthingDirStatus::OutOfSync, unfortunately there is currently
|
||||
// no way to expose this to QML without conflicting SyncthingStatus
|
||||
visible: status === 4
|
||||
visible: pullErrorCount > 0
|
||||
onClicked: {
|
||||
plasmoid.nativeInterface.showDirectoryErrors(
|
||||
index)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "../../connector/syncthingconnection.h"
|
||||
#include "../../model/syncthingdirectorymodel.h"
|
||||
#include "../../widgets/misc/textviewdialog.h"
|
||||
#include "../../widgets/misc/direrrorsdialog.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QCursor>
|
||||
|
@ -31,7 +31,7 @@ void DirView::mouseReleaseEvent(QMouseEvent *event)
|
|||
QTreeView::mouseReleaseEvent(event);
|
||||
|
||||
// get SyncthingDir object
|
||||
const SyncthingDirectoryModel *dirModel = qobject_cast<const SyncthingDirectoryModel *>(model());
|
||||
auto *const dirModel = qobject_cast<SyncthingDirectoryModel *>(model());
|
||||
if (!dirModel) {
|
||||
return;
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ void DirView::mouseReleaseEvent(QMouseEvent *event)
|
|||
if (!clickedIndex.isValid() || clickedIndex.column() != 1) {
|
||||
return;
|
||||
}
|
||||
const SyncthingDir *const dir = dirModel->dirInfo(clickedIndex);
|
||||
const auto *const dir = dirModel->dirInfo(clickedIndex);
|
||||
if (!dir) {
|
||||
return;
|
||||
}
|
||||
|
@ -60,8 +60,11 @@ void DirView::mouseReleaseEvent(QMouseEvent *event)
|
|||
} else {
|
||||
emit openDir(*dir);
|
||||
}
|
||||
} else if (clickedIndex.row() == 9 && !dir->itemErrors.empty()) {
|
||||
auto *const textViewDlg = TextViewDialog::forDirectoryErrors(*dir);
|
||||
} else if (clickedIndex.row() == 9 && dir->pullErrorCount) {
|
||||
auto &connection(*dirModel->connection());
|
||||
connection.requestDirPullErrors(dir->id);
|
||||
|
||||
auto *const textViewDlg = new DirectoryErrorsDialog(connection, *dir);
|
||||
textViewDlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
textViewDlg->show();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ set(WIDGETS_HEADER_FILES
|
|||
webview/webviewdialog.h
|
||||
misc/textviewdialog.h
|
||||
misc/internalerrorsdialog.h
|
||||
misc/direrrorsdialog.h
|
||||
misc/statusinfo.h
|
||||
misc/dbusstatusnotifier.h
|
||||
misc/internalerror.h
|
||||
|
@ -33,6 +34,7 @@ set(WIDGETS_SRC_FILES
|
|||
webview/webviewinterceptor.cpp
|
||||
misc/textviewdialog.cpp
|
||||
misc/internalerrorsdialog.cpp
|
||||
misc/direrrorsdialog.cpp
|
||||
misc/statusinfo.cpp
|
||||
misc/dbusstatusnotifier.cpp
|
||||
misc/internalerror.cpp
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
#include "./direrrorsdialog.h"
|
||||
|
||||
#include "../../connector/syncthingconnection.h"
|
||||
#include "../../connector/syncthingdir.h"
|
||||
#include "../../connector/utils.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QStringBuilder>
|
||||
#include <QTextBrowser>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
using namespace std;
|
||||
using namespace ChronoUtilities;
|
||||
using namespace Data;
|
||||
|
||||
namespace QtGui {
|
||||
|
||||
DirectoryErrorsDialog::DirectoryErrorsDialog(const Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent)
|
||||
: TextViewDialog(tr("Errors for directory %1").arg(dir.displayName()), parent)
|
||||
, m_connection(connection)
|
||||
, m_dirId(dir.id)
|
||||
{
|
||||
// add layout to show status and additional buttons
|
||||
auto *const buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->setMargin(0);
|
||||
layout()->addLayout(buttonLayout);
|
||||
|
||||
// add label for overall status
|
||||
m_statusLabel = new QLabel(this);
|
||||
QFont boldFont(m_statusLabel->font());
|
||||
boldFont.setBold(true);
|
||||
m_statusLabel->setFont(boldFont);
|
||||
buttonLayout->addWidget(m_statusLabel);
|
||||
|
||||
// add a button for removing all non-empty directories
|
||||
m_rmNonEmptyDirsButton = new QPushButton(this);
|
||||
m_rmNonEmptyDirsButton->setText(tr("Remove non-empty directories"));
|
||||
m_rmNonEmptyDirsButton->setIcon(QIcon::fromTheme(QStringLiteral("remove")));
|
||||
buttonLayout->setMargin(0);
|
||||
buttonLayout->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
|
||||
buttonLayout->addWidget(m_rmNonEmptyDirsButton);
|
||||
|
||||
// connect signals and slots
|
||||
connect(&connection, &SyncthingConnection::dirStatusChanged, this, &DirectoryErrorsDialog::handleDirStatusChanged);
|
||||
connect(&connection, &SyncthingConnection::newDirs, this, &DirectoryErrorsDialog::handleNewDirs);
|
||||
connect(m_rmNonEmptyDirsButton, &QPushButton::clicked, this, &DirectoryErrorsDialog::removeNonEmptyDirs);
|
||||
|
||||
// populate initially available errors
|
||||
updateErrors(dir);
|
||||
}
|
||||
|
||||
DirectoryErrorsDialog::~DirectoryErrorsDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void DirectoryErrorsDialog::handleDirStatusChanged(const SyncthingDir &dir)
|
||||
{
|
||||
if (dir.id == m_dirId) {
|
||||
updateErrors(dir);
|
||||
}
|
||||
}
|
||||
|
||||
void DirectoryErrorsDialog::handleNewDirs()
|
||||
{
|
||||
int index;
|
||||
if (const auto *const dir = m_connection.findDirInfo(m_dirId, index)) {
|
||||
updateErrors(*dir);
|
||||
}
|
||||
}
|
||||
|
||||
void DirectoryErrorsDialog::updateErrors(const Data::SyncthingDir &dir)
|
||||
{
|
||||
// update status
|
||||
m_statusLabel->setText(tr("%1 item(s) out-of-sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.pullErrorCount));
|
||||
m_rmNonEmptyDirsButton->setHidden(m_nonEmptyDirs.empty());
|
||||
|
||||
// clear previous errors
|
||||
auto *const textBrowser = browser();
|
||||
textBrowser->clear();
|
||||
m_nonEmptyDirs.clear();
|
||||
|
||||
// add item errors to textBrowser
|
||||
for (const SyncthingItemError &error : dir.itemErrors) {
|
||||
textBrowser->append(error.path % QChar(':') % QChar('\n') % error.message % QChar('\n'));
|
||||
if (error.message.endsWith(QStringLiteral("directory not empty"))) {
|
||||
m_nonEmptyDirs << dir.path + error.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString printDirectories(const QString &message, const QStringList &dirs)
|
||||
{
|
||||
return QStringLiteral("<p>") % message % QStringLiteral("</p><ul><li>") % dirs.join(QStringLiteral("</li><li>")) % QStringLiteral("</ul>");
|
||||
}
|
||||
|
||||
void DirectoryErrorsDialog::removeNonEmptyDirs()
|
||||
{
|
||||
int index;
|
||||
const auto *const dir = m_connection.findDirInfo(m_dirId, index);
|
||||
if (!dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString title(tr("Remove non-empty directories for folder \"%1\"").arg(dir->displayName()));
|
||||
if (QMessageBox::warning(this, title, printDirectories(tr("Do you really want to remove the following directories:"), m_nonEmptyDirs),
|
||||
QMessageBox::YesToAll, QMessageBox::NoToAll | QMessageBox::Default | QMessageBox::Escape)
|
||||
!= QMessageBox::YesToAll) {
|
||||
return;
|
||||
}
|
||||
QStringList removedDirs, failedDirs;
|
||||
for (const QString &dirPath : m_nonEmptyDirs) {
|
||||
bool ok = false;
|
||||
QDir dir(dirPath);
|
||||
if (!dir.exists() || !dir.removeRecursively()) {
|
||||
// check whether dir has already been removed by removing its parent
|
||||
for (const QString &removedDir : removedDirs) {
|
||||
if (dirPath.startsWith(removedDir)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok = true;
|
||||
}
|
||||
(ok ? removedDirs : failedDirs) << dirPath;
|
||||
}
|
||||
if (!failedDirs.isEmpty()) {
|
||||
QMessageBox::critical(this, title, printDirectories(tr("Unable to remove the following dirs:"), failedDirs));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QtGui
|
|
@ -0,0 +1,38 @@
|
|||
#ifndef SYNCTHINGWIDGETS_DIRECTORY_ERRORS_DIALOG_H
|
||||
#define SYNCTHINGWIDGETS_DIRECTORY_ERRORS_DIALOG_H
|
||||
|
||||
#include "./textviewdialog.h"
|
||||
|
||||
QT_FORWARD_DECLARE_CLASS(QLabel)
|
||||
QT_FORWARD_DECLARE_CLASS(QPushButton)
|
||||
|
||||
namespace Data {
|
||||
class SyncthingConnection;
|
||||
struct SyncthingDir;
|
||||
} // namespace Data
|
||||
|
||||
namespace QtGui {
|
||||
|
||||
class SYNCTHINGWIDGETS_EXPORT DirectoryErrorsDialog : public TextViewDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DirectoryErrorsDialog(const Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent = nullptr);
|
||||
~DirectoryErrorsDialog() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void handleDirStatusChanged(const Data::SyncthingDir &dir);
|
||||
void handleNewDirs();
|
||||
void updateErrors(const Data::SyncthingDir &dir);
|
||||
void removeNonEmptyDirs();
|
||||
|
||||
private:
|
||||
const Data::SyncthingConnection &m_connection;
|
||||
QString m_dirId;
|
||||
QStringList m_nonEmptyDirs;
|
||||
QLabel *m_statusLabel;
|
||||
QPushButton *m_rmNonEmptyDirsButton;
|
||||
};
|
||||
|
||||
} // namespace QtGui
|
||||
|
||||
#endif // SYNCTHINGWIDGETS_DIRECTORY_ERRORS_DIALOG_H
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
#include <qtutilities/misc/dialogutils.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFontDatabase>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
|
@ -20,10 +19,6 @@
|
|||
#include <QTextBrowser>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::placeholders;
|
||||
using namespace Dialogs;
|
||||
|
@ -61,84 +56,6 @@ TextViewDialog::TextViewDialog(const QString &title, QWidget *parent)
|
|||
centerWidget(this);
|
||||
}
|
||||
|
||||
QString printDirectories(const QString &message, const QStringList &dirs)
|
||||
{
|
||||
return QStringLiteral("<p>") % message % QStringLiteral("</p><ul><li>") % dirs.join(QStringLiteral("</li><li>")) % QStringLiteral("</ul>");
|
||||
}
|
||||
|
||||
TextViewDialog *TextViewDialog::forDirectoryErrors(const Data::SyncthingDir &dir)
|
||||
{
|
||||
// create TextViewDialog
|
||||
auto *const textViewDlg = new TextViewDialog(tr("Errors of %1").arg(dir.displayName()));
|
||||
auto *const browser = textViewDlg->browser();
|
||||
|
||||
// add errors to text view and find errors about non-empty directories to be removed
|
||||
QStringList nonEmptyDirs;
|
||||
for (const SyncthingItemError &error : dir.itemErrors) {
|
||||
browser->append(error.path % QChar(':') % QChar('\n') % error.message % QChar('\n'));
|
||||
if (error.message.endsWith(QStringLiteral("directory not empty"))) {
|
||||
nonEmptyDirs << dir.path + error.path;
|
||||
}
|
||||
}
|
||||
|
||||
// add layout to show status and additional buttons
|
||||
auto *const buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->setMargin(0);
|
||||
|
||||
// add label for overall status
|
||||
auto *const statusLabel = new QLabel(textViewDlg);
|
||||
statusLabel->setText(tr("%1 item(s) out-of-sync", nullptr, static_cast<int>(min<size_t>(dir.itemErrors.size(), numeric_limits<int>::max())))
|
||||
.arg(dir.itemErrors.size()));
|
||||
QFont boldFont(statusLabel->font());
|
||||
boldFont.setBold(true);
|
||||
statusLabel->setFont(boldFont);
|
||||
buttonLayout->addWidget(statusLabel);
|
||||
|
||||
// add a button for removing all non-empty directories
|
||||
if (!nonEmptyDirs.isEmpty()) {
|
||||
auto *const rmNonEmptyDirsButton = new QPushButton(textViewDlg);
|
||||
rmNonEmptyDirsButton->setText(tr("Remove non-empty directories"));
|
||||
rmNonEmptyDirsButton->setIcon(QIcon::fromTheme(QStringLiteral("remove")));
|
||||
buttonLayout->setMargin(0);
|
||||
buttonLayout->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
|
||||
buttonLayout->addWidget(rmNonEmptyDirsButton);
|
||||
|
||||
// define directory removal function
|
||||
const QString title(tr("Remove non-empty directories for folder \"%1\"").arg(dir.displayName()));
|
||||
connect(rmNonEmptyDirsButton, &QPushButton::clicked, [textViewDlg, nonEmptyDirs, title] {
|
||||
if (QMessageBox::warning(textViewDlg, title,
|
||||
printDirectories(tr("Do you really want to remove the following directories:"), nonEmptyDirs), QMessageBox::YesToAll,
|
||||
QMessageBox::NoToAll | QMessageBox::Default | QMessageBox::Escape)
|
||||
== QMessageBox::YesToAll) {
|
||||
QStringList removedDirs;
|
||||
QStringList failedDirs;
|
||||
for (const QString &dirPath : nonEmptyDirs) {
|
||||
bool ok = false;
|
||||
QDir dir(dirPath);
|
||||
if (!dir.exists() || !dir.removeRecursively()) {
|
||||
// check whether dir has already been removed by removing its parent
|
||||
for (const QString &removedDir : removedDirs) {
|
||||
if (dirPath.startsWith(removedDir)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok = true;
|
||||
}
|
||||
(ok ? removedDirs : failedDirs) << dirPath;
|
||||
}
|
||||
if (!failedDirs.isEmpty()) {
|
||||
QMessageBox::critical(textViewDlg, title, printDirectories(tr("Unable to remove the following dirs:"), failedDirs));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
textViewDlg->m_layout->addLayout(buttonLayout);
|
||||
return textViewDlg;
|
||||
}
|
||||
|
||||
TextViewDialog *TextViewDialog::forLogEntries(SyncthingConnection &connection)
|
||||
{
|
||||
auto *const dlg = new TextViewDialog(tr("Log"));
|
||||
|
|
|
@ -22,7 +22,7 @@ public:
|
|||
TextViewDialog(const QString &title = QString(), QWidget *parent = nullptr);
|
||||
|
||||
QTextBrowser *browser();
|
||||
static TextViewDialog *forDirectoryErrors(const Data::SyncthingDir &dir);
|
||||
QVBoxLayout *layout();
|
||||
static TextViewDialog *forLogEntries(Data::SyncthingConnection &connection);
|
||||
static TextViewDialog *forLogEntries(const std::vector<Data::SyncthingLogEntry> &logEntries, const QString &title = QString());
|
||||
|
||||
|
@ -43,6 +43,12 @@ inline QTextBrowser *TextViewDialog::browser()
|
|||
{
|
||||
return m_browser;
|
||||
}
|
||||
|
||||
inline QVBoxLayout *TextViewDialog::layout()
|
||||
{
|
||||
return m_layout;
|
||||
}
|
||||
|
||||
} // namespace QtGui
|
||||
|
||||
#endif // SYNCTHINGWIDGETS_TEXTVIEWDIALOG_H
|
||||
|
|
Loading…
Reference in New Issue