diff --git a/resources.qrc b/resources.qrc
index 2d7f227484ca2..fdd4bcf570e63 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -56,5 +56,8 @@
src/gui/tray/ListItemLineAndSubline.qml
src/gui/tray/TrayFoldersMenuButton.qml
src/gui/tray/TrayFolderListItem.qml
+ src/gui/ResolveConflictsDialog.qml
+ src/gui/ConflictDelegate.qml
+ src/gui/ConflictItemFileInfo.qml
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 8e67738292d26..e4af0e33a6fef 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -186,6 +186,8 @@ set(client_SRCS
userstatusselectormodel.cpp
emojimodel.h
emojimodel.cpp
+ syncconflictsmodel.h
+ syncconflictsmodel.cpp
fileactivitylistmodel.h
fileactivitylistmodel.cpp
filedetails/filedetails.h
diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml
new file mode 100644
index 0000000000000..ff6b48eb00e71
--- /dev/null
+++ b/src/gui/ConflictDelegate.qml
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 by Matthieu Gallien
+ *
+ * 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.
+ */
+
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+import Style 1.0
+import com.nextcloud.desktopclient 1.0
+import "./tray"
+
+Item {
+ id: root
+
+ required property string existingFileName
+ required property string existingSize
+ required property string conflictSize
+ required property string existingDate
+ required property string conflictDate
+ required property bool existingSelected
+ required property bool conflictSelected
+ required property url existingPreviewUrl
+ required property url conflictPreviewUrl
+ required property var model
+
+ EnforcedPlainTextLabel {
+ id: existingFileNameLabel
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+
+ text: root.existingFileName
+
+ font.weight: Font.Bold
+ font.pixelSize: Style.fontPixelSizeResolveConflictsDialog
+ }
+
+ RowLayout {
+ anchors.top: existingFileNameLabel.bottom
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottomMargin: 8
+
+ ConflictItemFileInfo {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ itemSelected: root.existingSelected
+ itemPreviewUrl: root.existingPreviewUrl
+ itemVersionLabel: qsTr('Local version')
+ itemDateLabel: root.existingDate
+ itemFileSizeLabel: root.existingSize
+
+ onSelectedChanged: function() {
+ model.existingSelected = itemSelected
+ }
+ }
+
+ ConflictItemFileInfo {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ itemSelected: root.conflictSelected
+ itemPreviewUrl: root.conflictPreviewUrl
+ itemVersionLabel: qsTr('Server version')
+ itemDateLabel: root.conflictDate
+ itemFileSizeLabel: root.conflictSize
+
+ onSelectedChanged: function() {
+ model.conflictSelected = itemSelected
+ }
+ }
+ }
+}
diff --git a/src/gui/ConflictItemFileInfo.qml b/src/gui/ConflictItemFileInfo.qml
new file mode 100644
index 0000000000000..be345f7e8a945
--- /dev/null
+++ b/src/gui/ConflictItemFileInfo.qml
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 by Matthieu Gallien
+ *
+ * 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.
+ */
+
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+import Style 1.0
+import "./tray"
+
+Item {
+ property alias itemSelected: selectItem.checked
+ property alias itemPreviewUrl: itemPreview.source
+ property alias itemVersionLabel: versionLabel.text
+ property alias itemDateLabel: dateLabel.text
+ property alias itemFileSizeLabel: fileSizeLabel.text
+
+ signal selectedChanged()
+
+ CheckBox {
+ id: selectItem
+
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+
+ leftPadding: 0
+ spacing: 0
+
+ onToggled: function() {
+ selectedChanged()
+ }
+ }
+
+ Image {
+ id: itemPreview
+
+ anchors.left: selectItem.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ width: 48
+ height: 48
+ sourceSize.width: 48
+ sourceSize.height: 48
+ }
+
+ ColumnLayout {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: itemPreview.right
+ anchors.right: parent.right
+ anchors.leftMargin: 10
+
+ spacing: 0
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ EnforcedPlainTextLabel {
+ id: versionLabel
+
+ Layout.fillWidth: true
+
+ font.pixelSize: Style.fontPixelSizeResolveConflictsDialog
+ }
+
+ EnforcedPlainTextLabel {
+ id: dateLabel
+
+ Layout.fillWidth: true
+
+ font.pixelSize: Style.fontPixelSizeResolveConflictsDialog
+ }
+
+ EnforcedPlainTextLabel {
+ id: fileSizeLabel
+
+ Layout.fillWidth: true
+
+ font.pixelSize: Style.fontPixelSizeResolveConflictsDialog
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml
new file mode 100644
index 0000000000000..99bd61da5a8ed
--- /dev/null
+++ b/src/gui/ResolveConflictsDialog.qml
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 by Matthieu Gallien
+ *
+ * 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.
+ */
+
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Window 2.15 as QtWindow
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+import QtQml.Models 2.15
+import Style 1.0
+import com.nextcloud.desktopclient 1.0
+import "./tray"
+
+QtWindow.Window {
+ id: conflictsDialog
+
+ required property var allConflicts
+
+ flags: Qt.Window | Qt.Dialog
+ visible: true
+
+ width: Style.minimumWidthResolveConflictsDialog
+ height: Style.minimumHeightResolveConflictsDialog
+ minimumWidth: Style.minimumWidthResolveConflictsDialog
+ minimumHeight: Style.minimumHeightResolveConflictsDialog
+ title: qsTr('Solve sync conflicts')
+
+ onClosing: function(close) {
+ Systray.destroyDialog(self);
+ close.accepted = true
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 20
+ anchors.rightMargin: 20
+ anchors.bottomMargin: 20
+ anchors.topMargin: 20
+ spacing: 15
+ z: 2
+
+ EnforcedPlainTextLabel {
+ text: qsTr("%1 files in conflict", 'indicate the number of conflicts to resolve', delegateModel.count).arg(delegateModel.count)
+ font.bold: true
+ font.pixelSize: Style.bigFontPixelSizeResolveConflictsDialog
+ Layout.fillWidth: true
+ }
+
+ EnforcedPlainTextLabel {
+ text: qsTr("Choose if you want to keep the local version, server version, or both. If you choose both, the local file will have a number added to its name.")
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ font.pixelSize: Style.fontPixelSizeResolveConflictsDialog
+ Layout.fillWidth: true
+ Layout.topMargin: -15
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: 15
+
+ CheckBox {
+ id: selectExisting
+
+ Layout.fillWidth: true
+
+ text: qsTr('All local versions')
+
+ leftPadding: 0
+ implicitWidth: 100
+
+ font.pixelSize: Style.fontPixelSizeResolveConflictsDialog
+
+ checked: realModel.allExistingsSelected
+ onToggled: function() {
+ realModel.selectAllExisting(checked)
+ }
+ }
+
+ CheckBox {
+ id: selectConflict
+
+ Layout.fillWidth: true
+
+ text: qsTr('All server versions')
+
+ leftPadding: 0
+ implicitWidth: 100
+
+ font.pixelSize: Style.fontPixelSizeResolveConflictsDialog
+
+ checked: realModel.allConflictingSelected
+ onToggled: function() {
+ realModel.selectAllConflicting(checked)
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.leftMargin: 5
+ Layout.rightMargin: 5
+ color: Style.menuBorder
+ height: 1
+ }
+
+ SyncConflictsModel {
+ id: realModel
+
+ conflictActivities: conflictsDialog.allConflicts
+ }
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+ ListView {
+ id: conflictListView
+
+ model: DelegateModel {
+ id: delegateModel
+
+ model: realModel
+
+ delegate: ConflictDelegate {
+ width: conflictListView.contentItem.width
+ height: 100
+ }
+ }
+ }
+ }
+
+ DialogButtonBox {
+ Layout.fillWidth: true
+
+ Button {
+ text: qsTr("Resolve conflicts")
+ DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+ }
+ Button {
+ text: qsTr("Cancel")
+ DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
+ }
+
+ onAccepted: function() {
+ realModel.applySolution()
+ Systray.destroyDialog(conflictsDialog)
+ }
+
+ onRejected: function() {
+ Systray.destroyDialog(conflictsDialog)
+ }
+ }
+ }
+
+ Rectangle {
+ color: Theme.systemPalette.window
+ anchors.fill: parent
+ z: 1
+ }
+}
diff --git a/src/gui/conflictsolver.h b/src/gui/conflictsolver.h
index b000e78dcc2f7..018741b05ec89 100644
--- a/src/gui/conflictsolver.h
+++ b/src/gui/conflictsolver.h
@@ -32,6 +32,7 @@ class ConflictSolver : public QObject
KeepRemoteVersion,
KeepBothVersions
};
+ Q_ENUM(Solution);
explicit ConflictSolver(QWidget *parent = nullptr);
diff --git a/src/gui/folderman.h b/src/gui/folderman.h
index 6b9186b72e2da..933954b173f56 100644
--- a/src/gui/folderman.h
+++ b/src/gui/folderman.h
@@ -30,6 +30,7 @@ class TestCfApiShellExtensionsIPC;
class TestShareModel;
class ShareTestHelper;
class EndToEndTestHelper;
+class TestSyncConflictsModel;
namespace OCC {
@@ -391,6 +392,7 @@ private slots:
explicit FolderMan(QObject *parent = nullptr);
friend class OCC::Application;
friend class ::TestFolderMan;
+ friend class ::TestSyncConflictsModel;
friend class ::TestCfApiShellExtensionsIPC;
friend class ::ShareTestHelper;
friend class ::EndToEndTestHelper;
diff --git a/src/gui/main.cpp b/src/gui/main.cpp
index 40af80ac019c5..a53aa160b1d54 100644
--- a/src/gui/main.cpp
+++ b/src/gui/main.cpp
@@ -66,7 +66,7 @@ int main(int argc, char **argv)
// the platformtheme plugin won't try to force qqc2-desktops-style
// anymore.
// Can be removed once the bug in qqc2-desktop-style is gone.
- QQuickStyle::setStyle("Default");
+ QQuickStyle::setStyle("Fusion");
// OpenSSL 1.1.0: No explicit initialisation or de-initialisation is necessary.
diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp
index a43363879ef23..45215d53ee333 100644
--- a/src/gui/owncloudgui.cpp
+++ b/src/gui/owncloudgui.cpp
@@ -31,6 +31,7 @@
#include "settingsdialog.h"
#include "theme.h"
#include "wheelhandler.h"
+#include "syncconflictsmodel.h"
#include "filedetails/filedetails.h"
#include "filedetails/shareemodel.h"
#include "filedetails/sharemodel.h"
@@ -125,6 +126,7 @@ ownCloudGui::ownCloudGui(Application *parent)
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ShareModel");
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ShareeModel");
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedShareModel");
+ qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel");
qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
@@ -138,6 +140,7 @@ ownCloudGui::ownCloudGui(Application *parent)
qRegisterMetaType("SharePtr");
qRegisterMetaType("ShareePtr");
qRegisterMetaType("Sharee");
+ qRegisterMetaType("ActivityList");
qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserModel", UserModel::instance());
qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserAppsModel", UserAppsModel::instance());
diff --git a/src/gui/syncconflictsmodel.cpp b/src/gui/syncconflictsmodel.cpp
new file mode 100644
index 0000000000000..3d455d8729269
--- /dev/null
+++ b/src/gui/syncconflictsmodel.cpp
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2023 by Matthieu Gallien
+ *
+ * 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
+
+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 _data.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(SyncConflictRoles::ExistingFileName) && role <= static_cast(SyncConflictRoles::ConflictPreviewUrl)) {
+ const auto convertedRole = static_cast(role);
+
+ switch (convertedRole) {
+ case SyncConflictRoles::ExistingFileName:
+ result = _conflictData[index.row()].mExistingFileName;
+ break;
+ case SyncConflictRoles::ExistingSize:
+ result = _conflictData[index.row()].mExistingSize;
+ break;
+ case SyncConflictRoles::ConflictSize:
+ result = _conflictData[index.row()].mConflictSize;
+ break;
+ case SyncConflictRoles::ExistingDate:
+ result = _conflictData[index.row()].mExistingDate;
+ break;
+ case SyncConflictRoles::ConflictDate:
+ result = _conflictData[index.row()].mConflictDate;
+ break;
+ case SyncConflictRoles::ExistingSelected:
+ result = _conflictData[index.row()].mExistingSelected == ConflictInfo::ConflictSolution::SolutionSelected;
+ break;
+ case SyncConflictRoles::ConflictSelected:
+ result = _conflictData[index.row()].mConflictSelected == ConflictInfo::ConflictSolution::SolutionSelected;
+ break;
+ case SyncConflictRoles::ExistingPreviewUrl:
+ result = _conflictData[index.row()].mExistingPreviewUrl;
+ break;
+ case SyncConflictRoles::ConflictPreviewUrl:
+ result = _conflictData[index.row()].mConflictPreviewUrl;
+ break;
+ }
+ }
+
+ return result;
+}
+
+bool SyncConflictsModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ auto result = false;
+
+ Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid));
+
+ if (index.parent().isValid()) {
+ return result;
+ }
+
+ if (role >= static_cast(SyncConflictRoles::ExistingFileName) && role <= static_cast(SyncConflictRoles::ConflictPreviewUrl)) {
+ const auto convertedRole = static_cast(role);
+
+ switch(convertedRole) {
+ case SyncConflictRoles::ExistingFileName:
+ break;
+ case SyncConflictRoles::ExistingSize:
+ break;
+ case SyncConflictRoles::ConflictSize:
+ break;
+ case SyncConflictRoles::ExistingDate:
+ break;
+ case SyncConflictRoles::ConflictDate:
+ break;
+ case SyncConflictRoles::ExistingSelected:
+ setExistingSelected(value.toBool(), index, role);
+ result = true;
+ break;
+ case SyncConflictRoles::ConflictSelected:
+ setConflictingSelected(value.toBool(), index, role);
+ result = true;
+ break;
+ case SyncConflictRoles::ExistingPreviewUrl:
+ break;
+ case SyncConflictRoles::ConflictPreviewUrl:
+ break;
+ }
+
+ result = false;
+ }
+
+ return result;
+}
+
+QHash SyncConflictsModel::roleNames() const
+{
+ auto result = QAbstractListModel::roleNames();
+
+ result[static_cast(SyncConflictRoles::ExistingFileName)] = "existingFileName";
+ result[static_cast(SyncConflictRoles::ExistingSize)] = "existingSize";
+ result[static_cast(SyncConflictRoles::ConflictSize)] = "conflictSize";
+ result[static_cast(SyncConflictRoles::ExistingDate)] = "existingDate";
+ result[static_cast(SyncConflictRoles::ConflictDate)] = "conflictDate";
+ result[static_cast(SyncConflictRoles::ExistingSelected)] = "existingSelected";
+ result[static_cast(SyncConflictRoles::ConflictSelected)] = "conflictSelected";
+ result[static_cast(SyncConflictRoles::ExistingPreviewUrl)] = "existingPreviewUrl";
+ result[static_cast(SyncConflictRoles::ConflictPreviewUrl)] = "conflictPreviewUrl";
+
+ return result;
+}
+
+Qt::ItemFlags SyncConflictsModel::flags(const QModelIndex &index) const
+{
+ auto result = Qt::ItemFlags{};
+
+ if (!index.parent().isValid()) {
+ result = QAbstractListModel::flags(index);
+ return result;
+ }
+
+ result = Qt::ItemIsSelectable | Qt::ItemIsEditable;
+ return result;
+}
+
+ActivityList SyncConflictsModel::conflictActivities() const
+{
+ return _data;
+}
+
+bool SyncConflictsModel::allExistingsSelected() const
+{
+ return _allExistingsSelected;
+}
+
+bool SyncConflictsModel::allConflictingSelected() const
+{
+ return _allConflictingsSelected;
+}
+
+void SyncConflictsModel::setConflictActivities(ActivityList conflicts)
+{
+ if (_data == conflicts) {
+ return;
+ }
+
+ beginResetModel();
+
+ _data = conflicts;
+ emit conflictActivitiesChanged();
+
+ updateConflictsData();
+
+ endResetModel();
+}
+
+void SyncConflictsModel::selectAllExisting(bool selected)
+{
+ for (auto &singleConflict : _conflictData) {
+ singleConflict.mExistingSelected = selected ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected;
+ }
+
+ Q_EMIT dataChanged(index(0), index(rowCount() - 1), {static_cast(SyncConflictRoles::ExistingSelected)});
+
+ if (selected && !_allExistingsSelected) {
+ _allExistingsSelected = true;
+ Q_EMIT allExistingsSelectedChanged();
+ } else if (!selected && _allExistingsSelected) {
+ _allExistingsSelected = false;
+ Q_EMIT allExistingsSelectedChanged();
+ }
+}
+
+void SyncConflictsModel::selectAllConflicting(bool selected)
+{
+ for (auto &singleConflict : _conflictData) {
+ singleConflict.mConflictSelected = selected ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected;
+ }
+
+ Q_EMIT dataChanged(index(0), index(rowCount() - 1), {static_cast(SyncConflictRoles::ConflictSelected)});
+
+ if (selected && !_allConflictingsSelected) {
+ _allConflictingsSelected = true;
+ Q_EMIT allConflictingSelectedChanged();
+ } else if (!selected && _allConflictingsSelected) {
+ _allConflictingsSelected = false;
+ Q_EMIT allConflictingSelectedChanged();
+ }
+}
+
+void SyncConflictsModel::applySolution()
+{
+ for(const auto &syncConflict : qAsConst(_conflictData)) {
+ if (syncConflict.isValid()) {
+ qCInfo(lcSyncConflictsModel) << syncConflict.mExistingFilePath << syncConflict.mConflictingFilePath << syncConflict.solution();
+ ConflictSolver solver;
+ solver.setLocalVersionFilename(syncConflict.mConflictingFilePath);
+ solver.setRemoteVersionFilename(syncConflict.mExistingFilePath);
+ solver.exec(syncConflict.solution());
+ }
+ }
+}
+
+void SyncConflictsModel::updateConflictsData()
+{
+ _conflictData.clear();
+ _conflictData.reserve(_data.size());
+
+ for (const auto &oneConflict : qAsConst(_data)) {
+ const auto folder = FolderMan::instance()->folder(oneConflict._folder);
+ if (!folder) {
+ qCWarning(lcSyncConflictsModel) << "no Folder instance for" << oneConflict._folder;
+ _conflictData.push_back({});
+ continue;
+ }
+
+ const auto conflictedRelativePath = oneConflict._file;
+ const auto baseRelativePath = folder->journalDb() ? folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8()) : QString{};
+
+ const auto dir = QDir(folder->path());
+ const auto conflictedPath = dir.filePath(conflictedRelativePath);
+ const auto basePath = dir.filePath(baseRelativePath);
+
+ const auto existingFileInfo = QFileInfo(basePath);
+ const auto conflictFileInfo = QFileInfo(conflictedPath);
+
+ auto newConflictData = ConflictInfo{
+ existingFileInfo.fileName(),
+ _locale.formattedDataSize(existingFileInfo.size()),
+ _locale.formattedDataSize(conflictFileInfo.size()),
+ existingFileInfo.lastModified().toString(),
+ conflictFileInfo.lastModified().toString(),
+ QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + existingFileInfo.filePath()},
+ QUrl{QStringLiteral("image://tray-image-provider/:/fileicon") + conflictFileInfo.filePath()},
+ ConflictInfo::ConflictSolution::SolutionDeselected,
+ ConflictInfo::ConflictSolution::SolutionDeselected,
+ existingFileInfo.filePath(),
+ conflictFileInfo.filePath(),
+ };
+
+ _conflictData.push_back(std::move(newConflictData));
+ }
+}
+
+void SyncConflictsModel::setExistingSelected(bool value,
+ const QModelIndex &index,
+ int role)
+{
+ _conflictData[index.row()].mExistingSelected = value ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected;
+ Q_EMIT dataChanged(index, index, {role});
+
+ if (_conflictData[index.row()].mExistingSelected == ConflictInfo::ConflictSolution::SolutionDeselected && _allExistingsSelected) {
+ _allExistingsSelected = false;
+ Q_EMIT allExistingsSelectedChanged();
+ } else if (_conflictData[index.row()].mExistingSelected == ConflictInfo::ConflictSolution::SolutionSelected && !_allExistingsSelected) {
+ const auto deselectedConflictIt = std::find_if(_conflictData.constBegin(), _conflictData.constEnd(), [] (const auto singleConflict) {
+ return singleConflict.mExistingSelected == ConflictInfo::ConflictSolution::SolutionDeselected;
+ });
+ const auto allSelected = (deselectedConflictIt == _conflictData.constEnd());
+ if (allSelected) {
+ _allExistingsSelected = true;
+ Q_EMIT allExistingsSelectedChanged();
+ }
+ }
+}
+
+void SyncConflictsModel::setConflictingSelected(bool value,
+ const QModelIndex &index,
+ int role)
+{
+ _conflictData[index.row()].mConflictSelected = value ? ConflictInfo::ConflictSolution::SolutionSelected : ConflictInfo::ConflictSolution::SolutionDeselected;
+ Q_EMIT dataChanged(index, index, {role});
+
+ if (_conflictData[index.row()].mConflictSelected == ConflictInfo::ConflictSolution::SolutionDeselected && _allConflictingsSelected) {
+ _allConflictingsSelected = false;
+ Q_EMIT allConflictingSelectedChanged();
+ } else if (_conflictData[index.row()].mConflictSelected == ConflictInfo::ConflictSolution::SolutionSelected && !_allConflictingsSelected) {
+ const auto deselectedConflictIt = std::find_if(_conflictData.constBegin(), _conflictData.constEnd(), [] (const auto singleConflict) {
+ return singleConflict.mConflictSelected == ConflictInfo::ConflictSolution::SolutionDeselected;
+ });
+ const auto allSelected = (deselectedConflictIt == _conflictData.constEnd());
+ if (allSelected) {
+ _allConflictingsSelected = true;
+ Q_EMIT allConflictingSelectedChanged();
+ }
+ }
+}
+
+ConflictSolver::Solution SyncConflictsModel::ConflictInfo::solution() const
+{
+ auto result = ConflictSolver::Solution{};
+
+ if (mConflictSelected == ConflictSolution::SolutionSelected && mExistingSelected == ConflictSolution::SolutionSelected) {
+ result = ConflictSolver::KeepBothVersions;
+ } else if (mConflictSelected == ConflictSolution::SolutionDeselected && mExistingSelected == ConflictSolution::SolutionSelected) {
+ result = ConflictSolver::KeepLocalVersion;
+ } else if (mConflictSelected == ConflictSolution::SolutionSelected && mExistingSelected == ConflictSolution::SolutionDeselected) {
+ result = ConflictSolver::KeepRemoteVersion;
+ }
+
+ return result;
+}
+
+bool SyncConflictsModel::ConflictInfo::isValid() const
+{
+ return mConflictSelected == ConflictInfo::ConflictSolution::SolutionSelected || mExistingSelected == ConflictInfo::ConflictSolution::SolutionSelected;
+}
+
+}
diff --git a/src/gui/syncconflictsmodel.h b/src/gui/syncconflictsmodel.h
new file mode 100644
index 0000000000000..af7d8a73eb845
--- /dev/null
+++ b/src/gui/syncconflictsmodel.h
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 by Matthieu Gallien
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include "tray/activitydata.h"
+
+#include "conflictsolver.h"
+
+#include
+#include
+#include
+
+namespace OCC {
+
+class SyncConflictsModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(OCC::ActivityList conflictActivities READ conflictActivities WRITE setConflictActivities NOTIFY conflictActivitiesChanged)
+
+ Q_PROPERTY(bool allExistingsSelected READ allExistingsSelected NOTIFY allExistingsSelectedChanged)
+
+ Q_PROPERTY(bool allConflictingSelected READ allConflictingSelected NOTIFY allConflictingSelectedChanged)
+
+ struct ConflictInfo {
+ enum class ConflictSolution : bool{
+ SolutionSelected = true,
+ SolutionDeselected = false,
+ };
+
+
+ QString mExistingFileName;
+ QString mExistingSize;
+ QString mConflictSize;
+ QString mExistingDate;
+ QString mConflictDate;
+ QUrl mExistingPreviewUrl;
+ QUrl mConflictPreviewUrl;
+ ConflictSolution mExistingSelected = ConflictSolution::SolutionDeselected;
+ ConflictSolution mConflictSelected = ConflictSolution::SolutionDeselected;
+ QString mExistingFilePath;
+ QString mConflictingFilePath;
+
+ [[nodiscard]] ConflictSolver::Solution solution() const;
+ [[nodiscard]] bool isValid() const;
+ };
+
+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]] bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+
+ [[nodiscard]] QHash roleNames() const override;
+
+ [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ [[nodiscard]] OCC::ActivityList conflictActivities() const;
+
+ [[nodiscard]] bool allExistingsSelected() const;
+
+ [[nodiscard]] bool allConflictingSelected() const;
+
+public slots:
+ void setConflictActivities(OCC::ActivityList conflicts);
+
+ void selectAllExisting(bool selected);
+
+ void selectAllConflicting(bool selected);
+
+ void applySolution();
+
+signals:
+ void conflictActivitiesChanged();
+
+ void allExistingsSelectedChanged();
+
+ void allConflictingSelectedChanged();
+
+private:
+ void updateConflictsData();
+
+ void setExistingSelected(bool value,
+ const QModelIndex &index,
+ int role);
+
+ void setConflictingSelected(bool value,
+ const QModelIndex &index,
+ int role);
+
+ OCC::ActivityList _data;
+
+ QVector _conflictData;
+
+ QLocale _locale;
+
+ bool _allExistingsSelected = false;
+
+ bool _allConflictingsSelected = false;
+};
+
+}
diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp
index c45b870b09cb9..08872c28925fc 100644
--- a/src/gui/systray.cpp
+++ b/src/gui/systray.cpp
@@ -285,6 +285,36 @@ void Systray::destroyEditFileLocallyLoadingDialog()
_editFileLocallyLoadingDialog = nullptr;
}
+void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts)
+{
+ const auto conflictsDialog = std::make_unique(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml"));
+ const QVariantMap initialProperties{
+ {"allConflicts", QVariant::fromValue(allConflicts)},
+ };
+
+ if(conflictsDialog->isError()) {
+ qCWarning(lcSystray) << conflictsDialog->errorString();
+ return;
+ }
+
+ // This call dialog gets deallocated on close conditions
+ // by a call from the QML side to the destroyDialog slot
+ auto dialog = QScopedPointer(conflictsDialog->createWithInitialProperties(initialProperties));
+ if (!dialog) {
+ return;
+ }
+ dialog->setParent(QGuiApplication::instance());
+
+ auto dialogWindow = qobject_cast(dialog.data());
+ if (!dialogWindow) {
+ return;
+ }
+ dialogWindow->show();
+ dialogWindow->raise();
+ dialogWindow->requestActivate();
+ dialog.take();
+}
+
bool Systray::raiseDialogs()
{
return raiseFileDetailDialogs();
diff --git a/src/gui/systray.h b/src/gui/systray.h
index fdc9861752b48..21607233e6ed3 100644
--- a/src/gui/systray.h
+++ b/src/gui/systray.h
@@ -121,6 +121,7 @@ public slots:
void createCallDialog(const OCC::Activity &callNotification, const OCC::AccountStatePtr accountState);
void createEditFileLocallyLoadingDialog(const QString &fileName);
void destroyEditFileLocallyLoadingDialog();
+ void createResolveConflictsDialog(const OCC::ActivityList &allConflicts);
void slotCurrentUserChanged();
diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml
index a1dc370c8555a..40167322c8968 100644
--- a/src/gui/tray/SyncStatus.qml
+++ b/src/gui/tray/SyncStatus.qml
@@ -121,7 +121,8 @@ RowLayout {
contentsFont.bold: true
bgColor: Style.currentUserHeaderColor
- visible: !syncStatus.syncing &&
+ visible: !activityModel.hasSyncConflicts &&
+ !syncStatus.syncing &&
NC.UserModel.currentUser.hasLocalFolder &&
NC.UserModel.currentUser.isConnected
enabled: visible
@@ -131,4 +132,25 @@ RowLayout {
}
}
}
+
+ CustomButton {
+ Layout.preferredWidth: syncNowFm.boundingRect(text).width +
+ leftPadding +
+ rightPadding +
+ Style.standardSpacing * 2
+ Layout.rightMargin: Style.trayHorizontalMargin
+
+ text: qsTr("Resolve conflicts")
+ textColor: Style.adjustedCurrentUserHeaderColor
+ textColorHovered: Style.currentUserHeaderTextColor
+ contentsFont.bold: true
+ bgColor: Style.currentUserHeaderColor
+
+ visible: activityModel.hasSyncConflicts &&
+ !syncStatus.syncing &&
+ NC.UserModel.currentUser.hasLocalFolder &&
+ NC.UserModel.currentUser.isConnected
+ enabled: visible
+ onClicked: NC.Systray.createResolveConflictsDialog(activityModel.allConflicts);
+ }
}
diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h
index c4e8947240a70..36671e4739679 100644
--- a/src/gui/tray/activitydata.h
+++ b/src/gui/tray/activitydata.h
@@ -178,6 +178,7 @@ using ActivityList = QList;
}
Q_DECLARE_METATYPE(OCC::Activity)
+Q_DECLARE_METATYPE(OCC::ActivityList)
Q_DECLARE_METATYPE(OCC::Activity::Type)
Q_DECLARE_METATYPE(OCC::ActivityLink)
Q_DECLARE_METATYPE(OCC::PreviewData)
diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp
index b820ed941c2b1..7b152a9842b04 100644
--- a/src/gui/tray/activitylistmodel.cpp
+++ b/src/gui/tray/activitylistmodel.cpp
@@ -32,7 +32,7 @@
#include
#include
#include
-#include
+#include
namespace OCC {
@@ -428,6 +428,8 @@ void ActivityListModel::startFetchJob()
void ActivityListModel::setFinalList(const ActivityList &finalList)
{
_finalList = finalList;
+
+ emit allConflictsChanged();
}
const ActivityList &ActivityListModel::finalList() const
@@ -548,6 +550,13 @@ void ActivityListModel::addEntriesToActivityList(const ActivityList &activityLis
_finalList.append(activity);
}
endInsertRows();
+
+ const auto deselectedConflictIt = std::find_if(_finalList.constBegin(), _finalList.constEnd(), [] (const auto activity) {
+ return activity._syncFileItemStatus == SyncFileItem::Conflict;
+ });
+ const auto conflictsFound = (deselectedConflictIt != _finalList.constEnd());
+
+ setHasSyncConflicts(conflictsFound);
}
void ActivityListModel::addErrorToActivityList(const Activity &activity)
@@ -640,34 +649,8 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
const auto activity = _finalList.at(activityIndex);
if (activity._syncFileItemStatus == SyncFileItem::Conflict) {
- Q_ASSERT(!activity._file.isEmpty());
- Q_ASSERT(!activity._folder.isEmpty());
- Q_ASSERT(Utility::isConflictFile(activity._file));
-
- const auto folder = FolderMan::instance()->folder(activity._folder);
+ displaySingleConflictDialog(activity);
- const auto conflictedRelativePath = activity._file;
- const auto baseRelativePath = folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8());
-
- const auto dir = QDir(folder->path());
- const auto conflictedPath = dir.filePath(conflictedRelativePath);
- const auto basePath = dir.filePath(baseRelativePath);
-
- const auto baseName = QFileInfo(basePath).fileName();
-
- if (!_currentConflictDialog.isNull()) {
- _currentConflictDialog->close();
- }
- _currentConflictDialog = new ConflictDialog;
- _currentConflictDialog->setBaseFilename(baseName);
- _currentConflictDialog->setLocalVersionFilename(conflictedPath);
- _currentConflictDialog->setRemoteVersionFilename(basePath);
- _currentConflictDialog->setAttribute(Qt::WA_DeleteOnClose);
- connect(_currentConflictDialog, &ConflictDialog::accepted, folder, [folder]() {
- folder->scheduleThisFolderSoon();
- });
- _currentConflictDialog->open();
- ownCloudGui::raiseDialog(_currentConflictDialog);
return;
} else if (activity._syncFileItemStatus == SyncFileItem::FileNameClash) {
triggerCaseClashAction(activity);
@@ -730,6 +713,46 @@ void ActivityListModel::triggerCaseClashAction(Activity activity)
ownCloudGui::raiseDialog(_currentCaseClashFilenameDialog);
}
+void ActivityListModel::displaySingleConflictDialog(const Activity &activity)
+{
+ Q_ASSERT(!activity._file.isEmpty());
+ Q_ASSERT(!activity._folder.isEmpty());
+ Q_ASSERT(Utility::isConflictFile(activity._file));
+
+ const auto folder = FolderMan::instance()->folder(activity._folder);
+
+ const auto conflictedRelativePath = activity._file;
+ const auto baseRelativePath = folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8());
+
+ const auto dir = QDir(folder->path());
+ const auto conflictedPath = dir.filePath(conflictedRelativePath);
+ const auto basePath = dir.filePath(baseRelativePath);
+
+ const auto baseName = QFileInfo(basePath).fileName();
+
+ if (!_currentConflictDialog.isNull()) {
+ _currentConflictDialog->close();
+ }
+ _currentConflictDialog = new ConflictDialog;
+ _currentConflictDialog->setBaseFilename(baseName);
+ _currentConflictDialog->setLocalVersionFilename(conflictedPath);
+ _currentConflictDialog->setRemoteVersionFilename(basePath);
+ _currentConflictDialog->setAttribute(Qt::WA_DeleteOnClose);
+ connect(_currentConflictDialog, &ConflictDialog::accepted, folder, [folder]() {
+ folder->scheduleThisFolderSoon();
+ });
+ _currentConflictDialog->open();
+ ownCloudGui::raiseDialog(_currentConflictDialog);
+}
+
+void ActivityListModel::setHasSyncConflicts(bool conflictsFound)
+{
+ if (_hasSyncConflicts != conflictsFound) {
+ _hasSyncConflicts = conflictsFound;
+ emit hasSyncConflictsChanged();
+ }
+}
+
void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex)
{
if (activityIndex < 0 || activityIndex >= _finalList.size()) {
@@ -885,4 +908,23 @@ QString ActivityListModel::replyMessageSent(const Activity &activity) const
{
return activity._talkNotificationData.messageSent;
}
+
+bool ActivityListModel::hasSyncConflicts() const
+{
+ return _hasSyncConflicts;
+}
+
+ActivityList ActivityListModel::allConflicts() const
+{
+ auto result = ActivityList{};
+
+ for(const auto &activity : _finalList) {
+ if (activity._syncFileItemStatus == SyncFileItem::Conflict) {
+ result.push_back(activity);
+ }
+ }
+
+ return result;
+}
+
}
diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h
index 3e21ebd1162f7..05e76bacefe50 100644
--- a/src/gui/tray/activitylistmodel.h
+++ b/src/gui/tray/activitylistmodel.h
@@ -42,6 +42,8 @@ class ActivityListModel : public QAbstractListModel
Q_OBJECT
Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT)
Q_PROPERTY(AccountState *accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged)
+ Q_PROPERTY(bool hasSyncConflicts READ hasSyncConflicts NOTIFY hasSyncConflictsChanged)
+ Q_PROPERTY(OCC::ActivityList allConflicts READ allConflicts NOTIFY allConflictsChanged)
public:
enum DataRole {
@@ -105,6 +107,10 @@ class ActivityListModel : public QAbstractListModel
[[nodiscard]] QString replyMessageSent(const Activity &activity) const;
+ [[nodiscard]] bool hasSyncConflicts() const;
+
+ [[nodiscard]] OCC::ActivityList allConflicts() const;
+
public slots:
void slotRefreshActivity();
void slotRefreshActivityInitial();
@@ -126,6 +132,8 @@ public slots:
signals:
void accountStateChanged();
+ void hasSyncConflictsChanged();
+ void allConflictsChanged();
void activityJobStatusCode(int statusCode);
void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
@@ -162,6 +170,9 @@ private slots:
void insertOrRemoveDummyFetchingActivity();
void triggerCaseClashAction(Activity activity);
+ void displaySingleConflictDialog(const Activity &activity);
+ void setHasSyncConflicts(bool conflictsFound);
+
Activity _notificationIgnoredFiles;
Activity _dummyFetchingActivities;
@@ -190,6 +201,8 @@ private slots:
bool _doneFetching = false;
bool _hideOldActivities = true;
+ bool _hasSyncConflicts = false;
+
static constexpr quint32 MaxActionButtons = 3;
};
}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index d6c27a1304941..2dbb5a9b7f134 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -71,6 +71,7 @@ nextcloud_add_test(ShareeModel)
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)
diff --git a/test/testsyncconflictsmodel.cpp b/test/testsyncconflictsmodel.cpp
new file mode 100644
index 0000000000000..de1596cc38e5f
--- /dev/null
+++ b/test/testsyncconflictsmodel.cpp
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) by Claudio Cambra
+ *
+ * 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
+#include
+#include
+
+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 testSettingConflicts()
+ {
+ 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(SyncConflictsModel::SyncConflictRoles::ExistingFileName)), QString{"a2"});
+ QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingSize)), QString{"6 bytes"});
+ QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictSize)), QString{"5 bytes"});
+ QVERIFY(!model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingDate)).toString().isEmpty());
+ QVERIFY(!model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictDate)).toString().isEmpty());
+ QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingPreviewUrl)), QVariant::fromValue(QUrl{QStringLiteral("image://tray-image-provider/:/fileicon%1A/a2").arg(fakeFolder.localPath())}));
+ QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictPreviewUrl)), QVariant::fromValue(QUrl{QStringLiteral("image://tray-image-provider/:/fileicon%1%2").arg(fakeFolder.localPath(), conflicts.constFirst())}));
+ QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ExistingSelected)), false);
+ QCOMPARE(model.data(model.index(0), static_cast(SyncConflictsModel::SyncConflictRoles::ConflictSelected)), false);
+ }
+};
+
+QTEST_GUILESS_MAIN(TestSyncConflictsModel)
+#include "testsyncconflictsmodel.moc"
diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml
index 21bfeb460e883..e9fbdbf2ee68c 100644
--- a/theme/Style/Style.qml
+++ b/theme/Style/Style.qml
@@ -131,6 +131,11 @@ QtObject {
readonly property var fontMetrics: FontMetrics {}
+ readonly property int bigFontPixelSizeResolveConflictsDialog: 20
+ readonly property int fontPixelSizeResolveConflictsDialog: 15
+ readonly property int minimumWidthResolveConflictsDialog: 600
+ readonly property int minimumHeightResolveConflictsDialog: 800
+
readonly property int activityContentSpace: 4
readonly property double smallIconScaleFactor: 0.6