Kaynağa Gözat

basic implementation of a dialog to resolve conflicts as a batch

will allow solving all conflicts at once

FIX #2786

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
Matthieu Gallien 2 yıl önce
ebeveyn
işleme
1491c134c3

+ 2 - 0
resources.qrc

@@ -56,5 +56,7 @@
         <file>src/gui/tray/ListItemLineAndSubline.qml</file>
         <file>src/gui/tray/TrayFoldersMenuButton.qml</file>
         <file>src/gui/tray/TrayFolderListItem.qml</file>
+        <file>src/gui/ResolveConflictsDialog.qml</file>
+        <file>src/gui/ConflictDelegate.qml</file>
     </qresource>
 </RCC>

+ 151 - 0
src/gui/ConflictDelegate.qml

@@ -0,0 +1,151 @@
+/*
+ * 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.
+ */
+
+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 conflictFileName
+    required property string existingSize
+    required property string conflictSize
+    required property string existingDate
+    required property string conflictDate
+    required property bool existingSelected
+    required property bool conflictSelected
+
+    EnforcedPlainTextLabel {
+        id: existingFileNameLabel
+
+        anchors.top: parent.top
+        anchors.left: parent.left
+
+        text: root.existingFileName
+
+        font.weight: Font.Light
+        font.pixelSize: 15
+    }
+
+    RowLayout {
+        anchors.top: existingFileNameLabel.bottom
+        anchors.bottom: parent.bottom
+        anchors.left: parent.left
+        anchors.right: parent.right
+
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+
+            Image {
+                id: existingPreview
+
+                anchors.top: parent.top
+                anchors.left: parent.left
+
+                source: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6'
+                width: 64
+                height: 64
+                sourceSize.width: 64
+                sourceSize.height: 64
+            }
+
+            ColumnLayout {
+                anchors.top: parent.top
+                anchors.bottom: parent.bottom
+                anchors.left: existingPreview.right
+                anchors.right: parent.right
+
+                CheckBox {
+                    id: selectExisting
+
+                    Layout.alignment: Layout.TopLeft
+
+                    checked: root.existingSelected
+                }
+
+                EnforcedPlainTextLabel {
+                    Layout.fillWidth: true
+
+                    text: root.existingDate
+
+                    font.pixelSize: 15
+                }
+
+                EnforcedPlainTextLabel {
+                    Layout.fillWidth: true
+
+                    text: existingSize
+
+                    font.pixelSize: 15
+                }
+            }
+        }
+
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+
+            Image {
+                id: conflictPreview
+
+                anchors.top: parent.top
+                anchors.left: parent.left
+
+                source: 'https://nextcloud.local/index.php/apps/theming/img/core/filetypes/text.svg?v=b9feb2d6'
+                width: 64
+                height: 64
+                sourceSize.width: 64
+                sourceSize.height: 64
+            }
+
+            ColumnLayout {
+                anchors.top: parent.top
+                anchors.bottom: parent.bottom
+                anchors.left: conflictPreview.right
+                anchors.right: parent.right
+
+                CheckBox {
+                    id: selectConflict
+
+                    Layout.alignment: Layout.TopLeft
+
+                    checked: root.conflictSelected
+                }
+
+                EnforcedPlainTextLabel {
+                    Layout.fillWidth: true
+
+                    text: root.conflictDate
+
+                    font.pixelSize: 15
+                }
+
+                EnforcedPlainTextLabel {
+                    Layout.fillWidth: true
+
+                    text: conflictSize
+
+                    font.pixelSize: 15
+                }
+            }
+        }
+    }
+}

+ 189 - 0
src/gui/ResolveConflictsDialog.qml

