diff --git a/connector/syncthingconnection.cpp b/connector/syncthingconnection.cpp index 1d01cc6..809920c 100644 --- a/connector/syncthingconnection.cpp +++ b/connector/syncthingconnection.cpp @@ -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); diff --git a/connector/syncthingconnection.h b/connector/syncthingconnection.h index 1a6011d..8aa157c 100644 --- a/connector/syncthingconnection.h +++ b/connector/syncthingconnection.h @@ -151,9 +151,11 @@ public: std::vector 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 &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(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(this)->findDevInfo(devId, row); +} + } // namespace Data Q_DECLARE_METATYPE(Data::SyncthingLogEntry) diff --git a/connector/syncthingconnection_requests.cpp b/connector/syncthingconnection_requests.cpp index b7ea354..75b6464 100644 --- a/connector/syncthingconnection_requests.cpp +++ b/connector/syncthingconnection_requests.cpp @@ -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(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); } diff --git a/connector/syncthingconnectionmockhelpers.cpp b/connector/syncthingconnectionmockhelpers.cpp index e644317..292abe9 100644 --- a/connector/syncthingconnectionmockhelpers.cpp +++ b/connector/syncthingconnectionmockhelpers.cpp @@ -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")) { diff --git a/connector/syncthingdir.cpp b/connector/syncthingdir.cpp index 00c013b..d3078fb 100644 --- a/connector/syncthingdir.cpp +++ b/connector/syncthingdir.cpp @@ -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; } diff --git a/connector/syncthingdir.h b/connector/syncthingdir.h index 38a9c0d..ad4fb95 100644 --- a/connector/syncthingdir.h +++ b/connector/syncthingdir.h @@ -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 completionByDevice; QString globalError; + quint64 pullErrorCount = 0; std::vector itemErrors; - std::vector previousItemErrors; std::vector recentChanges; SyncthingStatistics globalStats, localStats, neededStats; ChronoUtilities::DateTime lastStatisticsUpdate; diff --git a/fileitemactionplugin/syncthingfileitemaction.cpp b/fileitemactionplugin/syncthingfileitemaction.cpp index 7f5e518..8b25904 100644 --- a/fileitemactionplugin/syncthingfileitemaction.cpp +++ b/fileitemactionplugin/syncthingfileitemaction.cpp @@ -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; } diff --git a/model/syncthingdirectorymodel.cpp b/model/syncthingdirectorymodel.cpp index a90d0f4..1753148 100644 --- a/model/syncthingdirectorymodel.cpp +++ b/model/syncthingdirectorymodel.cpp @@ -36,6 +36,7 @@ QHash 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(dir.itemErrors.size())).arg(dir.itemErrors.size()); - } - return tr("%1 and %2 item(s) out of sync", nullptr, static_cast(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 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 modelRoles2({ Qt::DisplayRole, Qt::EditRole, Qt::ForegroundRole }); diff --git a/model/syncthingdirectorymodel.h b/model/syncthingdirectorymodel.h index baace10..a728fe1 100644 --- a/model/syncthingdirectorymodel.h +++ b/model/syncthingdirectorymodel.h @@ -21,6 +21,7 @@ public: DirectoryStatusColor, DirectoryId, DirectoryPath, + DirectoryPullErrorCount, DirectoryDetail, }; diff --git a/model/syncthingmodel.h b/model/syncthingmodel.h index 8e7183b..05733fc 100644 --- a/model/syncthingmodel.h +++ b/model/syncthingmodel.h @@ -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; diff --git a/plasmoid/lib/syncthingapplet.cpp b/plasmoid/lib/syncthingapplet.cpp index fce0060..10117a9 100644 --- a/plasmoid/lib/syncthingapplet.cpp +++ b/plasmoid/lib/syncthingapplet.cpp @@ -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) diff --git a/plasmoid/lib/syncthingapplet.h b/plasmoid/lib/syncthingapplet.h index 5f9d9ed..9755d69 100644 --- a/plasmoid/lib/syncthingapplet.h +++ b/plasmoid/lib/syncthingapplet.h @@ -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(); diff --git a/plasmoid/package/contents/ui/DirectoriesPage.qml b/plasmoid/package/contents/ui/DirectoriesPage.qml index d7468b2..d93ac4d 100644 --- a/plasmoid/package/contents/ui/DirectoriesPage.qml +++ b/plasmoid/package/contents/ui/DirectoriesPage.qml @@ -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) diff --git a/tray/gui/dirview.cpp b/tray/gui/dirview.cpp index 06a14c0..33afc66 100644 --- a/tray/gui/dirview.cpp +++ b/tray/gui/dirview.cpp @@ -3,7 +3,7 @@ #include "../../connector/syncthingconnection.h" #include "../../model/syncthingdirectorymodel.h" -#include "../../widgets/misc/textviewdialog.h" +#include "../../widgets/misc/direrrorsdialog.h" #include #include @@ -31,7 +31,7 @@ void DirView::mouseReleaseEvent(QMouseEvent *event) QTreeView::mouseReleaseEvent(event); // get SyncthingDir object - const SyncthingDirectoryModel *dirModel = qobject_cast(model()); + auto *const dirModel = qobject_cast(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(); } diff --git a/widgets/CMakeLists.txt b/widgets/CMakeLists.txt index 8f7cb09..ad65647 100644 --- a/widgets/CMakeLists.txt +++ b/widgets/CMakeLists.txt @@ -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 diff --git a/widgets/misc/direrrorsdialog.cpp b/widgets/misc/direrrorsdialog.cpp new file mode 100644 index 0000000..039d48b --- /dev/null +++ b/widgets/misc/direrrorsdialog.cpp @@ -0,0 +1,141 @@ +#include "./direrrorsdialog.h" + +#include "../../connector/syncthingconnection.h" +#include "../../connector/syncthingdir.h" +#include "../../connector/utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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("

") % message % QStringLiteral("

  • ") % dirs.join(QStringLiteral("
  • ")) % QStringLiteral("
"); +} + +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 diff --git a/widgets/misc/direrrorsdialog.h b/widgets/misc/direrrorsdialog.h new file mode 100644 index 0000000..0d16fe0 --- /dev/null +++ b/widgets/misc/direrrorsdialog.h @@ -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 diff --git a/widgets/misc/textviewdialog.cpp b/widgets/misc/textviewdialog.cpp index 824d074..d7fd3c3 100644 --- a/widgets/misc/textviewdialog.cpp +++ b/widgets/misc/textviewdialog.cpp @@ -8,7 +8,6 @@ #include -#include #include #include #include @@ -20,10 +19,6 @@ #include #include -#include -#include -#include - 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("

") % message % QStringLiteral("

  • ") % dirs.join(QStringLiteral("
  • ")) % QStringLiteral("
"); -} - -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(min(dir.itemErrors.size(), numeric_limits::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")); diff --git a/widgets/misc/textviewdialog.h b/widgets/misc/textviewdialog.h index 4470596..3a8dca1 100644 --- a/widgets/misc/textviewdialog.h +++ b/widgets/misc/textviewdialog.h @@ -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 &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