userstatusselectormodel.cpp
emojimodel.h
emojimodel.cpp
+ syncconflictsmodel.h
+ syncconflictsmodel.cpp
fileactivitylistmodel.h
fileactivitylistmodel.cpp
filedetails/filedetails.h
height: 1
}
+ SyncConflictsModel {
+ id: realModel
+ }
+
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
class TestShareModel;
class ShareTestHelper;
class EndToEndTestHelper;
+class TestSyncConflictsModel;
namespace OCC {
explicit FolderMan(QObject *parent = nullptr);
friend class OCC::Application;
friend class ::TestFolderMan;
+ friend class ::TestSyncConflictsModel;
friend class ::TestCfApiShellExtensionsIPC;
friend class ::ShareTestHelper;
friend class ::EndToEndTestHelper;
#include "settingsdialog.h"
#include "theme.h"
#include "wheelhandler.h"
+#include "syncconflictsmodel.h"
#include "filedetails/filedetails.h"
#include "filedetails/shareemodel.h"
#include "filedetails/sharemodel.h"
qmlRegisterType<ShareModel>("com.nextcloud.desktopclient", 1, 0, "ShareModel");
qmlRegisterType<ShareeModel>("com.nextcloud.desktopclient", 1, 0, "ShareeModel");
qmlRegisterType<SortedShareModel>("com.nextcloud.desktopclient", 1, 0, "SortedShareModel");
+ qmlRegisterType<SyncConflictsModel>("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel");
qmlRegisterUncreatableType<UnifiedSearchResultsListModel>("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
--- /dev/null
+/*
+ * Copyright (C) 2023 by Matthieu Gallien <matthieu.gallien@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "syncconflictsmodel.h"
+#include "folderman.h"
+
+#include <QLoggingCategory>
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcSyncConflictsModel, "nextcloud.syncconflictsmodel", QtInfoMsg)
+
+SyncConflictsModel::SyncConflictsModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+int SyncConflictsModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return mData.size();
+}
+
+QVariant SyncConflictsModel::data(const QModelIndex &index, int role) const
+{
+ auto result = QVariant{};
+
+ Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid));
+
+ if (index.parent().isValid()) {
+ return result;
+ }
+
+ if (role >= static_cast<int>(SyncConflictRoles::ExistingFileName) && role <= static_cast<int>(SyncConflictRoles::ConflictPreviewUrl)) {
+ auto convertedRole = static_cast<SyncConflictRoles>(role);
+
+ switch (convertedRole) {
+ case SyncConflictRoles::ExistingFileName:
+ result = mConflictData[index.row()].mExistingFileName;
+ break;
+ case SyncConflictRoles::ExistingSize:
+ result = mConflictData[index.row()].mExistingSize;
+ break;
+ case SyncConflictRoles::ConflictSize:
+ result = mConflictData[index.row()].mConflictSize;
+ break;
+ case SyncConflictRoles::ExistingDate:
+ result = mConflictData[index.row()].mExistingDate;
+ break;
+ case SyncConflictRoles::ConflictDate:
+ result = mConflictData[index.row()].mConflictDate;
+ break;
+ case SyncConflictRoles::ExistingSelected:
+ result = mConflictData[index.row()].mExistingSelected;
+ break;
+ case SyncConflictRoles::ConflictSelected:
+ result = mConflictData[index.row()].mConflictSelected;
+ break;
+ case SyncConflictRoles::ExistingPreviewUrl:
+ result = mConflictData[index.row()].mExistingPreviewUrl;
+ break;
+ case SyncConflictRoles::ConflictPreviewUrl:
+ result = mConflictData[index.row()].mConflictPreviewUrl;
+ break;
+ }
+ }
+
+ return result;
+}
+
+QHash<int, QByteArray> SyncConflictsModel::roleNames() const
+{
+ auto result = QAbstractListModel::roleNames();
+
+ result[static_cast<int>(SyncConflictRoles::ExistingFileName)] = "existingFileName";
+ result[static_cast<int>(SyncConflictRoles::ExistingSize)] = "existingSize";
+ result[static_cast<int>(SyncConflictRoles::ConflictSize)] = "conflictSize";
+ result[static_cast<int>(SyncConflictRoles::ExistingDate)] = "existingDate";
+ result[static_cast<int>(SyncConflictRoles::ConflictDate)] = "conflictDate";
+ result[static_cast<int>(SyncConflictRoles::ExistingSelected)] = "existingSelected";
+ result[static_cast<int>(SyncConflictRoles::ConflictSelected)] = "conflictSelected";
+ result[static_cast<int>(SyncConflictRoles::ExistingPreviewUrl)] = "existingPreviewUrl";
+ result[static_cast<int>(SyncConflictRoles::ConflictPreviewUrl)] = "conflictPreviewUrl";
+
+ return result;
+}
+
+ActivityList SyncConflictsModel::conflictActivities() const
+{
+ return mData;
+}
+
+void SyncConflictsModel::setConflictActivities(ActivityList conflicts)
+{
+ if (mData == conflicts) {
+ return;
+ }
+
+ beginResetModel();
+
+ mData = conflicts;
+ emit conflictActivitiesChanged();
+
+ updateConflictsData();
+
+ endResetModel();
+}
+
+void SyncConflictsModel::updateConflictsData()
+{
+ mConflictData.clear();
+ mConflictData.reserve(mData.size());
+
+ for (const auto &oneConflict : qAsConst(mData)) {
+ if (!FolderMan::instance()) {
+ qCWarning(lcSyncConflictsModel) << "no FolderMan instance";
+ mConflictData.push_back({});
+ continue;
+ }
+ const auto folder = FolderMan::instance()->folder(oneConflict._folder);
+ if (!folder) {
+ qCWarning(lcSyncConflictsModel) << "no Folder instance for" << oneConflict._folder;
+ mConflictData.push_back({});
+ continue;
+ }
+
+ const auto conflictedRelativePath = oneConflict._file;
+ const auto dbRecord = folder->journalDb();
+ const auto baseRelativePath = dbRecord ? dbRecord->conflictFileBaseName(conflictedRelativePath.toUtf8()) : QString{};
+
+ const auto dir = QDir(folder->path());
+ const auto conflictedPath = dir.filePath(conflictedRelativePath);
+ const auto basePath = dir.filePath(baseRelativePath);
+
+ qCInfo(lcSyncConflictsModel()) << "conflictedPath" << conflictedPath << "basePath" << basePath;
+
+ const auto existingFileInfo = QFileInfo(basePath);
+ const auto conflictFileInfo = QFileInfo(conflictedPath);
+
+ const auto existingMimeType = mMimeDb.mimeTypeForFile(existingFileInfo.fileName());
+ const auto conflictMimeType = mMimeDb.mimeTypeForFile(conflictFileInfo.fileName());
+
+ auto newConflictData = ConflictInfo{
+ existingFileInfo.fileName(),
+ mLocale.formattedDataSize(existingFileInfo.size()),
+ mLocale.formattedDataSize(conflictFileInfo.size()),
+ existingFileInfo.lastModified().toString(),
+ conflictFileInfo.lastModified().toString(),
+ QIcon::hasThemeIcon(existingMimeType.iconName()) ? QUrl{} : QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"},
+ QIcon::hasThemeIcon(conflictMimeType.iconName()) ? QUrl{} : QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"},
+ false,
+ false,
+ };
+
+ mConflictData.push_back(std::move(newConflictData));
+ }
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2023 by Matthieu Gallien <matthieu.gallien@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef SYNCCONFLICTSMODEL_H
+#define SYNCCONFLICTSMODEL_H
+
+#include "tray/activitydata.h"
+
+#include <QAbstractListModel>
+#include <QMimeDatabase>
+#include <QLocale>
+
+namespace OCC {
+
+class SyncConflictsModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(OCC::ActivityList conflictActivities READ conflictActivities WRITE setConflictActivities NOTIFY conflictActivitiesChanged)
+
+ struct ConflictInfo {
+ QString mExistingFileName;
+ QString mExistingSize;
+ QString mConflictSize;
+ QString mExistingDate;
+ QString mConflictDate;
+ QUrl mExistingPreviewUrl;
+ QUrl mConflictPreviewUrl;
+ bool mExistingSelected = false;
+ bool mConflictSelected = false;
+ };
+
+public:
+ enum class SyncConflictRoles : int {
+ ExistingFileName = Qt::UserRole,
+ ExistingSize,
+ ConflictSize,
+ ExistingDate,
+ ConflictDate,
+ ExistingSelected,
+ ConflictSelected,
+ ExistingPreviewUrl,
+ ConflictPreviewUrl,
+ };
+
+ Q_ENUM(SyncConflictRoles)
+
+ explicit SyncConflictsModel(QObject *parent = nullptr);
+
+ [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+ [[nodiscard]] QHash<int,QByteArray> roleNames() const override;
+
+ [[nodiscard]] OCC::ActivityList conflictActivities() const;
+
+public slots:
+ void setConflictActivities(OCC::ActivityList conflicts);
+
+signals:
+ void conflictActivitiesChanged();
+
+private:
+ void updateConflictsData();
+
+ OCC::ActivityList mData;
+
+ QVector<ConflictInfo> mConflictData;
+
+ QMimeDatabase mMimeDb;
+
+ QLocale mLocale;
+};
+
+}
+
+#endif // SYNCCONFLICTSMODEL_H
nextcloud_add_test(SortedShareModel)
nextcloud_add_test(SecureFileDrop)
nextcloud_add_test(FileTagModel)
+nextcloud_add_test(SyncConflictsModel)
target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync)
configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY)
--- /dev/null
+/*
+ * Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "gui/syncconflictsmodel.h"
+#include "folderman.h"
+#include "accountstate.h"
+#include "configfile.h"
+#include "syncfileitem.h"
+
+#include "syncenginetestutils.h"
+#include "testhelper.h"
+
+#include <QTest>
+#include <QAbstractItemModelTester>
+#include <QSignalSpy>
+
+namespace {
+
+QStringList findConflicts(const FileInfo &dir)
+{
+ QStringList conflicts;
+ for (const auto &item : dir.children) {
+ if (item.name.contains("(conflicted copy")) {
+ conflicts.append(item.path());
+ }
+ }
+ return conflicts;
+}
+
+}
+
+using namespace OCC;
+
+class TestSyncConflictsModel : public QObject
+{
+ Q_OBJECT
+
+private:
+
+private slots:
+ void initTestCase()
+ {
+ }
+
+ void testSuccessfulFetchShares()
+ {
+ auto dir = QTemporaryDir {};
+ ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file
+
+ FolderMan fm;
+
+ auto account = Account::create();
+ auto url = QUrl{"http://example.de"};
+ auto cred = new HttpCredentialsTest("testuser", "secret");
+ account->setCredentials(cred);
+ account->setUrl(url);
+ url.setUserName(cred->user());
+
+ auto newAccountState{AccountStatePtr{ new AccountState{account}}};
+ auto folderman = FolderMan::instance();
+ QCOMPARE(folderman, &fm);
+
+ auto fakeFolder = FakeFolder{FileInfo::A12_B12_C12_S12()};
+
+ QVERIFY(folderman->addFolder(newAccountState.data(), folderDefinition(fakeFolder.localPath())));
+
+ QVERIFY(fakeFolder.syncOnce());
+
+ fakeFolder.localModifier().appendByte("A/a2");
+ fakeFolder.remoteModifier().appendByte("A/a2");
+ fakeFolder.remoteModifier().appendByte("A/a2");
+
+ QVERIFY(fakeFolder.syncOnce());
+
+ OCC::ActivityList allConflicts;
+
+ const auto conflicts = findConflicts(fakeFolder.currentLocalState().children["A"]);
+ for (const auto &conflict : conflicts) {
+ auto conflictActivity = OCC::Activity{};
+ conflictActivity._file = fakeFolder.localPath() + conflict;
+ conflictActivity._folder = fakeFolder.localPath();
+ allConflicts.push_back(std::move(conflictActivity));
+ }
+
+ SyncConflictsModel model;
+ QAbstractItemModelTester modelTester(&model);
+
+ model.setConflictActivities(allConflicts);
+
+ QCOMPARE(model.rowCount(), 1);
+ QCOMPARE(model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ExistingFileName)), QString{"a2"});
+ QCOMPARE(model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ExistingSize)), QString{"6 bytes"});
+ QCOMPARE(model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ConflictSize)), QString{"5 bytes"});
+ QVERIFY(!model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ExistingDate)).toString().isEmpty());
+ QVERIFY(!model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ConflictDate)).toString().isEmpty());
+ QCOMPARE(model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ExistingPreviewUrl)), QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"});
+ QCOMPARE(model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ConflictPreviewUrl)), QUrl{":/qt-project.org/styles/commonstyle/images/file-128.png"});
+ QCOMPARE(model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ExistingSelected)), false);
+ QCOMPARE(model.data(model.index(0), static_cast<int>(SyncConflictsModel::SyncConflictRoles::ConflictSelected)), false);
+ }
+
+};
+
+QTEST_GUILESS_MAIN(TestSyncConflictsModel)
+#include "testsyncconflictsmodel.moc"