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>
This commit is contained in:
Matthieu Gallien 2023-04-28 12:43:04 +02:00 committed by Matthieu Gallien
parent 7b4245a3c3
commit 1491c134c3
8 changed files with 396 additions and 28 deletions

View File

@ -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>

View File

@ -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
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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.

View File

@ -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();

View File

@ -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();

View File

@ -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()) {

View File

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