@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Window 2.15
+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"
+
+Window {
+    id: root
+
+    flags: Qt.Dialog
+    visible: true
+
+    width: 600
+    height: 800
+    minimumWidth: 600
+    minimumHeight: 800
+
+    onClosing: function() {
+        Systray.destroyDialog(root);
+    }
+
+    Component.onCompleted: {
+        Systray.forceWindowInit(root);
+        Systray.positionNotificationWindow(root);
+
+        root.show();
+        root.raise();
+        root.requestActivate();
+    }
+
+    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").arg(12)
+            font.bold: true
+            font.pixelSize: 20
+            Layout.fillWidth: true
+        }
+
+        EnforcedPlainTextLabel {
+            text: qsTr("Which files do you want to keep?")
+            font.pixelSize: 15
+            Layout.fillWidth: true
+        }
+
+        EnforcedPlainTextLabel {
+            text: qsTr("If you select both versions, the local file will have a number added to its name.")
+            font.pixelSize: 15
+            Layout.fillWidth: true
+            Layout.topMargin: -15
+        }
+
+        RowLayout {
+            Layout.fillWidth: true
+            Layout.topMargin: 15
+
+            CheckBox {
+                id: selectExisting
+
+                Layout.fillWidth: true
+                Layout.alignment: Layout.TopLeft
+
+                text: qsTr('Local version')
+
+                font.pixelSize: 15
+            }
+
+            CheckBox {
+                id: selectConflict
+
+                Layout.fillWidth: true
+                Layout.alignment: Layout.TopLeft
+
+                text: qsTr('Server version')
+
+                font.pixelSize: 15
+            }
+        }
+
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.leftMargin: 5
+            Layout.rightMargin: 5
+            color: Style.menuBorder
+            height: 1
+        }
+
+        ScrollView {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            clip: true
+
+            ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+            ListView {
+                id: conflictListView
+
+                model: DelegateModel {
+                    model: ListModel {
+                        ListElement {
+                            existingFileName: 'Text File.txt'
+                            conflictFileName: 'Text File.txt'
+                            existingSize: '2 B'
+                            conflictSize: '15 B'
+                            existingDate: '28 avril 2023 09:53'
+                            conflictDate: '28 avril 2023 09:53'
+                            existingSelected: false
+                            conflictSelected: false
+                        }
+
+                        ListElement {
+                            existingFileName: 'Text File.txt'
+                            conflictFileName: 'Text File.txt'
+                            existingSize: '2 B'
+                            conflictSize: '15 B'
+                            existingDate: '28 avril 2023 09:53'
+                            conflictDate: '28 avril 2023 09:53'
+                            existingSelected: false
+                            conflictSelected: false
+                        }
+
+                        ListElement {
+                            existingFileName: 'Text File.txt'
+                            conflictFileName: 'Text File.txt'
+                            existingSize: '2 B'
+                            conflictSize: '15 B'
+                            existingDate: '28 avril 2023 09:53'
+                            conflictDate: '28 avril 2023 09:53'
+                            existingSelected: false
+                            conflictSelected: false
+                        }
+                    }
+
+                    delegate: ConflictDelegate {
+                        width: conflictListView.contentItem.width
+                        height: 100
+                    }
+                }
+            }
+        }
+
+        DialogButtonBox {
+            Layout.fillWidth: true
+
+            standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+
+            onAccepted: function() {
+                console.log("Ok clicked")
+                Systray.destroyDialog(root)
+            }
+
+            onRejected: function() {
+                console.log("Cancel clicked")
+                Systray.destroyDialog(root)
+            }
+        }
+    }
+
+    Rectangle {
+        color: Theme.systemPalette.window
+        anchors.fill: parent
+        z: 1
+    }
+}

+ 1 - 1
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.
 

+ 15 - 0
src/gui/systray.cpp

@@ -285,6 +285,21 @@ void Systray::destroyEditFileLocallyLoadingDialog()
     _editFileLocallyLoadingDialog = nullptr;
 }
 
+void Systray::createResolveConflictsDialog()
+{
+    const auto callDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml"));
+    const QVariantMap initialProperties{};
+
+    if(callDialog->isError()) {
+        qCWarning(lcSystray) << callDialog->errorString();
+        return;
+    }
+
+    // This call dialog gets deallocated on close conditions
+    // by a call from the QML side to the destroyDialog slot
+    callDialog->createWithInitialProperties(initialProperties);
+}
+
 bool Systray::raiseDialogs()
 {
     return raiseFileDetailDialogs();

+ 1 - 0
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();
 
     void slotCurrentUserChanged();
 

+ 35 - 27
src/gui/tray/activitylistmodel.cpp

@@ -640,34 +640,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));
+        displaySingleConflictDialog(activity);
 
-        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);
         return;
     } else if (activity._syncFileItemStatus == SyncFileItem::FileNameClash) {
         triggerCaseClashAction(activity);
@@ -730,6 +704,40 @@ 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);
+
+    Systray::instance()->createResolveConflictsDialog();
+}
+
 void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex)
 {
     if (activityIndex < 0 || activityIndex >= _finalList.size()) {

+ 2 - 0
src/gui/tray/activitylistmodel.h

@@ -162,6 +162,8 @@ private:
     void insertOrRemoveDummyFetchingActivity();
     void triggerCaseClashAction(Activity activity);
 
+    void displaySingleConflictDialog(const Activity &activity);
+
     Activity _notificationIgnoredFiles;
     Activity _dummyFetchingActivities;