Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/desktop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudio Cambra <claudio.cambra@gmail.com>2022-10-31 20:41:51 +0300
committerGitHub <noreply@github.com>2022-10-31 20:41:51 +0300
commit038048a64d000a97ee423bda8cc7c4fb18679ed9 (patch)
treedf7a600a5349a36158f84105ac4d65e6ec3db966
parent1dbdd8853f033a7ae5111eb8f23ba1e24093fe78 (diff)
parentd79312b2f7d32af4565221aab71cf694cad917cf (diff)
Merge pull request #4929 from nextcloud/feature/qml-file-details
Add a new file details window, unify file activity and sharing
-rw-r--r--resources.qrc15
-rw-r--r--src/3rdparty/kirigami/wheelhandler.cpp696
-rw-r--r--src/3rdparty/kirigami/wheelhandler.h271
-rw-r--r--src/gui/CMakeLists.txt17
-rw-r--r--src/gui/application.cpp3
-rw-r--r--src/gui/fileactivitylistmodel.cpp43
-rw-r--r--src/gui/fileactivitylistmodel.h12
-rw-r--r--src/gui/filedetails/FileActivityView.qml33
-rw-r--r--src/gui/filedetails/FileDetailsPage.qml190
-rw-r--r--src/gui/filedetails/FileDetailsWindow.qml42
-rw-r--r--src/gui/filedetails/NCInputTextEdit.qml70
-rw-r--r--src/gui/filedetails/NCInputTextField.qml65
-rw-r--r--src/gui/filedetails/NCTabButton.qml87
-rw-r--r--src/gui/filedetails/ShareDelegate.qml816
-rw-r--r--src/gui/filedetails/ShareView.qml312
-rw-r--r--src/gui/filedetails/ShareeDelegate.qml27
-rw-r--r--src/gui/filedetails/ShareeSearchField.qml249
-rw-r--r--src/gui/filedetails/filedetails.cpp156
-rw-r--r--src/gui/filedetails/filedetails.h75
-rw-r--r--src/gui/filedetails/shareemodel.cpp278
-rw-r--r--src/gui/filedetails/shareemodel.h105
-rw-r--r--src/gui/filedetails/sharemodel.cpp1030
-rw-r--r--src/gui/filedetails/sharemodel.h213
-rw-r--r--src/gui/filedetails/sortedsharemodel.cpp114
-rw-r--r--src/gui/filedetails/sortedsharemodel.h45
-rw-r--r--src/gui/folderman.cpp1
-rw-r--r--src/gui/folderman.h3
-rw-r--r--src/gui/owncloudgui.cpp74
-rw-r--r--src/gui/owncloudgui.h9
-rw-r--r--src/gui/sharedialog.cpp494
-rw-r--r--src/gui/sharedialog.h114
-rw-r--r--src/gui/sharedialog.ui217
-rw-r--r--src/gui/sharee.cpp156
-rw-r--r--src/gui/sharee.h42
-rw-r--r--src/gui/sharelinkwidget.cpp625
-rw-r--r--src/gui/sharelinkwidget.h157
-rw-r--r--src/gui/sharelinkwidget.ui439
-rw-r--r--src/gui/sharemanager.cpp23
-rw-r--r--src/gui/sharemanager.h140
-rw-r--r--src/gui/sharepermissions.h2
-rw-r--r--src/gui/shareusergroupwidget.cpp1129
-rw-r--r--src/gui/shareusergroupwidget.h236
-rw-r--r--src/gui/shareusergroupwidget.ui154
-rw-r--r--src/gui/socketapi/socketapi.cpp14
-rw-r--r--src/gui/socketapi/socketapi.h7
-rw-r--r--src/gui/systray.cpp97
-rw-r--r--src/gui/systray.h16
-rw-r--r--src/gui/tray/ActivityItem.qml6
-rw-r--r--src/gui/tray/ActivityItemContent.qml6
-rw-r--r--src/gui/tray/ActivityList.qml15
-rw-r--r--src/gui/tray/CustomButton.qml3
-rw-r--r--src/gui/tray/FileActivityDialog.qml40
-rw-r--r--src/gui/tray/Window.qml33
-rw-r--r--src/gui/tray/activitylistmodel.cpp1
-rw-r--r--src/gui/tray/activitylistmodel.h5
-rw-r--r--src/gui/tray/asyncimageresponse.cpp23
-rw-r--r--src/gui/tray/asyncimageresponse.h2
-rw-r--r--test/CMakeLists.txt4
-rw-r--r--test/sharetestutils.cpp442
-rw-r--r--test/sharetestutils.h136
-rw-r--r--test/syncenginetestutils.cpp8
-rw-r--r--test/testhelper.cpp18
-rw-r--r--test/testhelper.h2
-rw-r--r--test/testshareemodel.cpp442
-rw-r--r--test/testsharemodel.cpp962
-rw-r--r--test/testsortedsharemodel.cpp185
-rw-r--r--theme/Style/Style.qml2
67 files changed, 7205 insertions, 4243 deletions
diff --git a/resources.qrc b/resources.qrc
index e49b6ec48..3132330e2 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -7,17 +7,24 @@
<file>src/gui/PredefinedStatusButton.qml</file>
<file>src/gui/BasicComboBox.qml</file>
<file>src/gui/ErrorBox.qml</file>
+ <file>src/gui/filedetails/FileActivityView.qml</file>
+ <file>src/gui/filedetails/FileDetailsPage.qml</file>
+ <file>src/gui/filedetails/FileDetailsWindow.qml</file>
+ <file>src/gui/filedetails/NCInputTextEdit.qml</file>
+ <file>src/gui/filedetails/NCInputTextField.qml</file>
+ <file>src/gui/filedetails/NCTabButton.qml</file>
+ <file>src/gui/filedetails/ShareeDelegate.qml</file>
+ <file>src/gui/filedetails/ShareDelegate.qml</file>
+ <file>src/gui/filedetails/ShareeSearchField.qml</file>
+ <file>src/gui/filedetails/ShareView.qml</file>
<file>src/gui/tray/Window.qml</file>
<file>src/gui/tray/UserLine.qml</file>
<file>src/gui/tray/HeaderButton.qml</file>
<file>src/gui/tray/SyncStatus.qml</file>
- <file>theme/Style/Style.qml</file>
- <file>theme/Style/qmldir</file>
<file>src/gui/tray/ActivityActionButton.qml</file>
<file>src/gui/tray/ActivityItem.qml</file>
<file>src/gui/tray/AutoSizingMenu.qml</file>
<file>src/gui/tray/ActivityList.qml</file>
- <file>src/gui/tray/FileActivityDialog.qml</file>
<file>src/gui/tray/UnifiedSearchInputContainer.qml</file>
<file>src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml</file>
<file>src/gui/tray/UnifiedSearchResultItem.qml</file>
@@ -39,5 +46,7 @@
<file>src/gui/tray/EditFileLocallyLoadingDialog.qml</file>
<file>src/gui/tray/NCBusyIndicator.qml</file>
<file>src/gui/tray/NCToolTip.qml</file>
+ <file>theme/Style/Style.qml</file>
+ <file>theme/Style/qmldir</file>
</qresource>
</RCC>
diff --git a/src/3rdparty/kirigami/wheelhandler.cpp b/src/3rdparty/kirigami/wheelhandler.cpp
index fdfcee044..9dacae4db 100644
--- a/src/3rdparty/kirigami/wheelhandler.cpp
+++ b/src/3rdparty/kirigami/wheelhandler.cpp
@@ -6,275 +6,621 @@
#include "wheelhandler.h"
#include <QWheelEvent>
-#include <QQuickItem>
-#include <QDebug>
+#include <QQuickWindow>
-class GlobalWheelFilterSingleton
-{
-public:
- GlobalWheelFilter self;
-};
-
-Q_GLOBAL_STATIC(GlobalWheelFilterSingleton, privateGlobalWheelFilterSelf)
-
-GlobalWheelFilter::GlobalWheelFilter(QObject *parent)
+KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent)
: QObject(parent)
{
}
-GlobalWheelFilter::~GlobalWheelFilter() = default;
+KirigamiWheelEvent::~KirigamiWheelEvent()
+{
+}
-GlobalWheelFilter *GlobalWheelFilter::self()
+void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event)
{
- return &privateGlobalWheelFilterSelf()->self;
+ m_x = event->position().x();
+ m_y = event->position().y();
+ m_angleDelta = event->angleDelta();
+ m_pixelDelta = event->pixelDelta();
+ m_buttons = event->buttons();
+ m_modifiers = event->modifiers();
+ m_accepted = false;
+ m_inverted = event->inverted();
}
-void GlobalWheelFilter::setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
+qreal KirigamiWheelEvent::x() const
{
- if (!m_handlersForItem.contains(handler->target())) {
- handler->target()->installEventFilter(this);
- }
- m_handlersForItem.insert(item, handler);
+ return m_x;
+}
- connect(item, &QObject::destroyed, this, [this](QObject *obj) {
- auto item = dynamic_cast<QQuickItem *>(obj);
- m_handlersForItem.remove(item);
- });
+qreal KirigamiWheelEvent::y() const
+{
+ return m_y;
+}
- connect(handler, &QObject::destroyed, this, [this](QObject *obj) {
- auto handler = dynamic_cast<WheelHandler *>(obj);
- removeItemHandlerAssociation(handler->target(), handler);
- });
+QPointF KirigamiWheelEvent::angleDelta() const
+{
+ return m_angleDelta;
}
-void GlobalWheelFilter::removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
+QPointF KirigamiWheelEvent::pixelDelta() const
{
- if (!item || !handler) {
- return;
- }
- m_handlersForItem.remove(item, handler);
- if (!m_handlersForItem.contains(item)) {
- item->removeEventFilter(this);
- }
+ return m_pixelDelta;
}
-bool GlobalWheelFilter::eventFilter(QObject *watched, QEvent *event)
+int KirigamiWheelEvent::buttons() const
{
- if (event->type() == QEvent::Wheel) {
- auto item = qobject_cast<QQuickItem *>(watched);
- if (!item || !item->isEnabled()) {
- return QObject::eventFilter(watched, event);
- }
- auto we = dynamic_cast<QWheelEvent *>(event);
- m_wheelEvent.initializeFromEvent(we);
+ return m_buttons;
+}
- bool shouldBlock = false;
- bool shouldScrollFlickable = false;
+int KirigamiWheelEvent::modifiers() const
+{
+ return m_modifiers;
+}
- for (auto *handler : m_handlersForItem.values(item)) {
- if (handler->m_blockTargetWheel) {
- shouldBlock = true;
- }
- if (handler->m_scrollFlickableTarget) {
- shouldScrollFlickable = true;
- }
- emit handler->wheel(&m_wheelEvent);
- }
+bool KirigamiWheelEvent::inverted() const
+{
+ return m_inverted;
+}
- if (shouldScrollFlickable && !m_wheelEvent.isAccepted()) {
- manageWheel(item, we);
- }
+bool KirigamiWheelEvent::isAccepted()
+{
+ return m_accepted;
+}
- if (shouldBlock) {
- return true;
- }
- }
- return QObject::eventFilter(watched, event);
+void KirigamiWheelEvent::setAccepted(bool accepted)
+{
+ m_accepted = accepted;
}
-void GlobalWheelFilter::manageWheel(QQuickItem *target, QWheelEvent *event)
+///////////////////////////////
+
+WheelFilterItem::WheelFilterItem(QQuickItem *parent)
+ : QQuickItem(parent)
{
- // Duck typing: accept everyhint that has all the properties we need
- if (target->metaObject()->indexOfProperty("contentX") == -1
- || target->metaObject()->indexOfProperty("contentY") == -1
- || target->metaObject()->indexOfProperty("contentWidth") == -1
- || target->metaObject()->indexOfProperty("contentHeight") == -1
- || target->metaObject()->indexOfProperty("topMargin") == -1
- || target->metaObject()->indexOfProperty("bottomMargin") == -1
- || target->metaObject()->indexOfProperty("leftMargin") == -1
- || target->metaObject()->indexOfProperty("rightMargin") == -1
- || target->metaObject()->indexOfProperty("originX") == -1
- || target->metaObject()->indexOfProperty("originY") == -1) {
- return;
- }
+ setEnabled(false);
+}
- qreal contentWidth = target->property("contentWidth").toReal();
- qreal contentHeight = target->property("contentHeight").toReal();
- qreal contentX = target->property("contentX").toReal();
- qreal contentY = target->property("contentY").toReal();
- qreal topMargin = target->property("topMargin").toReal();
- qreal bottomMargin = target->property("bottomMargin").toReal();
- qreal leftMargin = target->property("leftMaring").toReal();
- qreal rightMargin = target->property("rightMargin").toReal();
- qreal originX = target->property("originX").toReal();
- qreal originY = target->property("originY").toReal();
+///////////////////////////////
- // Scroll Y
- if (contentHeight > target->height()) {
+WheelHandler::WheelHandler(QObject *parent)
+ : QObject(parent)
+ , m_filterItem(new WheelFilterItem(nullptr))
+{
+ m_filterItem->installEventFilter(this);
- int y = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
+ m_wheelScrollingTimer.setSingleShot(true);
+ m_wheelScrollingTimer.setInterval(m_wheelScrollingDuration);
+ m_wheelScrollingTimer.callOnTimeout([this]() {
+ setScrolling(false);
+ });
- //if we don't have a pixeldelta, apply the configured mouse wheel lines
- if (!event->pixelDelta().y()) {
- y *= 3; // Magic copied value from Kirigami::Settings
+ connect(QGuiApplication::styleHints(), &QStyleHints::wheelScrollLinesChanged, this, [this](int scrollLines) {
+ m_defaultPixelStepSize = 20 * scrollLines;
+ if (!m_explicitVStepSize && m_verticalStepSize != m_defaultPixelStepSize) {
+ m_verticalStepSize = m_defaultPixelStepSize;
+ Q_EMIT verticalStepSizeChanged();
}
-
- // Scroll one page regardless of delta:
- if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
- if (y > 0) {
- y = target->height();
- } else if (y < 0) {
- y = -target->height();
- }
+ if (!m_explicitHStepSize && m_horizontalStepSize != m_defaultPixelStepSize) {
+ m_horizontalStepSize = m_defaultPixelStepSize;
+ Q_EMIT horizontalStepSizeChanged();
}
+ });
+}
- qreal minYExtent = topMargin - originY;
- qreal maxYExtent = target->height() - (contentHeight + bottomMargin + originY);
+WheelHandler::~WheelHandler() = default;
- target->setProperty("contentY", qMin(-maxYExtent, qMax(-minYExtent, contentY - y)));
- }
+QQuickItem *WheelHandler::target() const
+{
+ return m_flickable;
+}
- //Scroll X
- if (contentWidth > target->width()) {
+void WheelHandler::setTarget(QQuickItem *target)
+{
+ if (m_flickable == target) {
+ return;
+ }
- int x = event->pixelDelta().x() != 0 ? event->pixelDelta().x() : event->angleDelta().x() / 8;
+ if (target && !target->inherits("QQuickFlickable")) {
+ qmlWarning(this) << "target must be a QQuickFlickable";
+ return;
+ }
- // Special case: when can't scroll vertically, scroll horizontally with vertical wheel as well
- if (x == 0 && contentHeight <= target->height()) {
- x = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
- }
+ if (m_flickable) {
+ m_flickable->removeEventFilter(this);
+ disconnect(m_flickable, nullptr, m_filterItem, nullptr);
+ }
- //if we don't have a pixeldelta, apply the configured mouse wheel lines
- if (!event->pixelDelta().x()) {
- x *= 3; // Magic copied value from Kirigami::Settings
+ m_flickable = target;
+ m_filterItem->setParentItem(target);
+
+ QQuickItem *vscrollbar = nullptr;
+ QQuickItem *hscrollbar = nullptr;
+
+ if (target) {
+ target->installEventFilter(this);
+
+ // Stack WheelFilterItem over the Flickable's scrollable content
+ m_filterItem->stackAfter(target->property("contentItem").value<QQuickItem*>());
+ // Make it fill the Flickable
+ m_filterItem->setWidth(target->width());
+ m_filterItem->setHeight(target->height());
+ connect(target, &QQuickItem::widthChanged, m_filterItem, [this, target](){
+ m_filterItem->setWidth(target->width());
+ });
+ connect(target, &QQuickItem::heightChanged, m_filterItem, [this, target](){
+ m_filterItem->setHeight(target->height());
+ });
+
+ // Get ScrollBars so that we can filter them too, even if they're not in the bounds of the Flickable
+ auto targetChildren = target->children();
+ for (auto child : targetChildren) {
+ if (child->inherits("QQuickScrollBarAttached")) {
+ vscrollbar = child->property("vertical").value<QQuickItem*>();
+ hscrollbar = child->property("horizontal").value<QQuickItem*>();
+ break;
+ }
}
-
- // Scroll one page regardless of delta:
- if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
- if (x > 0) {
- x = target->width();
- } else if (x < 0) {
- x = -target->width();
+ // Check ScrollView if there are no scrollbars attached to the Flickable.
+ // We need to check if the parent inherits QQuickScrollView in case the
+ // parent is another Flickable that already has a Kirigami WheelHandler.
+ auto targetParent = target->parentItem();
+ if (targetParent && targetParent->inherits("QQuickScrollView") && !vscrollbar && !hscrollbar) {
+ auto targetParentChildren = targetParent->children();
+ for (auto child : targetParentChildren) {
+ if (child->inherits("QQuickScrollBarAttached")) {
+ vscrollbar = child->property("vertical").value<QQuickItem*>();
+ hscrollbar = child->property("horizontal").value<QQuickItem*>();
+ break;
+ }
}
}
+ }
- qreal minXExtent = leftMargin - originX;
- qreal maxXExtent = target->width() - (contentWidth + rightMargin + originX);
+ if (m_verticalScrollBar != vscrollbar) {
+ if (m_verticalScrollBar) {
+ m_verticalScrollBar->removeEventFilter(this);
+ }
+ m_verticalScrollBar = vscrollbar;
+ if (vscrollbar) {
+ vscrollbar->installEventFilter(this);
+ }
+ }
- target->setProperty("contentX", qMin(-maxXExtent, qMax(-minXExtent, contentX - x)));
+ if (m_horizontalScrollBar != hscrollbar) {
+ if (m_horizontalScrollBar) {
+ m_horizontalScrollBar->removeEventFilter(this);
+ }
+ m_horizontalScrollBar = hscrollbar;
+ if (hscrollbar) {
+ hscrollbar->installEventFilter(this);
+ }
}
- //this is just for making the scrollbar
- target->metaObject()->invokeMethod(target, "flick", Q_ARG(double, 0), Q_ARG(double, 1));
- target->metaObject()->invokeMethod(target, "cancelFlick");
+ Q_EMIT targetChanged();
}
+qreal WheelHandler::verticalStepSize() const
+{
+ return m_verticalStepSize;
+}
-////////////////////////////
-KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent)
- : QObject(parent)
-{}
+void WheelHandler::setVerticalStepSize(qreal stepSize)
+{
+ m_explicitVStepSize = true;
+ if (qFuzzyCompare(m_verticalStepSize, stepSize)) {
+ return;
+ }
+ // Mimic the behavior of QQuickScrollBar when stepSize is 0
+ if (qFuzzyIsNull(stepSize)) {
+ resetVerticalStepSize();
+ return;
+ }
+ m_verticalStepSize = stepSize;
+ Q_EMIT verticalStepSizeChanged();
+}
-KirigamiWheelEvent::~KirigamiWheelEvent() = default;
+void WheelHandler::resetVerticalStepSize()
+{
+ m_explicitVStepSize = false;
+ if (qFuzzyCompare(m_verticalStepSize, m_defaultPixelStepSize)) {
+ return;
+ }
+ m_verticalStepSize = m_defaultPixelStepSize;
+ Q_EMIT verticalStepSizeChanged();
+}
-void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event)
+qreal WheelHandler::horizontalStepSize() const
{
- m_x = event->position().x();
- m_y = event->position().y();
- m_angleDelta = event->angleDelta();
- m_pixelDelta = event->pixelDelta();
- m_buttons = event->buttons();
- m_modifiers = event->modifiers();
- m_accepted = false;
- m_inverted = event->inverted();
+ return m_horizontalStepSize;
}
-qreal KirigamiWheelEvent::x() const
+void WheelHandler::setHorizontalStepSize(qreal stepSize)
{
- return m_x;
+ m_explicitHStepSize = true;
+ if (qFuzzyCompare(m_horizontalStepSize, stepSize)) {
+ return;
+ }
+ // Mimic the behavior of QQuickScrollBar when stepSize is 0
+ if (qFuzzyIsNull(stepSize)) {
+ resetHorizontalStepSize();
+ return;
+ }
+ m_horizontalStepSize = stepSize;
+ Q_EMIT horizontalStepSizeChanged();
}
-qreal KirigamiWheelEvent::y() const
+void WheelHandler::resetHorizontalStepSize()
{
- return m_y;
+ m_explicitHStepSize = false;
+ if (qFuzzyCompare(m_horizontalStepSize, m_defaultPixelStepSize)) {
+ return;
+ }
+ m_horizontalStepSize = m_defaultPixelStepSize;
+ Q_EMIT horizontalStepSizeChanged();
}
-QPointF KirigamiWheelEvent::angleDelta() const
+Qt::KeyboardModifiers WheelHandler::pageScrollModifiers() const
{
- return m_angleDelta;
+ return m_pageScrollModifiers;
}
-QPointF KirigamiWheelEvent::pixelDelta() const
+void WheelHandler::setPageScrollModifiers(Qt::KeyboardModifiers modifiers)
{
- return m_pixelDelta;
+ if (m_pageScrollModifiers == modifiers) {
+ return;
+ }
+ m_pageScrollModifiers = modifiers;
+ Q_EMIT pageScrollModifiersChanged();
}
-int KirigamiWheelEvent::buttons() const
+void WheelHandler::resetPageScrollModifiers()
{
- return m_buttons;
+ setPageScrollModifiers(m_defaultPageScrollModifiers);
}
-int KirigamiWheelEvent::modifiers() const
+bool WheelHandler::filterMouseEvents() const
{
- return m_modifiers;
+ return m_filterMouseEvents;
}
-bool KirigamiWheelEvent::inverted() const
+void WheelHandler::setFilterMouseEvents(bool enabled)
{
- return m_inverted;
+ if (m_filterMouseEvents == enabled) {
+ return;
+ }
+ m_filterMouseEvents = enabled;
+ Q_EMIT filterMouseEventsChanged();
}
-bool KirigamiWheelEvent::isAccepted()
+bool WheelHandler::keyNavigationEnabled() const
{
- return m_accepted;
+ return m_keyNavigationEnabled;
}
-void KirigamiWheelEvent::setAccepted(bool accepted)
+void WheelHandler::setKeyNavigationEnabled(bool enabled)
{
- m_accepted = accepted;
+ if (m_keyNavigationEnabled == enabled) {
+ return;
+ }
+ m_keyNavigationEnabled = enabled;
+ Q_EMIT keyNavigationEnabledChanged();
}
+void WheelHandler::setScrolling(bool scrolling)
+{
+ if (m_wheelScrolling == scrolling) {
+ if (m_wheelScrolling) {
+ m_wheelScrollingTimer.start();
+ }
+ return;
+ }
+ m_wheelScrolling = scrolling;
+ m_filterItem->setEnabled(m_wheelScrolling);
+}
-///////////////////////////////
+bool WheelHandler::scrollFlickable(QPointF pixelDelta, QPointF angleDelta, Qt::KeyboardModifiers modifiers)
+{
+ if (!m_flickable || (pixelDelta.isNull() && angleDelta.isNull())) {
+ return false;
+ }
-WheelHandler::WheelHandler(QObject *parent)
- : QObject(parent)
+ const qreal width = m_flickable->width();
+ const qreal height = m_flickable->height();
+ const qreal contentWidth = m_flickable->property("contentWidth").toReal();
+ const qreal contentHeight = m_flickable->property("contentHeight").toReal();
+ const qreal contentX = m_flickable->property("contentX").toReal();
+ const qreal contentY = m_flickable->property("contentY").toReal();
+ const qreal topMargin = m_flickable->property("topMargin").toReal();
+ const qreal bottomMargin = m_flickable->property("bottomMargin").toReal();
+ const qreal leftMargin = m_flickable->property("leftMargin").toReal();
+ const qreal rightMargin = m_flickable->property("rightMargin").toReal();
+ const qreal originX = m_flickable->property("originX").toReal();
+ const qreal originY = m_flickable->property("originY").toReal();
+ const qreal pageWidth = width - leftMargin - rightMargin;
+ const qreal pageHeight = height - topMargin - bottomMargin;
+ const auto window = m_flickable->window();
+ const qreal devicePixelRatio = window != nullptr ? window->devicePixelRatio() : qGuiApp->devicePixelRatio();
+
+ // HACK: Only transpose deltas when not using xcb in order to not conflict with xcb's own delta transposing
+ if (modifiers & m_defaultHorizontalScrollModifiers && qGuiApp->platformName() != QLatin1String("xcb")) {
+ angleDelta = angleDelta.transposed();
+ pixelDelta = pixelDelta.transposed();
+ }
+
+ const qreal xTicks = angleDelta.x() / 120;
+ const qreal yTicks = angleDelta.y() / 120;
+ qreal xChange;
+ qreal yChange;
+ bool scrolled = false;
+
+ // Scroll X
+ if (contentWidth > pageWidth) {
+ // Use page size with pageScrollModifiers. Matches QScrollBar, which uses QAbstractSlider behavior.
+ if (modifiers & m_pageScrollModifiers) {
+ xChange = qBound(-pageWidth, xTicks * pageWidth, pageWidth);
+ } else if (pixelDelta.x() != 0) {
+ xChange = pixelDelta.x();
+ } else {
+ xChange = xTicks * m_horizontalStepSize;
+ }
+
+ // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs
+
+ qreal minXExtent = leftMargin - originX;
+ qreal maxXExtent = width - (contentWidth + rightMargin + originX);
+
+ qreal newContentX = qBound(-minXExtent, contentX - xChange, -maxXExtent);
+ // Flickable::pixelAligned rounds the position, so round to mimic that behavior.
+ // Rounding prevents fractional positioning from causing text to be
+ // clipped off on the top and bottom.
+ // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio
+ // after to make position match pixels on the screen more closely.
+ newContentX = std::round(newContentX * devicePixelRatio) / devicePixelRatio;
+ if (contentX != newContentX) {
+ scrolled = true;
+ m_flickable->setProperty("contentX", newContentX);
+ }
+ }
+
+ // Scroll Y
+ if (contentHeight > pageHeight) {
+ if (modifiers & m_pageScrollModifiers) {
+ yChange = qBound(-pageHeight, yTicks * pageHeight, pageHeight);
+ } else if (pixelDelta.y() != 0) {
+ yChange = pixelDelta.y();
+ } else {
+ yChange = yTicks * m_verticalStepSize;
+ }
+
+ // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs
+
+ qreal minYExtent = topMargin - originY;
+ qreal maxYExtent = height - (contentHeight + bottomMargin + originY);
+
+ qreal newContentY = qBound(-minYExtent, contentY - yChange, -maxYExtent);
+ // Flickable::pixelAligned rounds the position, so round to mimic that behavior.
+ // Rounding prevents fractional positioning from causing text to be
+ // clipped off on the top and bottom.
+ // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio
+ // after to make position match pixels on the screen more closely.
+ newContentY = std::round(newContentY * devicePixelRatio) / devicePixelRatio;
+ if (contentY != newContentY) {
+ scrolled = true;
+ m_flickable->setProperty("contentY", newContentY);
+ }
+ }
+
+ return scrolled;
+}
+
+bool WheelHandler::scrollUp(qreal stepSize)
{
+ if (qFuzzyIsNull(stepSize)) {
+ return false;
+ } else if (stepSize < 0) {
+ stepSize = m_verticalStepSize;
+ }
+ // contentY uses reversed sign
+ return scrollFlickable(QPointF(0, stepSize));
}
-WheelHandler::~WheelHandler() = default;
+bool WheelHandler::scrollDown(qreal stepSize)
+{
+ if (qFuzzyIsNull(stepSize)) {
+ return false;
+ } else if (stepSize < 0) {
+ stepSize = m_verticalStepSize;
+ }
+ // contentY uses reversed sign
+ return scrollFlickable(QPointF(0, -stepSize));
+}
-QQuickItem *WheelHandler::target() const
+bool WheelHandler::scrollLeft(qreal stepSize)
{
- return m_target;
+ if (qFuzzyIsNull(stepSize)) {
+ return false;
+ } else if (stepSize < 0) {
+ stepSize = m_horizontalStepSize;
+ }
+ // contentX uses reversed sign
+ return scrollFlickable(QPoint(stepSize, 0));
}
-void WheelHandler::setTarget(QQuickItem *target)
+bool WheelHandler::scrollRight(qreal stepSize)
{
- if (m_target == target) {
- return;
+ if (qFuzzyIsNull(stepSize)) {
+ return false;
+ } else if (stepSize < 0) {
+ stepSize = m_horizontalStepSize;
}
+ // contentX uses reversed sign
+ return scrollFlickable(QPoint(-stepSize, 0));
+}
- if (m_target) {
- GlobalWheelFilter::self()->removeItemHandlerAssociation(m_target, this);
+bool WheelHandler::eventFilter(QObject *watched, QEvent *event)
+{
+ auto item = qobject_cast<QQuickItem*>(watched);
+ if (!item || !item->isEnabled()) {
+ return false;
}
- m_target = target;
+ qreal contentWidth = 0;
+ qreal contentHeight = 0;
+ qreal pageWidth = 0;
+ qreal pageHeight = 0;
+ if (m_flickable) {
+ contentWidth = m_flickable->property("contentWidth").toReal();
+ contentHeight = m_flickable->property("contentHeight").toReal();
+ pageWidth = m_flickable->width() - m_flickable->property("leftMargin").toReal() - m_flickable->property("rightMargin").toReal();
+ pageHeight = m_flickable->height() - m_flickable->property("topMargin").toReal() - m_flickable->property("bottomMargin").toReal();
+ }
- GlobalWheelFilter::self()->setItemHandlerAssociation(target, this);
+ // The code handling touch, mouse and hover events is mostly copied/adapted from QQuickScrollView::childMouseEventFilter()
+ switch (event->type()) {
+ case QEvent::Wheel: {
+ // QQuickScrollBar::interactive handling Matches behavior in QQuickScrollView::eventFilter()
+ if (m_filterMouseEvents) {
+ if (m_verticalScrollBar) {
+ m_verticalScrollBar->setProperty("interactive", true);
+ }
+ if (m_horizontalScrollBar) {
+ m_horizontalScrollBar->setProperty("interactive", true);
+ }
+ }
+ QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
+
+ // NOTE: On X11 with libinput, pixelDelta is identical to angleDelta when using a mouse that shouldn't use pixelDelta.
+ // If faulty pixelDelta, reset pixelDelta to (0,0).
+ if (wheelEvent->pixelDelta() == wheelEvent->angleDelta()) {
+ // In order to change any of the data, we have to create a whole new QWheelEvent from its constructor.
+ QWheelEvent newWheelEvent(
+ wheelEvent->position(),
+ wheelEvent->globalPosition(),
+ QPoint(0,0), // pixelDelta
+ wheelEvent->angleDelta(),
+ wheelEvent->buttons(),
+ wheelEvent->modifiers(),
+ wheelEvent->phase(),
+ wheelEvent->inverted(),
+ wheelEvent->source()
+ );
+ m_kirigamiWheelEvent.initializeFromEvent(&newWheelEvent);
+ } else {
+ m_kirigamiWheelEvent.initializeFromEvent(wheelEvent);
+ }
- emit targetChanged();
-}
+ Q_EMIT wheel(&m_kirigamiWheelEvent);
+
+ if (m_kirigamiWheelEvent.isAccepted()) {
+ return true;
+ }
+
+ bool scrolled = false;
+ if (m_scrollFlickableTarget || (contentHeight <= pageHeight && contentWidth <= pageWidth)) {
+ // Don't use pixelDelta from the event unless angleDelta is not available
+ // because scrolling by pixelDelta is too slow on Wayland with libinput.
+ QPointF pixelDelta = m_kirigamiWheelEvent.angleDelta().isNull() ? m_kirigamiWheelEvent.pixelDelta() : QPoint(0, 0);
+ scrolled = scrollFlickable(pixelDelta,
+ m_kirigamiWheelEvent.angleDelta(),
+ Qt::KeyboardModifiers(m_kirigamiWheelEvent.modifiers()));
+ }
+ setScrolling(scrolled);
+
+ // NOTE: Wheel events created by touchpad gestures with pixel deltas will cause scrolling to jump back
+ // to where scrolling started unless the event is always accepted before it reaches the Flickable.
+ bool flickableWillUseGestureScrolling = !(wheelEvent->source() == Qt::MouseEventNotSynthesized || wheelEvent->pixelDelta().isNull());
+ return scrolled || m_blockTargetWheel || flickableWillUseGestureScrolling;
+ }
+
+ case QEvent::TouchBegin: {
+ m_wasTouched = true;
+ if (!m_filterMouseEvents) {
+ break;
+ }
+ if (m_verticalScrollBar) {
+ m_verticalScrollBar->setProperty("interactive", false);
+ }
+ if (m_horizontalScrollBar) {
+ m_horizontalScrollBar->setProperty("interactive", false);
+ }
+ break;
+ }
+ case QEvent::TouchEnd: {
+ m_wasTouched = false;
+ break;
+ }
-#include "moc_wheelhandler.cpp"
+ case QEvent::MouseButtonPress: {
+ // NOTE: Flickable does not handle touch events, only synthesized mouse events
+ m_wasTouched = static_cast<QMouseEvent *>(event)->source() != Qt::MouseEventNotSynthesized;
+ if (!m_filterMouseEvents) {
+ break;
+ }
+ if (!m_wasTouched) {
+ if (m_verticalScrollBar) {
+ m_verticalScrollBar->setProperty("interactive", true);
+ }
+ if (m_horizontalScrollBar) {
+ m_horizontalScrollBar->setProperty("interactive", true);
+ }
+ break;
+ }
+ return !m_wasTouched && item == m_flickable;
+ }
+
+ case QEvent::MouseMove:
+ case QEvent::MouseButtonRelease: {
+ setScrolling(false);
+ if (!m_filterMouseEvents) {
+ break;
+ }
+ if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized && item == m_flickable) {
+ return true;
+ }
+ break;
+ }
+
+ case QEvent::HoverEnter:
+ case QEvent::HoverMove: {
+ if (!m_filterMouseEvents) {
+ break;
+ }
+ if (m_wasTouched && (item == m_verticalScrollBar || item == m_horizontalScrollBar)) {
+ if (m_verticalScrollBar) {
+ m_verticalScrollBar->setProperty("interactive", true);
+ }
+ if (m_horizontalScrollBar) {
+ m_horizontalScrollBar->setProperty("interactive", true);
+ }
+ }
+ break;
+ }
+
+ case QEvent::KeyPress: {
+ if (!m_keyNavigationEnabled) {
+ break;
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+ bool horizontalScroll = keyEvent->modifiers() & m_defaultHorizontalScrollModifiers;
+ switch (keyEvent->key()) {
+ case Qt::Key_Up: return scrollUp();
+ case Qt::Key_Down: return scrollDown();
+ case Qt::Key_Left: return scrollLeft();
+ case Qt::Key_Right: return scrollRight();
+ case Qt::Key_PageUp: return horizontalScroll ? scrollLeft(pageWidth) : scrollUp(pageHeight);
+ case Qt::Key_PageDown: return horizontalScroll ? scrollRight(pageWidth) : scrollDown(pageHeight);
+ case Qt::Key_Home: return horizontalScroll ? scrollLeft(contentWidth) : scrollUp(contentHeight);
+ case Qt::Key_End: return horizontalScroll ? scrollRight(contentWidth) : scrollDown(contentHeight);
+ default: break;
+ }
+ break;
+ }
+
+ default: break;
+ }
+
+ return false;
+}
diff --git a/src/3rdparty/kirigami/wheelhandler.h b/src/3rdparty/kirigami/wheelhandler.h
index 41f6fb954..d921a6c9d 100644
--- a/src/3rdparty/kirigami/wheelhandler.h
+++ b/src/3rdparty/kirigami/wheelhandler.h
@@ -1,18 +1,18 @@
-/*
- * SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
- *
- * SPDX-License-Identifier: LGPL-2.0-or-later
+/* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
+ * SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
+ * SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
-#include <QtQml>
+#include <QGuiApplication>
+#include <QObject>
#include <QPoint>
#include <QQuickItem>
-#include <QObject>
+#include <QStyleHints>
+#include <QtQml>
class QWheelEvent;
-
class WheelHandler;
/**
@@ -111,13 +111,13 @@ public:
void initializeFromEvent(QWheelEvent *event);
- [[nodiscard]] qreal x() const;
- [[nodiscard]] qreal y() const;
- [[nodiscard]] QPointF angleDelta() const;
- [[nodiscard]] QPointF pixelDelta() const;
- [[nodiscard]] int buttons() const;
- [[nodiscard]] int modifiers() const;
- [[nodiscard]] bool inverted() const;
+ qreal x() const;
+ qreal y() const;
+ QPointF angleDelta() const;
+ QPointF pixelDelta() const;
+ int buttons() const;
+ int modifiers() const;
+ bool inverted() const;
bool isAccepted();
void setAccepted(bool accepted);
@@ -132,59 +132,135 @@ private:
bool m_accepted = false;
};
-class GlobalWheelFilter : public QObject
+class WheelFilterItem : public QQuickItem
{
Q_OBJECT
-
public:
- GlobalWheelFilter(QObject *parent = nullptr);
- ~GlobalWheelFilter() override;
-
- static GlobalWheelFilter *self();
-
- void setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
- void removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
-
-protected:
- bool eventFilter(QObject *watched, QEvent *event) override;
-
-private:
- void manageWheel(QQuickItem *target, QWheelEvent *wheel);
-
- QMultiHash<QQuickItem *, WheelHandler *> m_handlersForItem;
- KirigamiWheelEvent m_wheelEvent;
+ WheelFilterItem(QQuickItem *parent = nullptr);
};
-
-
/**
- * This class intercepts the mouse wheel events of its target, and gives them to the user code as a signal, which can be used for custom mouse wheel management code.
- * The handler can block completely the wheel events from its target, and if it's a Flickable, it can automatically handle scrolling on it
+ * @brief Handles scrolling for a Flickable and 2 attached ScrollBars.
+ *
+ * WheelHandler filters events from a Flickable, a vertical ScrollBar and a horizontal ScrollBar.
+ * Wheel and KeyPress events (when `keyNavigationEnabled` is true) are used to scroll the Flickable.
+ * When `filterMouseEvents` is true, WheelHandler blocks mouse button input from reaching the Flickable
+ * and sets the `interactive` property of the scrollbars to false when touch input is used.
+ *
+ * Wheel event handling behavior:
+ *
+ * - Pixel delta is ignored unless angle delta is not available because pixel delta scrolling is too slow. Qt Widgets doesn't use pixel delta either, so the default scroll speed should be consistent with Qt Widgets.
+ * - When using angle delta, scroll using the step increments defined by `verticalStepSize` and `horizontalStepSize`.
+ * - When one of the keyboard modifiers in `pageScrollModifiers` is used, scroll by pages.
+ * - When using a device that doesn't use 120 angle delta unit increments such as a touchpad, the `verticalStepSize`, `horizontalStepSize` and page increments (if using page scrolling) will be multiplied by `angle delta / 120` to keep scrolling smooth.
+ * - If scrolling has happened in the last 400ms, use an internal QQuickItem stacked over the Flickable's contentItem to catch wheel events and use those wheel events to scroll, if possible. This prevents controls inside the Flickable's contentItem that allow scrolling to change the value (e.g., Sliders, SpinBoxes) from conflicting with scrolling the page.
+ *
+ * Common usage with a Flickable:
+ *
+ * @include wheelhandler/FlickableUsage.qml
+ *
+ * Common usage inside of a ScrollView template:
+ *
+ * @include wheelhandler/ScrollViewUsage.qml
+ *
*/
class WheelHandler : public QObject
{
Q_OBJECT
/**
- * target: Item
+ * @brief This property holds the Qt Quick Flickable that the WheelHandler will control.
+ */
+ Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged FINAL)
+
+ /**
+ * @brief This property holds the vertical step size.
+ *
+ * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea.
+ *
+ * @sa horizontalStepSize
*
- * The target we want to manage wheel events.
- * We will receive wheel() signals every time the user moves
- * the mouse wheel (or scrolls with the touchpad) on top
- * of that item.
+ * @since KDE Frameworks 5.89
*/
- Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)
+ Q_PROPERTY(qreal verticalStepSize READ verticalStepSize
+ WRITE setVerticalStepSize RESET resetVerticalStepSize
+ NOTIFY verticalStepSizeChanged FINAL)
/**
- * blockTargetWheel: bool
+ * @brief This property holds the horizontal step size.
*
- * If true, the target won't receive any wheel event at all (default true)
+ * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea.
+ *
+ * @sa verticalStepSize
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_PROPERTY(qreal horizontalStepSize READ horizontalStepSize
+ WRITE setHorizontalStepSize RESET resetHorizontalStepSize
+ NOTIFY horizontalStepSizeChanged FINAL)
+
+ /**
+ * @brief This property holds the keyboard modifiers that will be used to start page scrolling.
+ *
+ * The default value is equivalent to `Qt.ControlModifier | Qt.ShiftModifier`. This matches QScrollBar, which uses QAbstractSlider behavior.
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers
+ WRITE setPageScrollModifiers RESET resetPageScrollModifiers
+ NOTIFY pageScrollModifiersChanged FINAL)
+
+ /**
+ * @brief This property holds whether the WheelHandler filters mouse events like a Qt Quick Controls ScrollView would.
+ *
+ * Touch events are allowed to flick the view and they make the scrollbars not interactive.
+ *
+ * Mouse events are not allowed to flick the view and they make the scrollbars interactive.
+ *
+ * Hover events on the scrollbars and wheel events on anything also make the scrollbars interactive when this property is set to true.
+ *
+ * The default value is `false`.
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_PROPERTY(bool filterMouseEvents READ filterMouseEvents
+ WRITE setFilterMouseEvents NOTIFY filterMouseEventsChanged FINAL)
+
+ /**
+ * @brief This property holds whether the WheelHandler handles keyboard scrolling.
+ *
+ * - Left arrow scrolls a step to the left.
+ * - Right arrow scrolls a step to the right.
+ * - Up arrow scrolls a step upwards.
+ * - Down arrow scrolls a step downwards.
+ * - PageUp scrolls to the previous page.
+ * - PageDown scrolls to the next page.
+ * - Home scrolls to the beginning.
+ * - End scrolls to the end.
+ * - When Alt is held, scroll horizontally when using PageUp, PageDown, Home or End.
+ *
+ * The default value is `false`.
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_PROPERTY(bool keyNavigationEnabled READ keyNavigationEnabled
+ WRITE setKeyNavigationEnabled NOTIFY keyNavigationEnabledChanged FINAL)
+
+ /**
+ * @brief This property holds whether the WheelHandler blocks all wheel events from reaching the Flickable.
+ *
+ * When this property is false, scrolling the Flickable with WheelHandler will only block an event from reaching the Flickable if the Flickable is actually scrolled by WheelHandler.
+ *
+ * NOTE: Wheel events created by touchpad gestures with pixel deltas will always be accepted no matter what. This is because they will cause the Flickable to jump back to where scrolling started unless the events are always accepted before they reach the Flickable.
+ *
+ * The default value is true.
*/
Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged)
/**
- * scrollFlickableTarget: bool
- * If this property is true and the target is a Flickable, wheel events will cause the Flickable to scroll (default true)
+ * @brief This property holds whether the WheelHandler can use wheel events to scroll the Flickable.
+ *
+ * The default value is true.
*/
Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged)
@@ -192,22 +268,111 @@ public:
explicit WheelHandler(QObject *parent = nullptr);
~WheelHandler() override;
- [[nodiscard]] QQuickItem *target() const;
+ QQuickItem *target() const;
void setTarget(QQuickItem *target);
+ qreal verticalStepSize() const;
+ void setVerticalStepSize(qreal stepSize);
+ void resetVerticalStepSize();
+
+ qreal horizontalStepSize() const;
+ void setHorizontalStepSize(qreal stepSize);
+ void resetHorizontalStepSize();
+
+ Qt::KeyboardModifiers pageScrollModifiers() const;
+ void setPageScrollModifiers(Qt::KeyboardModifiers modifiers);
+ void resetPageScrollModifiers();
+
+ bool filterMouseEvents() const;
+ void setFilterMouseEvents(bool enabled);
+
+ bool keyNavigationEnabled() const;
+ void setKeyNavigationEnabled(bool enabled);
+
+ /**
+ * Scroll up one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
+ *
+ * returns true if the contentItem was moved.
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_INVOKABLE bool scrollUp(qreal stepSize = -1);
+
+ /**
+ * Scroll down one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
+ *
+ * returns true if the contentItem was moved.
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_INVOKABLE bool scrollDown(qreal stepSize = -1);
+
+ /**
+ * Scroll left one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used.
+ *
+ * returns true if the contentItem was moved.
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_INVOKABLE bool scrollLeft(qreal stepSize = -1);
+
+ /**
+ * Scroll right one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used.
+ *
+ * returns true if the contentItem was moved.
+ *
+ * @since KDE Frameworks 5.89
+ */
+ Q_INVOKABLE bool scrollRight(qreal stepSize = -1);
+
Q_SIGNALS:
void targetChanged();
+ void verticalStepSizeChanged();
+ void horizontalStepSizeChanged();
+ void pageScrollModifiersChanged();
+ void filterMouseEventsChanged();
+ void keyNavigationEnabledChanged();
void blockTargetWheelChanged();
void scrollFlickableTargetChanged();
+
+ /**
+ * @brief This signal is emitted when a wheel event reaches the event filter, just before scrolling is handled.
+ *
+ * Accepting the wheel event in the `onWheel` signal handler prevents scrolling from happening.
+ */
void wheel(KirigamiWheelEvent *wheel);
+protected:
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
private:
- QPointer<QQuickItem> m_target;
+ void setScrolling(bool scrolling);
+ bool scrollFlickable(QPointF pixelDelta,
+ QPointF angleDelta = {},
+ Qt::KeyboardModifiers modifiers = Qt::NoModifier);
+
+ QPointer<QQuickItem> m_flickable;
+ QPointer<QQuickItem> m_verticalScrollBar;
+ QPointer<QQuickItem> m_horizontalScrollBar;
+ QPointer<QQuickItem> m_filterItem;
+ // Matches QScrollArea and QTextEdit
+ qreal m_defaultPixelStepSize = 20 * QGuiApplication::styleHints()->wheelScrollLines();
+ qreal m_verticalStepSize = m_defaultPixelStepSize;
+ qreal m_horizontalStepSize = m_defaultPixelStepSize;
+ bool m_explicitVStepSize = false;
+ bool m_explicitHStepSize = false;
+ bool m_wheelScrolling = false;
+ constexpr static qreal m_wheelScrollingDuration = 400;
+ bool m_filterMouseEvents = false;
+ bool m_keyNavigationEnabled = false;
+ bool m_wasTouched = false;
bool m_blockTargetWheel = true;
bool m_scrollFlickableTarget = true;
- KirigamiWheelEvent m_wheelEvent;
-
- friend class GlobalWheelFilter;
+ // Same as QXcbWindow.
+ constexpr static Qt::KeyboardModifiers m_defaultHorizontalScrollModifiers = Qt::AltModifier;
+ // Same as QScrollBar/QAbstractSlider.
+ constexpr static Qt::KeyboardModifiers m_defaultPageScrollModifiers = Qt::ControlModifier | Qt::ShiftModifier;
+ Qt::KeyboardModifiers m_pageScrollModifiers = m_defaultPageScrollModifiers;
+ QTimer m_wheelScrollingTimer;
+ KirigamiWheelEvent m_kirigamiWheelEvent;
};
-
-
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 3a845531e..77d875b85 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -39,9 +39,6 @@ set(client_UI_SRCS
ignorelisttablewidget.ui
networksettings.ui
settingsdialog.ui
- sharedialog.ui
- sharelinkwidget.ui
- shareusergroupwidget.ui
shareuserline.ui
sslerrordialog.ui
addcertificatedialog.ui
@@ -139,14 +136,8 @@ set(client_SRCS
selectivesyncdialog.cpp
settingsdialog.h
settingsdialog.cpp
- sharedialog.h
- sharedialog.cpp
- sharelinkwidget.h
- sharelinkwidget.cpp
sharemanager.h
sharemanager.cpp
- shareusergroupwidget.h
- shareusergroupwidget.cpp
profilepagewidget.h
profilepagewidget.cpp
sharee.h
@@ -193,6 +184,14 @@ set(client_SRCS
emojimodel.cpp
fileactivitylistmodel.h
fileactivitylistmodel.cpp
+ filedetails/filedetails.h
+ filedetails/filedetails.cpp
+ filedetails/sharemodel.h
+ filedetails/sharemodel.cpp
+ filedetails/shareemodel.h
+ filedetails/shareemodel.cpp
+ filedetails/sortedsharemodel.h
+ filedetails/sortedsharemodel.cpp
tray/svgimageprovider.h
tray/svgimageprovider.cpp
tray/syncstatussummary.h
diff --git a/src/gui/application.cpp b/src/gui/application.cpp
index fafc33a24..9dd436dc6 100644
--- a/src/gui/application.cpp
+++ b/src/gui/application.cpp
@@ -32,7 +32,6 @@
#include "sslerrordialog.h"
#include "theme.h"
#include "clientproxy.h"
-#include "sharedialog.h"
#include "accountmanager.h"
#include "creds/abstractcredentials.h"
#include "pushnotifications.h"
@@ -379,7 +378,7 @@ Application::Application(int &argc, char **argv)
_gui.data(), &ownCloudGui::slotShowShareDialog);
connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived,
- Systray::instance(), &Systray::showFileActivityDialog);
+ _gui.data(), &ownCloudGui::slotShowFileActivityDialog);
// startup procedure.
connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection);
diff --git a/src/gui/fileactivitylistmodel.cpp b/src/gui/fileactivitylistmodel.cpp
index 5fadec981..4e4c3c220 100644
--- a/src/gui/fileactivitylistmodel.cpp
+++ b/src/gui/fileactivitylistmodel.cpp
@@ -24,23 +24,54 @@ FileActivityListModel::FileActivityListModel(QObject *parent)
: ActivityListModel(nullptr, parent)
{
setDisplayActions(false);
+ connect(this, &FileActivityListModel::accountStateChanged, this, &FileActivityListModel::load);
}
-void FileActivityListModel::load(AccountState *accountState, const int objectId)
+QString FileActivityListModel::localPath() const
{
- Q_ASSERT(accountState);
- if (!accountState || currentlyFetching()) {
+ return _localPath;
+}
+
+void FileActivityListModel::setLocalPath(const QString &localPath)
+{
+ if(localPath == _localPath) {
+ return;
+ }
+
+ _localPath = localPath;
+ Q_EMIT localPathChanged();
+
+ load();
+}
+
+void FileActivityListModel::load()
+{
+ if (!accountState() || _localPath.isEmpty() || currentlyFetching()) {
+ return;
+ }
+
+ const auto folder = FolderMan::instance()->folderForPath(_localPath);
+
+ if (!folder) {
+ qCWarning(lcFileActivityListModel) << "Invalid folder for localPath:" << _localPath << "will not load activity list model.";
+ return;
+ }
+
+ const auto folderRelativePath = _localPath.mid(folder->cleanPath().length() + 1);
+ SyncJournalFileRecord record;
+
+ if (!folder->journalDb()->getFileRecord(folderRelativePath, &record) || !record.isValid()) {
+ qCWarning(lcFileActivityListModel) << "Invalid file record for path:" << _localPath << "will not load activity list model.";
return;
}
- setAccountState(accountState);
- _objectId = objectId;
+ _objectId = record.numericFileId().toInt();
slotRefreshActivity();
}
void FileActivityListModel::startFetchJob()
{
- if (!accountState()->isConnected()) {
+ if (!accountState()->isConnected() || _objectId == -1) {
return;
}
setAndRefreshCurrentlyFetching(true);
diff --git a/src/gui/fileactivitylistmodel.h b/src/gui/fileactivitylistmodel.h
index 18d8d9830..a11197405 100644
--- a/src/gui/fileactivitylistmodel.h
+++ b/src/gui/fileactivitylistmodel.h
@@ -22,17 +22,25 @@ namespace OCC {
class FileActivityListModel : public ActivityListModel
{
Q_OBJECT
+ Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged)
public:
explicit FileActivityListModel(QObject *parent = nullptr);
+ [[nodiscard]] QString localPath() const;
+
+signals:
+ void localPathChanged();
+
public slots:
- void load(AccountState *accountState, const int objectId);
+ void setLocalPath(const QString &localPath);
+ void load();
protected slots:
void startFetchJob() override;
private:
- int _objectId;
+ int _objectId = -1;
+ QString _localPath;
};
}
diff --git a/src/gui/filedetails/FileActivityView.qml b/src/gui/filedetails/FileActivityView.qml
new file mode 100644
index 000000000..002654b79
--- /dev/null
+++ b/src/gui/filedetails/FileActivityView.qml
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+import "../tray"
+
+ActivityList {
+ id: root
+
+ property alias localPath: activityListModel.localPath
+ property alias accountState: activityListModel.accountState
+
+ isFileActivityList: true
+ model: FileActivityListModel {
+ id: activityListModel
+ }
+}
diff --git a/src/gui/filedetails/FileDetailsPage.qml b/src/gui/filedetails/FileDetailsPage.qml
new file mode 100644
index 000000000..fc39825a3
--- /dev/null
+++ b/src/gui/filedetails/FileDetailsPage.qml
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+
+Page {
+ id: root
+
+ property var accountState: ({})
+ property string localPath: ({})
+
+ // We want the SwipeView to "spill" over the edges of the window to really
+ // make it look nice. If we apply page-wide padding, however, the swipe
+ // contents only go as far as the page contents, clipped by the padding.
+ // This property reflects the padding we intend to display, but not the real
+ // padding, which we have to apply selectively to achieve our desired effect.
+ property int intendedPadding: Style.standardSpacing * 2
+ property int iconSize: 32
+
+ property FileDetails fileDetails: FileDetails {
+ id: fileDetails
+ localPath: root.localPath
+ }
+
+ Connections {
+ target: Systray
+ function onShowFileDetailsPage(fileLocalPath, page) {
+ if(fileLocalPath === root.localPath) {
+ switch(page) {
+ case Systray.FileDetailsPage.Activity:
+ swipeView.currentIndex = fileActivityView.swipeIndex;
+ break;
+ case Systray.FileDetailsPage.Sharing:
+ swipeView.currentIndex = shareView.swipeIndex;
+ break;
+ }
+ }
+ }
+ }
+
+ topPadding: intendedPadding
+ bottomPadding: intendedPadding
+
+ background: Rectangle {
+ color: Style.backgroundColor
+ }
+
+ header: ColumnLayout {
+ spacing: root.intendedPadding
+
+ GridLayout {
+ id: headerGridLayout
+
+ readonly property bool showFileLockedString: root.fileDetails.lockExpireString !== ""
+
+ Layout.fillWidth: parent
+ Layout.topMargin: root.topPadding
+
+ columns: 2
+ rows: showFileLockedString ? 3 : 2
+
+ rowSpacing: Style.standardSpacing / 2
+ columnSpacing: Style.standardSpacing
+
+ Image {
+ id: fileIcon
+
+ Layout.rowSpan: headerGridLayout.rows
+ Layout.preferredWidth: Style.trayListItemIconSize
+ Layout.leftMargin: root.intendedPadding
+ Layout.fillHeight: true
+
+ verticalAlignment: Image.AlignVCenter
+ horizontalAlignment: Image.AlignHCenter
+ source: root.fileDetails.iconUrl
+ sourceSize.width: Style.trayListItemIconSize
+ sourceSize.height: Style.trayListItemIconSize
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Label {
+ id: fileNameLabel
+
+ Layout.fillWidth: true
+ Layout.rightMargin: root.intendedPadding
+
+ text: root.fileDetails.name
+ color: Style.ncTextColor
+ font.bold: true
+ wrapMode: Text.Wrap
+ }
+
+ Label {
+ id: fileDetailsLabel
+
+ Layout.fillWidth: true
+ Layout.rightMargin: root.intendedPadding
+
+ text: `${root.fileDetails.sizeString} · ${root.fileDetails.lastChangedString}`
+ color: Style.ncSecondaryTextColor
+ wrapMode: Text.Wrap
+ }
+
+ Label {
+ id: fileLockedLabel
+
+ Layout.fillWidth: true
+ Layout.rightMargin: root.intendedPadding
+
+ text: root.fileDetails.lockExpireString
+ color: Style.ncSecondaryTextColor
+ wrapMode: Text.Wrap
+ visible: headerGridLayout.showFileLockedString
+ }
+ }
+
+ TabBar {
+ id: viewBar
+
+ Layout.leftMargin: root.intendedPadding
+ Layout.rightMargin: root.intendedPadding
+
+ padding: 0
+ background: Rectangle {
+ color: Style.backgroundColor
+ }
+
+ NCTabButton {
+ svgCustomColorSource: "image://svgimage-custom-color/activity.svg"
+ text: qsTr("Activity")
+ checked: swipeView.currentIndex === fileActivityView.swipeIndex
+ onClicked: swipeView.currentIndex = fileActivityView.swipeIndex
+ }
+
+ NCTabButton {
+ svgCustomColorSource: "image://svgimage-custom-color/share.svg"
+ text: qsTr("Sharing")
+ checked: swipeView.currentIndex === shareView.swipeIndex
+ onClicked: swipeView.currentIndex = shareView.swipeIndex
+ }
+ }
+ }
+
+ SwipeView {
+ id: swipeView
+
+ anchors.fill: parent
+ clip: true
+
+ FileActivityView {
+ id: fileActivityView
+
+ property int swipeIndex: SwipeView.index
+
+ delegateHorizontalPadding: root.intendedPadding
+
+ accountState: root.accountState
+ localPath: root.localPath
+ iconSize: root.iconSize
+ }
+
+ ShareView {
+ id: shareView
+
+ property int swipeIndex: SwipeView.index
+
+ accountState: root.accountState
+ localPath: root.localPath
+ fileDetails: root.fileDetails
+ horizontalPadding: root.intendedPadding
+ iconSize: root.iconSize
+ }
+ }
+}
diff --git a/src/gui/filedetails/FileDetailsWindow.qml b/src/gui/filedetails/FileDetailsWindow.qml
new file mode 100644
index 000000000..04320615c
--- /dev/null
+++ b/src/gui/filedetails/FileDetailsWindow.qml
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+
+ApplicationWindow {
+ id: root
+
+ property var accountState
+ property string localPath: ""
+
+ width: 400
+ height: 500
+ minimumWidth: 300
+ minimumHeight: 300
+
+ title: qsTr("File details of %1 · %2").arg(fileDetailsPage.fileDetails.name).arg(Systray.windowTitle)
+
+ FileDetailsPage {
+ id: fileDetailsPage
+ anchors.fill: parent
+ accountState: root.accountState
+ localPath: root.localPath
+ }
+}
diff --git a/src/gui/filedetails/NCInputTextEdit.qml b/src/gui/filedetails/NCInputTextEdit.qml
new file mode 100644
index 000000000..85cd39940
--- /dev/null
+++ b/src/gui/filedetails/NCInputTextEdit.qml
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+
+TextEdit {
+ id: root
+
+ property color accentColor: Style.ncBlue
+ property color secondaryColor: Style.menuBorder
+ property alias submitButton: submitButton
+
+ clip: true
+ color: Style.ncTextColor
+ textMargin: Style.smallSpacing
+ wrapMode: TextEdit.Wrap
+ selectByMouse: true
+ height: Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight)
+
+ Rectangle {
+ id: textFieldBorder
+ anchors.fill: parent
+ radius: Style.slightlyRoundedButtonRadius
+ border.width: Style.normalBorderWidth
+ border.color: root.activeFocus ? root.accentColor : root.secondaryColor
+ color: Style.backgroundColor
+ z: -1
+ }
+
+ Button {
+ id: submitButton
+
+ anchors.bottom: root.bottom
+ anchors.right: root.right
+ anchors.margins: 1
+
+ width: height
+ height: parent.height
+
+ background: Rectangle {
+ radius: width / 2
+ color: textFieldBorder.color
+ }
+
+ flat: true
+ icon.source: "image://svgimage-custom-color/confirm.svg" + "/" + root.secondaryColor
+ icon.color: hovered && enabled ? UserModel.currentUser.accentColor : root.secondaryColor
+
+ enabled: root.text !== ""
+
+ onClicked: root.editingFinished()
+ }
+}
+
diff --git a/src/gui/filedetails/NCInputTextField.qml b/src/gui/filedetails/NCInputTextField.qml
new file mode 100644
index 000000000..36dd42ee7
--- /dev/null
+++ b/src/gui/filedetails/NCInputTextField.qml
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+
+TextField {
+ id: root
+
+ property color accentColor: Style.ncBlue
+ property color secondaryColor: Style.menuBorder
+ property alias submitButton: submitButton
+
+ implicitHeight: Style.talkReplyTextFieldPreferredHeight
+ color: Style.ncTextColor
+ placeholderTextColor: secondaryColor
+
+ rightPadding: submitButton.width
+
+ selectByMouse: true
+
+ background: Rectangle {
+ id: textFieldBorder
+ radius: Style.slightlyRoundedButtonRadius
+ border.width: Style.normalBorderWidth
+ border.color: root.activeFocus ? root.accentColor : root.secondaryColor
+ color: Style.backgroundColor
+ }
+
+ Button {
+ id: submitButton
+
+ anchors.top: root.top
+ anchors.right: root.right
+ anchors.margins: 1
+
+ width: height
+ height: parent.height
+
+ background: null
+ flat: true
+ icon.source: "image://svgimage-custom-color/confirm.svg" + "/" + root.secondaryColor
+ icon.color: hovered && enabled ? UserModel.currentUser.accentColor : root.secondaryColor
+
+ enabled: root.text !== ""
+
+ onClicked: root.accepted()
+ }
+}
+
diff --git a/src/gui/filedetails/NCTabButton.qml b/src/gui/filedetails/NCTabButton.qml
new file mode 100644
index 000000000..cd863cea8
--- /dev/null
+++ b/src/gui/filedetails/NCTabButton.qml
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+
+TabButton {
+ id: tabButton
+
+ property string svgCustomColorSource: ""
+
+ padding: Style.smallSpacing
+ background: Rectangle {
+ radius: Style.slightlyRoundedButtonRadius
+ color: tabButton.pressed ? Style.lightHover : Style.backgroundColor
+ }
+
+ contentItem: ColumnLayout {
+ id: tabButtonLayout
+
+ property var elementColors: tabButton.checked || tabButton.hovered ? Style.ncTextColor : Style.ncSecondaryTextColor
+
+ // We'd like to just set the height of the Image, but this causes crashing.
+ // So we use a wrapping Item and use anchors to adjust the size.
+ Item {
+ id: iconItem
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ height: 20
+
+ Image {
+ id: iconItemImage
+ anchors.fill: parent
+ anchors.margins: tabButton.checked ? 0 : 2
+ horizontalAlignment: Image.AlignHCenter
+ verticalAlignment: Image.AlignVCenter
+ fillMode: Image.PreserveAspectFit
+ source: tabButton.svgCustomColorSource + "/" + tabButtonLayout.elementColors
+ sourceSize.width: 32
+ sourceSize.height: 32
+ }
+ }
+
+ Label {
+ id: tabButtonLabel
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ color: tabButtonLayout.elementColors
+ text: tabButton.text
+ font.bold: tabButton.checked
+ }
+
+ Rectangle {
+ FontMetrics {
+ id: fontMetrics
+ font.family: tabButtonLabel.font.family
+ font.pixelSize: tabButtonLabel.font.pixelSize
+ font.bold: true
+ }
+
+ property int textWidth: fontMetrics.boundingRect(tabButtonLabel.text).width
+
+ Layout.fillWidth: true
+ implicitWidth: textWidth + Style.standardSpacing * 2
+ implicitHeight: 2
+
+ color: tabButton.checked ? Style.ncBlue : tabButton.hovered ? Style.lightHover : "transparent"
+ }
+ }
+}
diff --git a/src/gui/filedetails/ShareDelegate.qml b/src/gui/filedetails/ShareDelegate.qml
new file mode 100644
index 000000000..7b8626e7b
--- /dev/null
+++ b/src/gui/filedetails/ShareDelegate.qml
@@ -0,0 +1,816 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+import QtGraphicalEffects 1.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+import "../tray"
+import "../"
+
+GridLayout {
+ id: root
+
+ signal deleteShare
+ signal createNewLinkShare
+
+ signal toggleAllowEditing(bool enable)
+ signal toggleAllowResharing(bool enable)
+ signal togglePasswordProtect(bool enable)
+ signal toggleExpirationDate(bool enable)
+ signal toggleNoteToRecipient(bool enable)
+
+ signal setLinkShareLabel(string label)
+ signal setExpireDate(var milliseconds) // Since QML ints are only 32 bits, use a variant
+ signal setPassword(string password)
+ signal setNote(string note)
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ columns: 3
+ rows: linkDetailLabel.visible ? 1 : 2
+
+ columnSpacing: Style.standardSpacing / 2
+ rowSpacing: Style.standardSpacing / 2
+
+ property int iconSize: 32
+
+ property var share: model.share ?? ({})
+
+ property string iconUrl: model.iconUrl ?? ""
+ property string avatarUrl: model.avatarUrl ?? ""
+ property string text: model.display ?? ""
+ property string detailText: model.detailText ?? ""
+ property string link: model.link ?? ""
+ property string note: model.note ?? ""
+ property string password: model.password ?? ""
+ property string passwordPlaceholder: "●●●●●●●●●●"
+
+ property var expireDate: model.expireDate // Don't use int as we are limited
+ property var maximumExpireDate: model.enforcedMaximumExpireDate
+
+ property string linkShareLabel: model.linkShareLabel ?? ""
+
+ property bool editingAllowed: model.editingAllowed
+ property bool noteEnabled: model.noteEnabled
+ property bool expireDateEnabled: model.expireDateEnabled
+ property bool expireDateEnforced: model.expireDateEnforced
+ property bool passwordProtectEnabled: model.passwordProtectEnabled
+ property bool passwordEnforced: model.passwordEnforced
+
+ property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink
+ property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink
+
+ property bool canCreateLinkShares: true
+
+ property bool waitingForEditingAllowedChange: false
+ property bool waitingForNoteEnabledChange: false
+ property bool waitingForExpireDateEnabledChange: false
+ property bool waitingForPasswordProtectEnabledChange: false
+ property bool waitingForExpireDateChange: false
+ property bool waitingForLinkShareLabelChange: false
+ property bool waitingForPasswordChange: false
+ property bool waitingForNoteChange: false
+
+ function showPasswordSetError(message) {
+ passwordErrorBoxLoader.message = message !== "" ?
+ message : qsTr("An error occurred setting the share password.");
+ }
+
+ function resetNoteField() {
+ noteTextEdit.text = note;
+ waitingForNoteChange = false;
+ }
+
+ function resetLinkShareLabelField() {
+ linkShareLabelTextField.text = linkShareLabel;
+ waitingForLinkShareLabelChange = false;
+ }
+
+ function resetPasswordField() {
+ passwordTextField.text = password !== "" ? password : passwordPlaceholder;
+ waitingForPasswordChange = false;
+ }
+
+ function resetExpireDateField() {
+ // Expire date changing is handled by the expireDateSpinBox
+ waitingForExpireDateChange = false;
+ }
+
+ function resetEditingAllowedField() {
+ editingAllowedMenuItem.checked = editingAllowed;
+ waitingForEditingAllowedChange = false;
+ }
+
+ function resetNoteEnabledField() {
+ noteEnabledMenuItem.checked = noteEnabled;
+ waitingForNoteEnabledChange = false;
+ }
+
+ function resetExpireDateEnabledField() {
+ expireDateEnabledMenuItem.checked = expireDateEnabled;
+ waitingForExpireDateEnabledChange = false;
+ }
+
+ function resetPasswordProtectEnabledField() {
+ passwordProtectEnabledMenuItem.checked = passwordProtectEnabled;
+ waitingForPasswordProtectEnabledChange = false;
+ }
+
+ function resetMenu() {
+ moreMenu.close();
+
+ resetNoteField();
+ resetPasswordField();
+ resetLinkShareLabelField();
+ resetExpireDateField();
+
+ resetEditingAllowedField();
+ resetNoteEnabledField();
+ resetExpireDateEnabledField();
+ resetPasswordProtectEnabledField();
+ }
+
+ // Renaming a link share can lead to the model being reshuffled.
+ // This can cause a situation where this delegate is assigned to
+ // a new row and it doesn't have its properties signalled as
+ // changed by the model, leading to bugs. We therefore reset all
+ // the fields here when we detect the share has been changed
+ onShareChanged: resetMenu()
+
+ // Reset value after property binding broken by user interaction
+ onNoteChanged: resetNoteField()
+ onPasswordChanged: resetPasswordField()
+ onLinkShareLabelChanged: resetLinkShareLabelField()
+ onExpireDateChanged: resetExpireDateField()
+
+ onEditingAllowedChanged: resetEditingAllowedField()
+ onNoteEnabledChanged: resetNoteEnabledField()
+ onExpireDateEnabledChanged: resetExpireDateEnabledField()
+ onPasswordProtectEnabledChanged: resetPasswordProtectEnabledField()
+
+ Item {
+ id: imageItem
+
+ property bool isAvatar: root.avatarUrl !== ""
+
+ Layout.row: 0
+ Layout.column: 0
+ Layout.rowSpan: root.rows
+ Layout.preferredWidth: root.iconSize
+ Layout.preferredHeight: root.iconSize
+
+ Rectangle {
+ id: backgroundOrMask
+ anchors.fill: parent
+ radius: width / 2
+ color: Style.ncBlue
+ visible: !imageItem.isAvatar
+ }
+
+ Image {
+ id: shareIconOrThumbnail
+
+ anchors.centerIn: parent
+
+ verticalAlignment: Image.AlignVCenter
+ horizontalAlignment: Image.AlignHCenter
+ fillMode: Image.PreserveAspectFit
+
+ source: imageItem.isAvatar ? root.avatarUrl : root.iconUrl + "/white"
+ sourceSize.width: imageItem.isAvatar ? root.iconSize : root.iconSize / 2
+ sourceSize.height: imageItem.isAvatar ? root.iconSize : root.iconSize / 2
+
+ visible: !imageItem.isAvatar
+ }
+
+ OpacityMask {
+ anchors.fill: parent
+ source: shareIconOrThumbnail
+ maskSource: backgroundOrMask
+ visible: imageItem.isAvatar
+ }
+ }
+
+ Label {
+ id: shareTypeLabel
+
+ Layout.fillWidth: true
+ Layout.alignment: linkDetailLabel.visible ? Qt.AlignBottom : Qt.AlignVCenter
+ Layout.row: 0
+ Layout.column: 1
+ Layout.rowSpan: root.rows
+
+ text: root.text
+ color: Style.ncTextColor
+ elide: Text.ElideRight
+ }
+
+ Label {
+ id: linkDetailLabel
+
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignTop
+ Layout.row: 1
+ Layout.column: 1
+
+ text: root.detailText
+ color: Style.ncSecondaryTextColor
+ elide: Text.ElideRight
+ visible: text !== ""
+ }
+
+ RowLayout {
+ Layout.row: 0
+ Layout.column: 2
+ Layout.rowSpan: root.rows
+ Layout.fillHeight: true
+
+ spacing: 0
+
+ CustomButton {
+ id: createLinkButton
+
+ Layout.alignment: Qt.AlignCenter
+ Layout.preferredWidth: Style.iconButtonWidth
+ Layout.preferredHeight: width
+
+ toolTipText: qsTr("Create a new share link")
+
+ bgColor: Style.lightHover
+ bgNormalOpacity: 0
+
+ imageSource: "qrc:///client/theme/add.svg"
+
+ visible: root.isPlaceholderLinkShare && root.canCreateLinkShares
+ enabled: visible
+
+ onClicked: root.createNewLinkShare()
+ }
+
+ CustomButton {
+ id: copyLinkButton
+
+ Layout.alignment: Qt.AlignCenter
+ Layout.preferredWidth: Style.iconButtonWidth
+ Layout.preferredHeight: width
+
+ toolTipText: qsTr("Copy share link location")
+
+ bgColor: Style.lightHover
+ bgNormalOpacity: 0
+
+ imageSource: "qrc:///client/theme/copy.svg"
+ icon.width: 16
+ icon.height: 16
+
+ visible: root.isLinkShare
+ enabled: visible
+
+ onClicked: {
+ clipboardHelper.text = root.link;
+ clipboardHelper.selectAll();
+ clipboardHelper.copy();
+ clipboardHelper.clear();
+ }
+
+ TextEdit { id: clipboardHelper; visible: false}
+ }
+
+ CustomButton {
+ id: moreButton
+
+ Layout.alignment: Qt.AlignCenter
+ Layout.preferredWidth: Style.iconButtonWidth
+ Layout.preferredHeight: width
+
+ toolTipText: qsTr("Share options")
+
+ bgColor: Style.lightHover
+ bgNormalOpacity: 0
+
+ imageSource: "qrc:///client/theme/more.svg"
+
+ visible: !root.isPlaceholderLinkShare
+ enabled: visible
+
+ onClicked: moreMenu.popup()
+
+ Menu {
+ id: moreMenu
+
+ property int rowIconWidth: 16
+ property int indicatorItemWidth: 20
+ property int indicatorSpacing: Style.standardSpacing
+ property int itemPadding: Style.smallSpacing
+
+ padding: Style.smallSpacing
+ // TODO: Rather than setting all these palette colours manually,
+ // create a custom style and do it for all components globally
+ palette {
+ text: Style.ncTextColor
+ windowText: Style.ncTextColor
+ buttonText: Style.ncTextColor
+ light: Style.lightHover
+ midlight: Style.lightHover
+ mid: Style.ncSecondaryTextColor
+ dark: Style.menuBorder
+ button: Style.menuBorder
+ window: Style.backgroundColor
+ base: Style.backgroundColor
+ }
+
+ RowLayout {
+ anchors.left: parent.left
+ anchors.leftMargin: moreMenu.itemPadding
+ anchors.right: parent.right
+ anchors.rightMargin: moreMenu.itemPadding
+ height: visible ? implicitHeight : 0
+ spacing: moreMenu.indicatorSpacing
+
+ visible: root.isLinkShare
+
+ Image {
+ Layout.preferredWidth: moreMenu.indicatorItemWidth
+ Layout.fillHeight: true
+
+ verticalAlignment: Image.AlignVCenter
+ horizontalAlignment: Image.AlignHCenter
+ fillMode: Image.Pad
+
+ source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
+ sourceSize.width: moreMenu.rowIconWidth
+ sourceSize.height: moreMenu.rowIconWidth
+ }
+
+ NCInputTextField {
+ id: linkShareLabelTextField
+
+ Layout.fillWidth: true
+ height: visible ? implicitHeight : 0
+
+ text: root.linkShareLabel
+ placeholderText: qsTr("Share label")
+
+ enabled: root.isLinkShare &&
+ !root.waitingForLinkShareLabelChange
+
+ onAccepted: if(text !== root.linkShareLabel) {
+ root.setLinkShareLabel(text);
+ root.waitingForLinkShareLabelChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForLinkShareLabelChange
+ running: visible
+ z: 1
+ }
+ }
+ }
+
+ // On these checkables, the clicked() signal is called after
+ // the check state changes.
+ CheckBox {
+ id: editingAllowedMenuItem
+
+ spacing: moreMenu.indicatorSpacing
+ padding: moreMenu.itemPadding
+ indicator.width: moreMenu.indicatorItemWidth
+ indicator.height: moreMenu.indicatorItemWidth
+
+ checkable: true
+ checked: root.editingAllowed
+ text: qsTr("Allow editing")
+ enabled: !root.waitingForEditingAllowedChange
+
+ onClicked: {
+ root.toggleAllowEditing(checked);
+ root.waitingForEditingAllowedChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForEditingAllowedChange
+ running: visible
+ z: 1
+ }
+ }
+
+ CheckBox {
+ id: passwordProtectEnabledMenuItem
+
+ spacing: moreMenu.indicatorSpacing
+ padding: moreMenu.itemPadding
+ indicator.width: moreMenu.indicatorItemWidth
+ indicator.height: moreMenu.indicatorItemWidth
+
+ checkable: true
+ checked: root.passwordProtectEnabled
+ text: qsTr("Password protect")
+ enabled: !root.waitingForPasswordProtectEnabledChange && !root.passwordEnforced
+
+ onClicked: {
+ root.togglePasswordProtect(checked);
+ root.waitingForPasswordProtectEnabledChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForPasswordProtectEnabledChange
+ running: visible
+ z: 1
+ }
+ }
+
+ RowLayout {
+ anchors.left: parent.left
+ anchors.leftMargin: moreMenu.itemPadding
+ anchors.right: parent.right
+ anchors.rightMargin: moreMenu.itemPadding
+ height: visible ? implicitHeight : 0
+ spacing: moreMenu.indicatorSpacing
+
+ visible: root.passwordProtectEnabled
+
+ Image {
+ Layout.preferredWidth: moreMenu.indicatorItemWidth
+ Layout.fillHeight: true
+
+ verticalAlignment: Image.AlignVCenter
+ horizontalAlignment: Image.AlignHCenter
+ fillMode: Image.Pad
+
+ source: "image://svgimage-custom-color/lock-https.svg/" + Style.menuBorder
+ sourceSize.width: moreMenu.rowIconWidth
+ sourceSize.height: moreMenu.rowIconWidth
+ }
+
+ NCInputTextField {
+ id: passwordTextField
+
+ Layout.fillWidth: true
+ height: visible ? implicitHeight : 0
+
+ text: root.password !== "" ? root.password : root.passwordPlaceholder
+ enabled: root.passwordProtectEnabled &&
+ !root.waitingForPasswordChange &&
+ !root.waitingForPasswordProtectEnabledChange
+
+ onAccepted: if(text !== root.password && text !== root.passwordPlaceholder) {
+ passwordErrorBoxLoader.message = "";
+ root.setPassword(text);
+ root.waitingForPasswordChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForPasswordChange ||
+ root.waitingForPasswordProtectEnabledChange
+ running: visible
+ z: 1
+ }
+ }
+ }
+
+ Loader {
+ id: passwordErrorBoxLoader
+
+ property string message: ""
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: message !== "" ? implicitHeight : 0
+
+ active: message !== ""
+ visible: active
+
+ sourceComponent: Item {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ // Artificially add vertical padding
+ implicitHeight: passwordErrorBox.implicitHeight + (Style.smallSpacing * 2)
+
+ ErrorBox {
+ id: passwordErrorBox
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ text: passwordErrorBoxLoader.message
+ }
+ }
+ }
+
+ CheckBox {
+ id: expireDateEnabledMenuItem
+
+ spacing: moreMenu.indicatorSpacing
+ padding: moreMenu.itemPadding
+ indicator.width: moreMenu.indicatorItemWidth
+ indicator.height: moreMenu.indicatorItemWidth
+
+ checkable: true
+ checked: root.expireDateEnabled
+ text: qsTr("Set expiration date")
+ enabled: !root.waitingForExpireDateEnabledChange && !root.expireDateEnforced
+
+ onClicked: {
+ root.toggleExpirationDate(checked);
+ root.waitingForExpireDateEnabledChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForExpireDateEnabledChange
+ running: visible
+ z: 1
+ }
+ }
+
+ RowLayout {
+ anchors.left: parent.left
+ anchors.leftMargin: moreMenu.itemPadding
+ anchors.right: parent.right
+ anchors.rightMargin: moreMenu.itemPadding
+ height: visible ? implicitHeight : 0
+ spacing: moreMenu.indicatorSpacing
+
+ visible: root.expireDateEnabled
+
+ Image {
+ Layout.preferredWidth: moreMenu.indicatorItemWidth
+ Layout.fillHeight: true
+
+ verticalAlignment: Image.AlignVCenter
+ horizontalAlignment: Image.AlignHCenter
+ fillMode: Image.Pad
+
+ source: "image://svgimage-custom-color/calendar.svg/" + Style.menuBorder
+ sourceSize.width: moreMenu.rowIconWidth
+ sourceSize.height: moreMenu.rowIconWidth
+ }
+
+ // QML dates are essentially JavaScript dates, which makes them very finicky and unreliable.
+ // Instead, we exclusively deal with msecs from epoch time to make things less painful when editing.
+ // We only use the QML Date when showing the nice string to the user.
+ SpinBox {
+ id: expireDateSpinBox
+
+ // Work arounds the limitations of QML's 32 bit integer when handling msecs from epoch
+ // Instead, we handle everything as days since epoch
+ readonly property int dayInMSecs: 24 * 60 * 60 * 1000
+ readonly property int expireDateReduced: Math.floor(root.expireDate / dayInMSecs)
+ // Reset the model data after binding broken on user interact
+ onExpireDateReducedChanged: value = expireDateReduced
+
+ // We can't use JS's convenient Infinity or Number.MAX_VALUE as
+ // JS Number type is 64 bits, whereas QML's int type is only 32 bits
+ readonly property IntValidator intValidator: IntValidator {}
+ readonly property int maximumExpireDateReduced: root.expireDateEnforced ?
+ Math.floor(root.maximumExpireDate / dayInMSecs) :
+ intValidator.top
+ readonly property int minimumExpireDateReduced: {
+ const currentDate = new Date();
+ const minDateUTC = new Date(Date.UTC(currentDate.getFullYear(),
+ currentDate.getMonth(),
+ currentDate.getDate() + 1));
+ return Math.floor(minDateUTC / dayInMSecs) // Start of day at 00:00:0000 UTC
+ }
+
+ // Taken from Kalendar 22.08
+ // https://invent.kde.org/pim/kalendar/-/blob/release/22.08/src/contents/ui/KalendarUtils/dateutils.js
+ function parseDateString(dateString) {
+ function defaultParse() {
+ const defaultParsedDate = Date.fromLocaleDateString(Qt.locale(), dateString, Locale.NarrowFormat);
+ // JS always generates date in system locale, eliminate timezone difference to UTC
+ const msecsSinceEpoch = defaultParsedDate.getTime() - (defaultParsedDate.getTimezoneOffset() * 60 * 1000);
+ return new Date(msecsSinceEpoch);
+ }
+
+ const dateStringDelimiterMatches = dateString.match(/\D/);
+ if(dateStringDelimiterMatches.length === 0) {
+ // Let the date method figure out this weirdness
+ return defaultParse();
+ }
+
+ const dateStringDelimiter = dateStringDelimiterMatches[0];
+
+ const localisedDateFormatSplit = Qt.locale().dateFormat(Locale.NarrowFormat).split(dateStringDelimiter);
+ const localisedDateDayPosition = localisedDateFormatSplit.findIndex((x) => /d/gi.test(x));
+ const localisedDateMonthPosition = localisedDateFormatSplit.findIndex((x) => /m/gi.test(x));
+ const localisedDateYearPosition = localisedDateFormatSplit.findIndex((x) => /y/gi.test(x));
+
+ let splitDateString = dateString.split(dateStringDelimiter);
+ let userProvidedYear = splitDateString[localisedDateYearPosition]
+
+ const dateNow = new Date();
+ const stringifiedCurrentYear = dateNow.getFullYear().toString();
+
+ // If we have any input weirdness, or if we have a fully-written year
+ // (e.g. 2022 instead of 22) then use default parse
+ if(splitDateString.length === 0 ||
+ splitDateString.length > 3 ||
+ userProvidedYear.length >= stringifiedCurrentYear.length) {
+
+ return defaultParse();
+ }
+
+ let fullyWrittenYear = userProvidedYear.split("");
+ const digitsToAdd = stringifiedCurrentYear.length - fullyWrittenYear.length;
+ for(let i = 0; i < digitsToAdd; i++) {
+ fullyWrittenYear.splice(i, 0, stringifiedCurrentYear[i])
+ }
+ fullyWrittenYear = fullyWrittenYear.join("");
+
+ const fixedYearNum = Number(fullyWrittenYear);
+ const monthIndexNum = Number(splitDateString[localisedDateMonthPosition]) - 1;
+ const dayNum = Number(splitDateString[localisedDateDayPosition]);
+
+ console.log(dayNum, monthIndexNum, fixedYearNum);
+
+ // Modification: return date in UTC
+ return new Date(Date.UTC(fixedYearNum, monthIndexNum, dayNum));
+ }
+
+ Layout.fillWidth: true
+ height: visible ? implicitHeight : 0
+
+
+ // We want all the internal benefits of the spinbox but don't actually want the
+ // buttons, so set an empty item as a dummy
+ up.indicator: Item {}
+ down.indicator: Item {}
+
+ background: Rectangle {
+ radius: Style.slightlyRoundedButtonRadius
+ border.width: Style.normalBorderWidth
+ border.color: expireDateSpinBox.activeFocus ? Style.ncBlue : Style.menuBorder
+ color: Style.backgroundColor
+ }
+
+ value: expireDateReduced
+ from: minimumExpireDateReduced
+ to: maximumExpireDateReduced
+
+ textFromValue: (value, locale) => {
+ const dateFromValue = new Date(value * dayInMSecs);
+ return dateFromValue.toLocaleDateString(Qt.locale(), Locale.NarrowFormat);
+ }
+ valueFromText: (text, locale) => {
+ const dateFromText = parseDateString(text);
+ return Math.floor(dateFromText.getTime() / dayInMSecs);
+ }
+
+ editable: true
+ inputMethodHints: Qt.ImhDate | Qt.ImhFormattedNumbersOnly
+
+ enabled: root.expireDateEnabled &&
+ !root.waitingForExpireDateChange &&
+ !root.waitingForExpireDateEnabledChange
+
+ onValueModified: {
+ if (!enabled || !activeFocus) {
+ return;
+ }
+
+ root.setExpireDate(value * dayInMSecs);
+ root.waitingForExpireDateChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForExpireDateEnabledChange ||
+ root.waitingForExpireDateChange
+ running: visible
+ z: 1
+ }
+ }
+ }
+
+ CheckBox {
+ id: noteEnabledMenuItem
+
+ spacing: moreMenu.indicatorSpacing
+ padding: moreMenu.itemPadding
+ indicator.width: moreMenu.indicatorItemWidth
+ indicator.height: moreMenu.indicatorItemWidth
+
+ checkable: true
+ checked: root.noteEnabled
+ text: qsTr("Note to recipient")
+ enabled: !root.waitingForNoteEnabledChange
+
+ onClicked: {
+ root.toggleNoteToRecipient(checked);
+ root.waitingForNoteEnabledChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForNoteEnabledChange
+ running: visible
+ z: 1
+ }
+ }
+
+ RowLayout {
+ anchors.left: parent.left
+ anchors.leftMargin: moreMenu.itemPadding
+ anchors.right: parent.right
+ anchors.rightMargin: moreMenu.itemPadding
+ height: visible ? implicitHeight : 0
+ spacing: moreMenu.indicatorSpacing
+
+ visible: root.noteEnabled
+
+ Image {
+ Layout.preferredWidth: moreMenu.indicatorItemWidth
+ Layout.fillHeight: true
+
+ verticalAlignment: Image.AlignVCenter
+ horizontalAlignment: Image.AlignHCenter
+ fillMode: Image.Pad
+
+ source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
+ sourceSize.width: moreMenu.rowIconWidth
+ sourceSize.height: moreMenu.rowIconWidth
+ }
+
+ NCInputTextEdit {
+ id: noteTextEdit
+
+ Layout.fillWidth: true
+ height: visible ? Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight) : 0
+ submitButton.height: Math.min(Style.talkReplyTextFieldPreferredHeight, height - 2)
+
+ text: root.note
+ enabled: root.noteEnabled &&
+ !root.waitingForNoteChange &&
+ !root.waitingForNoteEnabledChange
+
+ onEditingFinished: if(text !== root.note) {
+ root.setNote(text);
+ root.waitingForNoteChange = true;
+ }
+
+ NCBusyIndicator {
+ anchors.fill: parent
+ visible: root.waitingForNoteChange ||
+ root.waitingForNoteEnabledChange
+ running: visible
+ z: 1
+ }
+ }
+ }
+
+ MenuItem {
+ spacing: moreMenu.indicatorSpacing
+ padding: moreMenu.itemPadding
+
+ icon.width: moreMenu.indicatorItemWidth
+ icon.height: moreMenu.indicatorItemWidth
+ icon.color: Style.ncTextColor
+ icon.source: "qrc:///client/theme/close.svg"
+ text: qsTr("Unshare")
+
+ onTriggered: root.deleteShare()
+ }
+
+ MenuItem {
+ height: visible ? implicitHeight : 0
+ spacing: moreMenu.indicatorSpacing
+ padding: moreMenu.itemPadding
+
+ icon.width: moreMenu.indicatorItemWidth
+ icon.height: moreMenu.indicatorItemWidth
+ icon.color: Style.ncTextColor
+ icon.source: "qrc:///client/theme/add.svg"
+ text: qsTr("Add another link")
+
+ visible: root.isLinkShare && root.canCreateLinkShares
+ enabled: visible
+
+ onTriggered: root.createNewLinkShare()
+ }
+ }
+ }
+ }
+}
diff --git a/src/gui/filedetails/ShareView.qml b/src/gui/filedetails/ShareView.qml
new file mode 100644
index 000000000..73e1636f3
--- /dev/null
+++ b/src/gui/filedetails/ShareView.qml
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+import "../tray"
+import "../"
+
+ColumnLayout {
+ id: root
+
+ property string localPath: ""
+ property var accountState: ({})
+ property FileDetails fileDetails: FileDetails {}
+ property int horizontalPadding: 0
+ property int iconSize: 32
+
+ readonly property bool sharingPossible: shareModel && shareModel.canShare && shareModel.sharingEnabled
+ readonly property bool userGroupSharingPossible: sharingPossible && shareModel.userGroupSharingEnabled
+ readonly property bool publicLinkSharingPossible: sharingPossible && shareModel.publicLinkSharesEnabled
+
+ readonly property bool loading: sharingPossible && (!shareModel ||
+ shareModel.fetchOngoing ||
+ !shareModel.hasInitialShareFetchCompleted ||
+ waitingForSharesToChange)
+ property bool waitingForSharesToChange: true // Gets changed to false when listview count changes
+ property bool stopWaitingForSharesToChangeOnPasswordError: false
+
+ readonly property ShareModel shareModel: ShareModel {
+ accountState: root.accountState
+ localPath: root.localPath
+
+ onSharesChanged: root.waitingForSharesToChange = false
+
+ onServerError: {
+ if(errorBox.text === "") {
+ errorBox.text = message;
+ } else {
+ errorBox.text += "\n\n" + message
+ }
+
+ errorBox.visible = true;
+ root.waitingForSharesToChange = false;
+ }
+
+ onPasswordSetError: if(root.stopWaitingForSharesToChangeOnPasswordError) {
+ root.waitingForSharesToChange = false;
+ root.stopWaitingForSharesToChangeOnPasswordError = false;
+ }
+
+ onRequestPasswordForLinkShare: shareRequiresPasswordDialog.open()
+ onRequestPasswordForEmailSharee: {
+ shareRequiresPasswordDialog.sharee = sharee;
+ shareRequiresPasswordDialog.open();
+ }
+ }
+
+ Dialog {
+ id: shareRequiresPasswordDialog
+
+ property var sharee
+
+ function discardDialog() {
+ sharee = undefined;
+ root.waitingForSharesToChange = false;
+ close();
+ }
+
+ anchors.centerIn: parent
+ width: parent.width * 0.8
+
+ title: qsTr("Password required for new share")
+ standardButtons: Dialog.Ok | Dialog.Cancel
+ modal: true
+ closePolicy: Popup.NoAutoClose
+
+ // TODO: Rather than setting all these palette colours manually,
+ // create a custom style and do it for all components globally
+ palette {
+ text: Style.ncTextColor
+ windowText: Style.ncTextColor
+ buttonText: Style.ncTextColor
+ light: Style.lightHover
+ midlight: Style.lightHover
+ mid: Style.ncSecondaryTextColor
+ dark: Style.menuBorder
+ button: Style.menuBorder
+ window: Style.backgroundColor
+ base: Style.backgroundColor
+ }
+
+ visible: false
+
+ onAccepted: {
+ if(sharee) {
+ root.shareModel.createNewUserGroupShareWithPasswordFromQml(sharee, dialogPasswordField.text);
+ sharee = undefined;
+ } else {
+ root.shareModel.createNewLinkShareWithPassword(dialogPasswordField.text);
+ }
+
+ root.stopWaitingForSharesToChangeOnPasswordError = true;
+ dialogPasswordField.text = "";
+ }
+ onDiscarded: discardDialog()
+ onRejected: discardDialog()
+
+ NCInputTextField {
+ id: dialogPasswordField
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ placeholderText: qsTr("Share password")
+ onAccepted: shareRequiresPasswordDialog.accept()
+ }
+ }
+
+ ErrorBox {
+ id: errorBox
+
+ Layout.fillWidth: true
+ Layout.leftMargin: root.horizontalPadding
+ Layout.rightMargin: root.horizontalPadding
+
+ showCloseButton: true
+ visible: false
+
+ onCloseButtonClicked: {
+ text = "";
+ visible = false;
+ }
+ }
+
+ ShareeSearchField {
+ Layout.fillWidth: true
+ Layout.leftMargin: root.horizontalPadding
+ Layout.rightMargin: root.horizontalPadding
+
+ visible: root.userGroupSharingPossible
+ enabled: visible && !root.loading
+
+ accountState: root.accountState
+ shareItemIsFolder: root.fileDetails && root.fileDetails.isFolder
+ shareeBlocklist: root.shareModel.sharees
+
+ onShareeSelected: {
+ root.waitingForSharesToChange = true;
+ root.shareModel.createNewUserGroupShareFromQml(sharee)
+ }
+ }
+
+ Loader {
+ id: sharesViewLoader
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.leftMargin: root.horizontalPadding
+ Layout.rightMargin: root.horizontalPadding
+
+ active: root.sharingPossible
+
+ sourceComponent: ScrollView {
+ id: scrollView
+ anchors.fill: parent
+
+ contentWidth: availableWidth
+ clip: true
+ enabled: root.sharingPossible
+
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+ ListView {
+ id: shareLinksListView
+
+ enabled: !root.loading
+ model: SortedShareModel {
+ shareModel: root.shareModel
+ }
+
+ delegate: ShareDelegate {
+ id: shareDelegate
+
+ Connections {
+ target: root.shareModel
+ // Though we try to handle this internally by listening to onPasswordChanged,
+ // with passwords we will get the same value from the model data when a
+ // password set has failed, meaning we won't be able to easily tell when we
+ // have had a response from the server in QML. So we listen to this signal
+ // directly from the model and do the reset of the password field manually.
+ function onPasswordSetError(shareId, errorCode, errorMessage) {
+ if(shareId !== model.shareId) {
+ return;
+ }
+ shareDelegate.resetPasswordField();
+ shareDelegate.showPasswordSetError(errorMessage);
+ }
+
+ function onServerError() {
+ if(shareId !== model.shareId) {
+ return;
+ }
+
+ shareDelegate.resetMenu();
+ }
+ }
+
+ iconSize: root.iconSize
+ canCreateLinkShares: root.publicLinkSharingPossible
+
+ onCreateNewLinkShare: {
+ root.waitingForSharesToChange = true;
+ shareModel.createNewLinkShare();
+ }
+ onDeleteShare: {
+ root.waitingForSharesToChange = true;
+ shareModel.deleteShareFromQml(model.share);
+ }
+
+ onToggleAllowEditing: shareModel.toggleShareAllowEditingFromQml(model.share, enable)
+ onToggleAllowResharing: shareModel.toggleShareAllowResharingFromQml(model.share, enable)
+ onTogglePasswordProtect: shareModel.toggleSharePasswordProtectFromQml(model.share, enable)
+ onToggleExpirationDate: shareModel.toggleShareExpirationDateFromQml(model.share, enable)
+ onToggleNoteToRecipient: shareModel.toggleShareNoteToRecipientFromQml(model.share, enable)
+
+ onSetLinkShareLabel: shareModel.setLinkShareLabelFromQml(model.share, label)
+ onSetExpireDate: shareModel.setShareExpireDateFromQml(model.share, milliseconds)
+ onSetPassword: shareModel.setSharePasswordFromQml(model.share, password)
+ onSetNote: shareModel.setShareNoteFromQml(model.share, note)
+ }
+
+ Loader {
+ id: sharesFetchingLoader
+ anchors.fill: parent
+ active: root.loading
+ z: Infinity
+
+ sourceComponent: Rectangle {
+ color: Style.backgroundColor
+ opacity: 0.5
+
+ NCBusyIndicator {
+ anchors.centerIn: parent
+ color: Style.ncSecondaryTextColor
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: sharingNotPossibleView
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.leftMargin: root.horizontalPadding
+ Layout.rightMargin: root.horizontalPadding
+
+ active: !root.sharingPossible
+
+ sourceComponent: Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ Label {
+ id: sharingDisabledLabel
+ width: parent.width
+ text: qsTr("Sharing is disabled")
+ color: Style.ncSecondaryTextColor
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ Label {
+ width: parent.width
+ text: qsTr("This item cannot be shared.")
+ color: Style.ncSecondaryTextColor
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ visible: !root.shareModel.canShare
+ }
+ Label {
+ width: parent.width
+ text: qsTr("Sharing is disabled.")
+ color: Style.ncSecondaryTextColor
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ visible: !root.shareModel.sharingEnabled
+ }
+ }
+ }
+}
diff --git a/src/gui/filedetails/ShareeDelegate.qml b/src/gui/filedetails/ShareeDelegate.qml
new file mode 100644
index 000000000..a9128cb4c
--- /dev/null
+++ b/src/gui/filedetails/ShareeDelegate.qml
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+
+ItemDelegate {
+ id: root
+
+ text: model.display
+}
diff --git a/src/gui/filedetails/ShareeSearchField.qml b/src/gui/filedetails/ShareeSearchField.qml
new file mode 100644
index 000000000..286d27391
--- /dev/null
+++ b/src/gui/filedetails/ShareeSearchField.qml
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+import "../tray"
+
+TextField {
+ id: root
+
+ signal shareeSelected(var sharee)
+
+ property var accountState: ({})
+ property bool shareItemIsFolder: false
+ property var shareeBlocklist: ({})
+ property ShareeModel shareeModel: ShareeModel {
+ accountState: root.accountState
+ shareItemIsFolder: root.shareItemIsFolder
+ searchString: root.text
+ shareeBlocklist: root.shareeBlocklist
+ }
+
+ readonly property int horizontalPaddingOffset: Style.trayHorizontalMargin
+ readonly property color placeholderColor: Style.menuBorder
+ readonly property double iconsScaleFactor: 0.6
+
+ function triggerSuggestionsVisibility() {
+ shareeListView.count > 0 && text !== "" ? suggestionsPopup.open() : suggestionsPopup.close();
+ }
+
+ placeholderText: qsTr("Search for users or groups…")
+ placeholderTextColor: placeholderColor
+ color: Style.ncTextColor
+ enabled: !shareeModel.fetchOngoing
+
+ onActiveFocusChanged: triggerSuggestionsVisibility()
+ onTextChanged: triggerSuggestionsVisibility()
+ Keys.onPressed: {
+ if(suggestionsPopup.visible) {
+ switch(event.key) {
+ case Qt.Key_Escape:
+ suggestionsPopup.close();
+ shareeListView.currentIndex = -1;
+ event.accepted = true;
+ break;
+
+ case Qt.Key_Up:
+ shareeListView.decrementCurrentIndex();
+ event.accepted = true;
+ break;
+
+ case Qt.Key_Down:
+ shareeListView.incrementCurrentIndex();
+ event.accepted = true;
+ break;
+
+ case Qt.Key_Enter:
+ case Qt.Key_Return:
+ if(shareeListView.currentIndex > -1) {
+ shareeListView.itemAtIndex(shareeListView.currentIndex).selectSharee();
+ event.accepted = true;
+ break;
+ }
+ }
+ } else {
+ switch(event.key) {
+ case Qt.Key_Down:
+ triggerSuggestionsVisibility();
+ event.accepted = true;
+ break;
+ }
+ }
+ }
+
+ leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + horizontalPaddingOffset
+ rightPadding: clearTextButton.width + clearTextButton.anchors.rightMargin + horizontalPaddingOffset
+
+ background: Rectangle {
+ radius: 5
+ border.color: parent.activeFocus ? UserModel.currentUser.accentColor : Style.menuBorder
+ border.width: 1
+ color: Style.backgroundColor
+ }
+
+ Image {
+ id: searchIcon
+ anchors {
+ top: parent.top
+ left: parent.left
+ bottom: parent.bottom
+ margins: 4
+ }
+
+ width: height
+
+ smooth: true
+ antialiasing: true
+ mipmap: true
+ fillMode: Image.PreserveAspectFit
+ horizontalAlignment: Image.AlignLeft
+
+ source: "image://svgimage-custom-color/search.svg" + "/" + root.placeholderColor
+ sourceSize: Qt.size(parent.height * root.iconsScaleFactor, parent.height * root.iconsScaleFactor)
+
+ visible: !root.shareeModel.fetchOngoing
+ }
+
+ NCBusyIndicator {
+ id: busyIndicator
+
+ anchors {
+ top: parent.top
+ left: parent.left
+ bottom: parent.bottom
+ }
+
+ width: height
+ color: root.placeholderColor
+ visible: root.shareeModel.fetchOngoing
+ running: visible
+ }
+
+ Image {
+ id: clearTextButton
+
+ anchors {
+ top: parent.top
+ right: parent.right
+ bottom: parent.bottom
+ margins: 4
+ }
+
+ width: height
+
+ smooth: true
+ antialiasing: true
+ mipmap: true
+ fillMode: Image.PreserveAspectFit
+
+ source: "image://svgimage-custom-color/clear.svg" + "/" + root.placeholderColor
+ sourceSize: Qt.size(parent.height * root.iconsScaleFactor, parent.height * root.iconsScaleFactor)
+
+ visible: root.text
+
+ MouseArea {
+ id: clearTextButtonMouseArea
+ anchors.fill: parent
+ onClicked: root.clear()
+ }
+ }
+
+ Popup {
+ id: suggestionsPopup
+
+ width: root.width
+ height: 100
+ y: root.height
+
+ // TODO: Rather than setting all these palette colours manually,
+ // create a custom style and do it for all components globally
+ palette {
+ text: Style.ncTextColor
+ windowText: Style.ncTextColor
+ buttonText: Style.ncTextColor
+ light: Style.lightHover
+ midlight: Style.lightHover
+ mid: Style.ncSecondaryTextColor
+ dark: Style.menuBorder
+ button: Style.menuBorder
+ window: Style.backgroundColor
+ base: Style.backgroundColor
+ }
+
+ contentItem: ScrollView {
+ id: suggestionsScrollView
+
+ clip: true
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+ ListView {
+ id: shareeListView
+
+ spacing: 0
+ currentIndex: -1
+ interactive: true
+
+ highlight: Rectangle {
+ width: shareeListView.currentItem.width
+ height: shareeListView.currentItem.height
+ color: Style.lightHover
+ }
+ highlightFollowsCurrentItem: true
+ highlightMoveDuration: 0
+ highlightResizeDuration: 0
+ highlightRangeMode: ListView.ApplyRange
+ preferredHighlightBegin: 0
+ preferredHighlightEnd: suggestionsScrollView.height
+
+ onCountChanged: root.triggerSuggestionsVisibility()
+
+ model: root.shareeModel
+ delegate: ShareeDelegate {
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ function selectSharee() {
+ root.shareeSelected(model.sharee);
+ suggestionsPopup.close();
+
+ root.clear();
+ }
+
+ onHoveredChanged: if (hovered) {
+ // When we set the currentIndex the list view will scroll...
+ // unless we tamper with the preferred highlight points to stop this.
+ const savedPreferredHighlightBegin = shareeListView.preferredHighlightBegin;
+ const savedPreferredHighlightEnd = shareeListView.preferredHighlightEnd;
+ // Set overkill values to make sure no scroll happens when we hover with mouse
+ shareeListView.preferredHighlightBegin = -suggestionsScrollView.height;
+ shareeListView.preferredHighlightEnd = suggestionsScrollView.height * 2;
+
+ shareeListView.currentIndex = index
+
+ // Reset original values so keyboard navigation makes list view scroll
+ shareeListView.preferredHighlightBegin = savedPreferredHighlightBegin;
+ shareeListView.preferredHighlightEnd = savedPreferredHighlightEnd;
+ }
+ onClicked: selectSharee()
+ }
+ }
+ }
+ }
+}
diff --git a/src/gui/filedetails/filedetails.cpp b/src/gui/filedetails/filedetails.cpp
new file mode 100644
index 000000000..2f0e29a10
--- /dev/null
+++ b/src/gui/filedetails/filedetails.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2022 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 <QDateTime>
+
+#include "filedetails.h"
+#include "folderman.h"
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcFileDetails, "nextcloud.gui.filedetails", QtInfoMsg)
+
+FileDetails::FileDetails(QObject *parent)
+ : QObject(parent)
+{
+ _filelockStateUpdateTimer.setInterval(6000);
+ _filelockStateUpdateTimer.setSingleShot(false);
+ connect(&_filelockStateUpdateTimer, &QTimer::timeout, this, &FileDetails::updateLockExpireString);
+}
+
+void FileDetails::refreshFileDetails()
+{
+ _fileInfo.refresh();
+ Q_EMIT fileChanged();
+}
+
+QString FileDetails::localPath() const
+{
+ return _localPath;
+}
+
+void FileDetails::setLocalPath(const QString &localPath)
+{
+ if(localPath.isEmpty()) {
+ return;
+ }
+
+ if(!_localPath.isEmpty()) {
+ _fileWatcher.removePath(_localPath);
+ }
+
+ if(_fileInfo.exists()) {
+ disconnect(&_fileWatcher, &QFileSystemWatcher::fileChanged, this, &FileDetails::refreshFileDetails);
+ }
+
+ _localPath = localPath;
+ _fileInfo = QFileInfo(localPath);
+
+ _fileWatcher.addPath(localPath);
+ connect(&_fileWatcher, &QFileSystemWatcher::fileChanged, this, &FileDetails::refreshFileDetails);
+
+ const auto folder = FolderMan::instance()->folderForPath(_localPath);
+ const auto file = _localPath.mid(folder->cleanPath().length() + 1);
+
+ if (!folder->journalDb()->getFileRecord(file, &_fileRecord)) {
+ qCWarning(lcFileDetails) << "Invalid file record for path:"
+ << _localPath
+ << "will not load file details.";
+ }
+
+ _filelockState = _fileRecord._lockstate;
+ updateLockExpireString();
+
+ Q_EMIT fileChanged();
+}
+
+QString FileDetails::name() const
+{
+ return _fileInfo.fileName();
+}
+
+QString FileDetails::sizeString() const
+{
+ return _locale.formattedDataSize(_fileInfo.size());
+}
+
+QString FileDetails::lastChangedString() const
+{
+ static constexpr auto secsInMinute = 60;
+ static constexpr auto secsInHour = secsInMinute * 60;
+ static constexpr auto secsInDay = secsInHour * 24;
+ static constexpr auto secsInMonth = secsInDay * 30;
+ static constexpr auto secsInYear = secsInMonth * 12;
+
+ const auto elapsedSecs = _fileInfo.lastModified().secsTo(QDateTime::currentDateTime());
+
+ if(elapsedSecs < 60) {
+ const auto elapsedSecsAsInt = static_cast<int>(elapsedSecs);
+ return tr("%1 second(s) ago", "seconds elapsed since file last modified", elapsedSecsAsInt).arg(elapsedSecsAsInt);
+ } else if (elapsedSecs < secsInHour) {
+ const auto elapsedMinutes = static_cast<int>(elapsedSecs / secsInMinute);
+ return tr("%1 minute(s) ago", "minutes elapsed since file last modified", elapsedMinutes).arg(elapsedMinutes);
+ } else if (elapsedSecs < secsInDay) {
+ const auto elapsedHours = static_cast<int>(elapsedSecs / secsInHour);
+ return tr("%1 hour(s) ago", "hours elapsed since file last modified", elapsedHours).arg(elapsedHours);
+ } else if (elapsedSecs < secsInMonth) {
+ const auto elapsedDays = static_cast<int>(elapsedSecs / secsInDay);
+ return tr("%1 day(s) ago", "days elapsed since file last modified", elapsedDays).arg(elapsedDays);
+ } else if (elapsedSecs < secsInYear) {
+ const auto elapsedMonths = static_cast<int>(elapsedSecs / secsInMonth);
+ return tr("%1 month(s) ago", "months elapsed since file last modified", elapsedMonths).arg(elapsedMonths);
+ } else {
+ const auto elapsedYears = static_cast<int>(elapsedSecs / secsInYear);
+ return tr("%1 year(s) ago", "years elapsed since file last modified", elapsedYears).arg(elapsedYears);
+ }
+}
+
+QString FileDetails::iconUrl() const
+{
+ return QStringLiteral("image://tray-image-provider/:/fileicon") + _localPath;
+}
+
+QString FileDetails::lockExpireString() const
+{
+ return _lockExpireString;
+}
+
+void FileDetails::updateLockExpireString()
+{
+ if(!_filelockState._locked) {
+ _filelockStateUpdateTimer.stop();
+ _lockExpireString = QString();
+ Q_EMIT lockExpireStringChanged();
+ return;
+ }
+
+ if(!_filelockStateUpdateTimer.isActive()) {
+ _filelockStateUpdateTimer.start();
+ }
+
+ static constexpr auto SECONDS_PER_MINUTE = 60;
+ const auto lockExpirationTime = _filelockState._lockTime + _filelockState._lockTimeout;
+ const auto remainingTime = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime));
+ const auto remainingTimeInMinutes = static_cast<int>(remainingTime > 0 ? remainingTime / SECONDS_PER_MINUTE : 0);
+
+ _lockExpireString = tr("Locked by %1 - Expires in %2 minute(s)", "remaining time before lock expires", remainingTimeInMinutes).arg(_filelockState._lockOwnerDisplayName).arg(remainingTimeInMinutes);
+ Q_EMIT lockExpireStringChanged();
+}
+
+bool FileDetails::isFolder() const
+{
+ return _fileInfo.isDir();
+}
+
+} // namespace OCC
diff --git a/src/gui/filedetails/filedetails.h b/src/gui/filedetails/filedetails.h
new file mode 100644
index 000000000..3a5d1e529
--- /dev/null
+++ b/src/gui/filedetails/filedetails.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <QFileInfo>
+#include <QFileSystemWatcher>
+#include <QLocale>
+#include <QTimer>
+
+#include "common/syncjournalfilerecord.h"
+
+namespace OCC {
+
+class FileDetails : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged)
+ Q_PROPERTY(QString name READ name NOTIFY fileChanged)
+ Q_PROPERTY(QString sizeString READ sizeString NOTIFY fileChanged)
+ Q_PROPERTY(QString lastChangedString READ lastChangedString NOTIFY fileChanged)
+ Q_PROPERTY(QString iconUrl READ iconUrl NOTIFY fileChanged)
+ Q_PROPERTY(QString lockExpireString READ lockExpireString NOTIFY lockExpireStringChanged)
+ Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged)
+
+public:
+ explicit FileDetails(QObject *parent = nullptr);
+
+ [[nodiscard]] QString localPath() const;
+ [[nodiscard]] QString name() const;
+ [[nodiscard]] QString sizeString() const;
+ [[nodiscard]] QString lastChangedString() const;
+ [[nodiscard]] QString iconUrl() const;
+ [[nodiscard]] QString lockExpireString() const;
+ [[nodiscard]] bool isFolder() const;
+
+public slots:
+ void setLocalPath(const QString &localPath);
+
+signals:
+ void localPathChanged();
+ void fileChanged();
+ void lockExpireStringChanged();
+ void isFolderChanged();
+
+private slots:
+ void refreshFileDetails();
+ void updateLockExpireString();
+
+private:
+ QString _localPath;
+
+ QFileInfo _fileInfo;
+ QFileSystemWatcher _fileWatcher;
+ SyncJournalFileRecord _fileRecord;
+ SyncJournalFileLockInfo _filelockState;
+ QByteArray _numericFileId;
+ QString _lockExpireString;
+ QTimer _filelockStateUpdateTimer;
+
+ QLocale _locale;
+};
+
+} // namespace OCC
diff --git a/src/gui/filedetails/shareemodel.cpp b/src/gui/filedetails/shareemodel.cpp
new file mode 100644
index 000000000..2fb9be8f6
--- /dev/null
+++ b/src/gui/filedetails/shareemodel.cpp
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2022 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 "shareemodel.h"
+
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QJsonArray>
+
+#include "ocsshareejob.h"
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcShareeModel, "com.nextcloud.shareemodel")
+
+ShareeModel::ShareeModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+ _searchRateLimitingTimer.setSingleShot(true);
+ _searchRateLimitingTimer.setInterval(500);
+ connect(&_searchRateLimitingTimer, &QTimer::timeout, this, &ShareeModel::fetch);
+}
+
+// ---------------------- QAbstractListModel methods ---------------------- //
+
+int ShareeModel::rowCount(const QModelIndex &parent) const
+{
+ if(parent.isValid() || !_accountState) {
+ return 0;
+ }
+
+ return _sharees.count();
+}
+
+QHash<int, QByteArray> ShareeModel::roleNames() const
+{
+ auto roles = QAbstractListModel::roleNames();
+ roles[ShareeRole] = "sharee";
+ roles[AutoCompleterStringMatchRole] = "autoCompleterStringMatch";
+
+ return roles;
+}
+
+QVariant ShareeModel::data(const QModelIndex &index, const int role) const
+{
+ Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
+
+ const auto sharee = _sharees.at(index.row());
+
+ if(sharee.isNull()) {
+ return {};
+ }
+
+ switch(role) {
+ case Qt::DisplayRole:
+ return sharee->format();
+ case AutoCompleterStringMatchRole:
+ // Don't show this to the user
+ return QString(sharee->displayName() + " (" + sharee->shareWith() + ")");
+ case ShareeRole:
+ return QVariant::fromValue(sharee);
+ }
+
+ qCWarning(lcShareeModel) << "Got unknown role" << role << "returning null value.";
+ return {};
+}
+
+// --------------------------- QPROPERTY methods --------------------------- //
+
+AccountState *ShareeModel::accountState() const
+{
+ return _accountState;
+}
+
+void ShareeModel::setAccountState(AccountState *accountState)
+{
+ if (accountState == _accountState) {
+ return;
+ }
+
+ _accountState = accountState;
+ Q_EMIT accountStateChanged();
+}
+
+bool ShareeModel::shareItemIsFolder() const
+{
+ return _shareItemIsFolder;
+}
+
+void ShareeModel::setShareItemIsFolder(const bool shareItemIsFolder)
+{
+ if (shareItemIsFolder == _shareItemIsFolder) {
+ return;
+ }
+
+ _shareItemIsFolder = shareItemIsFolder;
+ Q_EMIT shareItemIsFolderChanged();
+}
+
+QString ShareeModel::searchString() const
+{
+ return _searchString;
+}
+
+void ShareeModel::setSearchString(const QString &searchString)
+{
+ if (searchString == _searchString) {
+ return;
+ }
+
+ _searchString = searchString;
+ Q_EMIT searchStringChanged();
+
+ _searchRateLimitingTimer.start();
+}
+
+bool ShareeModel::fetchOngoing() const
+{
+ return _fetchOngoing;
+}
+
+ShareeModel::LookupMode ShareeModel::lookupMode() const
+{
+ return _lookupMode;
+}
+
+void ShareeModel::setLookupMode(const ShareeModel::LookupMode lookupMode)
+{
+ if (lookupMode == _lookupMode) {
+ return;
+ }
+
+ _lookupMode = lookupMode;
+ Q_EMIT lookupModeChanged();
+}
+
+QVariantList ShareeModel::shareeBlocklist() const
+{
+ QVariantList returnSharees;
+ for (const auto &sharee : _shareeBlocklist) {
+ returnSharees.append(QVariant::fromValue(sharee));
+ }
+ return returnSharees;
+}
+
+void ShareeModel::setShareeBlocklist(const QVariantList shareeBlocklist)
+{
+ _shareeBlocklist.clear();
+ for (const auto &sharee : shareeBlocklist) {
+ _shareeBlocklist.append(sharee.value<ShareePtr>());
+ }
+ Q_EMIT shareeBlocklistChanged();
+
+ filterSharees();
+}
+
+// ------------------------- Internal data methods ------------------------- //
+
+void ShareeModel::fetch()
+{
+ if(!_accountState || !_accountState->account() || _searchString.isEmpty()) {
+ qCInfo(lcShareeModel) << "Not fetching sharees for searchString: " << _searchString;
+ return;
+ }
+
+ _fetchOngoing = true;
+ Q_EMIT fetchOngoingChanged();
+
+ const auto shareItemTypeString = _shareItemIsFolder ? QStringLiteral("folder") : QStringLiteral("file");
+
+ auto *job = new OcsShareeJob(_accountState->account());
+
+ connect(job, &OcsShareeJob::shareeJobFinished, this, &ShareeModel::shareesFetched);
+ connect(job, &OcsJob::ocsError, this, [&](const int statusCode, const QString &message) {
+ _fetchOngoing = false;
+ Q_EMIT fetchOngoingChanged();
+ Q_EMIT ShareeModel::displayErrorMessage(statusCode, message);
+ });
+
+ job->getSharees(_searchString, shareItemTypeString, 1, 50, _lookupMode == LookupMode::GlobalSearch ? true : false);
+}
+
+void ShareeModel::shareesFetched(const QJsonDocument &reply)
+{
+ _fetchOngoing = false;
+ Q_EMIT fetchOngoingChanged();
+
+ qCInfo(lcShareeModel) << "Reply: " << reply;
+
+ QVector<ShareePtr> newSharees;
+
+ const QStringList shareeTypes {"users", "groups", "emails", "remotes", "circles", "rooms"};
+
+ const auto appendSharees = [this, &shareeTypes, &newSharees](const QJsonObject &data) {
+ for (const auto &shareeType : shareeTypes) {
+ const auto category = data.value(shareeType).toArray();
+
+ for (const auto &sharee : category) {
+ const auto shareeJsonObject = sharee.toObject();
+ const auto parsedSharee = parseSharee(shareeJsonObject);
+
+ const auto shareeInBlacklistIt = std::find_if(_shareeBlocklist.cbegin(),
+ _shareeBlocklist.cend(),
+ [&parsedSharee](const ShareePtr &blacklistSharee) {
+ return parsedSharee->type() == blacklistSharee->type() &&
+ parsedSharee->shareWith() == blacklistSharee->shareWith();
+ });
+
+ if (shareeInBlacklistIt != _shareeBlocklist.cend()) {
+ continue;
+ }
+
+ newSharees.append(parsedSharee);
+ }
+ }
+ };
+ const auto replyDataObject = reply.object().value("ocs").toObject().value("data").toObject();
+ const auto replyDataExactMatchObject = replyDataObject.value("exact").toObject();
+
+ appendSharees(replyDataObject);
+ appendSharees(replyDataExactMatchObject);
+
+ Q_EMIT beginResetModel();
+ _sharees = newSharees;
+ Q_EMIT endResetModel();
+
+ Q_EMIT shareesReady();
+}
+
+ShareePtr ShareeModel::parseSharee(const QJsonObject &data) const
+{
+ auto displayName = data.value("label").toString();
+ const auto shareWith = data.value("value").toObject().value("shareWith").toString();
+ const auto type = (Sharee::Type)data.value("value").toObject().value("shareType").toInt();
+ const auto additionalInfo = data.value("value").toObject().value("shareWithAdditionalInfo").toString();
+ if (!additionalInfo.isEmpty()) {
+ displayName = tr("%1 (%2)", "sharee (shareWithAdditionalInfo)").arg(displayName, additionalInfo);
+ }
+
+ return ShareePtr(new Sharee(shareWith, displayName, type));
+}
+
+void ShareeModel::filterSharees()
+{
+ auto it = _sharees.begin();
+
+ while (it != _sharees.end()) {
+ const auto sharee = *it;
+ const auto shareeInBlacklistIt = std::find_if(_shareeBlocklist.cbegin(), _shareeBlocklist.cend(), [&sharee](const ShareePtr &blacklistSharee) {
+ return sharee->type() == blacklistSharee->type() &&
+ sharee->shareWith() == blacklistSharee->shareWith();
+ });
+
+ if (shareeInBlacklistIt != _shareeBlocklist.end()) {
+ const auto row = it - _sharees.begin();
+ beginRemoveRows({}, row, row);
+ it = _sharees.erase(it);
+ endRemoveRows();
+ } else {
+ ++it;
+ }
+ }
+
+ Q_EMIT shareesReady();
+}
+
+}
diff --git a/src/gui/filedetails/shareemodel.h b/src/gui/filedetails/shareemodel.h
new file mode 100644
index 000000000..26a0b533e
--- /dev/null
+++ b/src/gui/filedetails/shareemodel.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QTimer>
+
+#include "accountstate.h"
+#include "sharee.h"
+
+class QJsonDocument;
+class QJsonObject;
+
+namespace OCC {
+
+class ShareeModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged)
+ Q_PROPERTY(bool shareItemIsFolder READ shareItemIsFolder WRITE setShareItemIsFolder NOTIFY shareItemIsFolderChanged)
+ Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged)
+ Q_PROPERTY(bool fetchOngoing READ fetchOngoing NOTIFY fetchOngoingChanged)
+ Q_PROPERTY(LookupMode lookupMode READ lookupMode WRITE setLookupMode NOTIFY lookupModeChanged)
+ Q_PROPERTY(QVariantList shareeBlocklist READ shareeBlocklist WRITE setShareeBlocklist NOTIFY shareeBlocklistChanged)
+
+public:
+ enum class LookupMode {
+ LocalSearch = 0,
+ GlobalSearch = 1,
+ };
+ Q_ENUM(LookupMode);
+
+ enum Roles {
+ ShareeRole = Qt::UserRole + 1,
+ AutoCompleterStringMatchRole,
+ };
+ Q_ENUM(Roles);
+
+ explicit ShareeModel(QObject *parent = nullptr);
+
+ using ShareeSet = QVector<ShareePtr>; // FIXME: make it a QSet<Sharee> when Sharee can be compared
+
+ [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+ [[nodiscard]] QVariant data(const QModelIndex &index, const int role) const override;
+
+ [[nodiscard]] AccountState *accountState() const;
+ [[nodiscard]] bool shareItemIsFolder() const;
+ [[nodiscard]] QString searchString() const;
+ [[nodiscard]] bool fetchOngoing() const;
+ [[nodiscard]] LookupMode lookupMode() const;
+ [[nodiscard]] QVariantList shareeBlocklist() const;
+
+signals:
+ void accountStateChanged();
+ void shareItemIsFolderChanged();
+ void searchStringChanged();
+ void fetchOngoingChanged();
+ void lookupModeChanged();
+ void shareeBlocklistChanged();
+
+ void shareesReady();
+ void displayErrorMessage(const int code, const QString &message);
+
+public slots:
+ void setAccountState(AccountState *accountState);
+ void setShareItemIsFolder(const bool shareItemIsFolder);
+ void setSearchString(const QString &searchString);
+ void setLookupMode(const LookupMode lookupMode);
+ void setShareeBlocklist(const QVariantList shareeBlocklist);
+
+ void fetch();
+
+private slots:
+ void shareesFetched(const QJsonDocument &reply);
+ void filterSharees();
+
+private:
+ [[nodiscard]] ShareePtr parseSharee(const QJsonObject &data) const;
+
+ QTimer _searchRateLimitingTimer;
+
+ AccountState *_accountState;
+ QString _searchString;
+ bool _shareItemIsFolder = false;
+ bool _fetchOngoing = false;
+ LookupMode _lookupMode = LookupMode::LocalSearch;
+
+ QVector<ShareePtr> _sharees;
+ QVector<ShareePtr> _shareeBlocklist;
+};
+
+}
diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp
new file mode 100644
index 000000000..fdf2b28de
--- /dev/null
+++ b/src/gui/filedetails/sharemodel.cpp
@@ -0,0 +1,1030 @@
+/*
+ * Copyright (C) 2022 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 "sharemodel.h"
+
+#include <QFileInfo>
+#include <QTimeZone>
+
+#include "account.h"
+#include "folderman.h"
+#include "theme.h"
+#include "wordlist.h"
+
+namespace {
+
+static const QString placeholderLinkShareId = QStringLiteral("__placeholderLinkShareId__");
+
+QString createRandomPassword()
+{
+ const auto words = OCC::WordList::getRandomWords(10);
+
+ const auto addFirstLetter = [](const QString &current, const QString &next) -> QString {
+ return current + next.at(0);
+ };
+
+ return std::accumulate(std::cbegin(words), std::cend(words), QString(), addFirstLetter);
+}
+}
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcShareModel, "com.nextcloud.sharemodel")
+
+ShareModel::ShareModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+// ---------------------- QAbstractListModel methods ---------------------- //
+
+int ShareModel::rowCount(const QModelIndex &parent) const
+{
+ if(parent.isValid() || !_accountState || _localPath.isEmpty()) {
+ return 0;
+ }
+
+ return _shares.count();
+}
+
+QHash<int, QByteArray> ShareModel::roleNames() const
+{
+ auto roles = QAbstractListModel::roleNames();
+ roles[ShareRole] = "share";
+ roles[ShareTypeRole] = "shareType";
+ roles[ShareIdRole] = "shareId";
+ roles[IconUrlRole] = "iconUrl";
+ roles[AvatarUrlRole] = "avatarUrl";
+ roles[LinkRole] = "link";
+ roles[LinkShareNameRole] = "linkShareName";
+ roles[LinkShareLabelRole] = "linkShareLabel";
+ roles[NoteEnabledRole] = "noteEnabled";
+ roles[NoteRole] = "note";
+ roles[ExpireDateEnabledRole] = "expireDateEnabled";
+ roles[ExpireDateEnforcedRole] = "expireDateEnforced";
+ roles[ExpireDateRole] = "expireDate";
+ roles[EnforcedMaximumExpireDateRole] = "enforcedMaximumExpireDate";
+ roles[PasswordProtectEnabledRole] = "passwordProtectEnabled";
+ roles[PasswordRole] = "password";
+ roles[PasswordEnforcedRole] = "passwordEnforced";
+ roles[EditingAllowedRole] = "editingAllowed";
+
+ return roles;
+}
+
+QVariant ShareModel::data(const QModelIndex &index, const int role) const
+{
+ Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
+
+ const auto share = _shares.at(index.row());
+
+ if (!share) {
+ return {};
+ }
+
+ // Some roles only provide values for the link and user/group share types
+ if(const auto linkShare = share.objectCast<LinkShare>()) {
+ switch(role) {
+ case LinkRole:
+ return linkShare->getLink();
+ case LinkShareNameRole:
+ return linkShare->getName();
+ case LinkShareLabelRole:
+ return linkShare->getLabel();
+ case NoteEnabledRole:
+ return !linkShare->getNote().isEmpty();
+ case NoteRole:
+ return linkShare->getNote();
+ case ExpireDateEnabledRole:
+ return linkShare->getExpireDate().isValid();
+ case ExpireDateRole:
+ {
+ const auto startOfExpireDayUTC = linkShare->getExpireDate().startOfDay(QTimeZone::utc());
+ return startOfExpireDayUTC.toMSecsSinceEpoch();
+ }
+ }
+
+ } else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
+ switch(role) {
+ case NoteEnabledRole:
+ return !userGroupShare->getNote().isEmpty();
+ case NoteRole:
+ return userGroupShare->getNote();
+ case ExpireDateEnabledRole:
+ return userGroupShare->getExpireDate().isValid();
+ case ExpireDateRole:
+ {
+ const auto startOfExpireDayUTC = userGroupShare->getExpireDate().startOfDay(QTimeZone::utc());
+ return startOfExpireDayUTC.toMSecsSinceEpoch();
+ }
+ }
+ }
+
+ switch(role) {
+ case Qt::DisplayRole:
+ return displayStringForShare(share);
+ case ShareRole:
+ return QVariant::fromValue(share);
+ case ShareTypeRole:
+ return share->getShareType();
+ case ShareIdRole:
+ return share->getId();
+ case IconUrlRole:
+ return iconUrlForShare(share);
+ case AvatarUrlRole:
+ return avatarUrlForShare(share);
+ case ExpireDateEnforcedRole:
+ return expireDateEnforcedForShare(share);
+ case EnforcedMaximumExpireDateRole:
+ return enforcedMaxExpireDateForShare(share);
+ case PasswordProtectEnabledRole:
+ return share->isPasswordSet();
+ case PasswordRole:
+ if (!share->isPasswordSet() || !_shareIdRecentlySetPasswords.contains(share->getId())) {
+ return {};
+ }
+ return _shareIdRecentlySetPasswords.value(share->getId());
+ case PasswordEnforcedRole:
+ return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid() &&
+ ((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced()) ||
+ (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword()));
+ case EditingAllowedRole:
+ return share->getPermissions().testFlag(SharePermissionUpdate);
+
+ // Deal with roles that only return certain values for link or user/group share types
+ case NoteEnabledRole:
+ case ExpireDateEnabledRole:
+ return false;
+ case LinkRole:
+ case LinkShareNameRole:
+ case LinkShareLabelRole:
+ case NoteRole:
+ case ExpireDateRole:
+ return {};
+ }
+
+ qCWarning(lcShareModel) << "Got unknown role" << role
+ << "for share of type" << share->getShareType()
+ << "so returning null value.";
+ return {};
+}
+
+// ---------------------- Internal model data methods ---------------------- //
+
+void ShareModel::resetData()
+{
+ beginResetModel();
+
+ _folder = nullptr;
+ _sharePath.clear();
+ _maxSharingPermissions = {};
+ _numericFileId.clear();
+ _manager.clear();
+ _shares.clear();
+ _fetchOngoing = false;
+ _hasInitialShareFetchCompleted = false;
+ _sharees.clear();
+
+ Q_EMIT sharePermissionsChanged();
+ Q_EMIT fetchOngoingChanged();
+ Q_EMIT hasInitialShareFetchCompletedChanged();
+ Q_EMIT shareesChanged();
+
+ endResetModel();
+}
+
+void ShareModel::updateData()
+{
+ resetData();
+
+ if (_localPath.isEmpty() || !_accountState || _accountState->account().isNull()) {
+ qCWarning(lcShareModel) << "Not updating share model data. Local path is:" << _localPath
+ << "Is account state null:" << !_accountState;
+ return;
+ }
+
+ if (!sharingEnabled()) {
+ qCWarning(lcShareModel) << "Server does not support sharing";
+ return;
+ }
+
+ _folder = FolderMan::instance()->folderForPath(_localPath);
+
+ if (!_folder) {
+ qCWarning(lcShareModel) << "Could not update share model data for" << _localPath << "no responsible folder found";
+ resetData();
+ return;
+ }
+
+ qCDebug(lcShareModel) << "Updating share model data now.";
+
+ const auto relPath = _localPath.mid(_folder->cleanPath().length() + 1);
+ _sharePath = _folder->remotePathTrailingSlash() + relPath;
+
+ SyncJournalFileRecord fileRecord;
+ auto resharingAllowed = true; // lets assume the good
+
+ if(_folder->journalDb()->getFileRecord(relPath, &fileRecord) && fileRecord.isValid()) {
+ if (!fileRecord._remotePerm.isNull() &&
+ !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
+
+ resharingAllowed = false;
+ }
+ }
+
+ _maxSharingPermissions = resharingAllowed ? SharePermissions(_accountState->account()->capabilities().shareDefaultPermissions()) : SharePermissions({});
+ Q_EMIT sharePermissionsChanged();
+
+ _numericFileId = fileRecord.numericFileId();
+
+ // Will get added when shares are fetched if no link shares are fetched
+ _placeholderLinkShare.reset(new Share(_accountState->account(),
+ placeholderLinkShareId,
+ _accountState->account()->id(),
+ _accountState->account()->davDisplayName(),
+ _sharePath,
+ Share::TypePlaceholderLink));
+
+ auto job = new PropfindJob(_accountState->account(), _sharePath);
+ job->setProperties(
+ QList<QByteArray>()
+ << "https://open-collaboration-services.org/ns:share-permissions"
+ << "https://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
+ << "https://owncloud.org/ns:privatelink");
+ job->setTimeout(10 * 1000);
+ connect(job, &PropfindJob::result, this, &ShareModel::slotPropfindReceived);
+ connect(job, &PropfindJob::finishedWithError, this, [&](const QNetworkReply *reply) {
+ qCWarning(lcShareModel) << "Propfind for" << _sharePath << "failed";
+ _fetchOngoing = false;
+ Q_EMIT fetchOngoingChanged();
+ Q_EMIT serverError(reply->error(), reply->errorString());
+ });
+
+ _fetchOngoing = true;
+ Q_EMIT fetchOngoingChanged();
+ job->start();
+
+ initShareManager();
+}
+
+void ShareModel::initShareManager()
+{
+ if (!_accountState || _accountState->account().isNull()) {
+ return;
+ }
+
+ bool sharingPossible = true;
+ if (!publicLinkSharesEnabled()) {
+ qCWarning(lcSharing) << "Link shares have been disabled";
+ sharingPossible = false;
+ } else if (!canShare()) {
+ qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission.";
+ sharingPossible = false;
+ }
+
+ if (_manager.isNull() && sharingPossible) {
+ _manager.reset(new ShareManager(_accountState->account(), this));
+ connect(_manager.data(), &ShareManager::sharesFetched, this, &ShareModel::slotSharesFetched);
+ connect(_manager.data(), &ShareManager::shareCreated, this, [&]{ _manager->fetchShares(_sharePath); });
+ connect(_manager.data(), &ShareManager::linkShareCreated, this, &ShareModel::slotAddShare);
+ connect(_manager.data(), &ShareManager::linkShareRequiresPassword, this, &ShareModel::requestPasswordForLinkShare);
+ connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message){
+ _hasInitialShareFetchCompleted = true;
+ Q_EMIT hasInitialShareFetchCompletedChanged();
+ serverError(code, message);
+ });
+
+ _manager->fetchShares(_sharePath);
+ }
+}
+
+void ShareModel::handlePlaceholderLinkShare()
+{
+ // We want to add the placeholder if there are no link shares and
+ // if we are not already showing the placeholder link share
+ auto linkSharePresent = false;
+ auto placeholderLinkSharePresent = false;
+
+ for (const auto &share : _shares) {
+ const auto shareType = share->getShareType();
+
+ if (!linkSharePresent && shareType == Share::TypeLink) {
+ linkSharePresent = true;
+ } else if (!placeholderLinkSharePresent && shareType == Share::TypePlaceholderLink) {
+ placeholderLinkSharePresent = true;
+ }
+
+ if(linkSharePresent && placeholderLinkSharePresent) {
+ break;
+ }
+ }
+
+ if (linkSharePresent && placeholderLinkSharePresent) {
+ slotRemoveShareWithId(placeholderLinkShareId);
+ } else if (!linkSharePresent && !placeholderLinkSharePresent) {
+ slotAddShare(_placeholderLinkShare);
+ }
+}
+
+void ShareModel::slotPropfindReceived(const QVariantMap &result)
+{
+ _fetchOngoing = false;
+ Q_EMIT fetchOngoingChanged();
+
+ const QVariant receivedPermissions = result["share-permissions"];
+ if (!receivedPermissions.toString().isEmpty()) {
+ _maxSharingPermissions = static_cast<SharePermissions>(receivedPermissions.toInt());
+ Q_EMIT sharePermissionsChanged();
+ qCInfo(lcShareModel) << "Received sharing permissions for" << _sharePath << _maxSharingPermissions;
+ }
+
+ const auto privateLinkUrl = result["privatelink"].toString();
+ const auto numericFileId = result["fileid"].toByteArray();
+
+ if (!privateLinkUrl.isEmpty()) {
+ qCInfo(lcShareModel) << "Received private link url for" << _sharePath << privateLinkUrl;
+ _privateLinkUrl = privateLinkUrl;
+ } else if (!numericFileId.isEmpty()) {
+ qCInfo(lcShareModel) << "Received numeric file id for" << _sharePath << numericFileId;
+ _privateLinkUrl = _accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded);
+ }
+}
+
+void ShareModel::slotSharesFetched(const QList<SharePtr> &shares)
+{
+ if(!_hasInitialShareFetchCompleted) {
+ _hasInitialShareFetchCompleted = true;
+ Q_EMIT hasInitialShareFetchCompletedChanged();
+ }
+
+ qCInfo(lcSharing) << "Fetched" << shares.count() << "shares";
+
+ for (const auto &share : shares) {
+ if (share.isNull() ||
+ share->account().isNull() ||
+ share->getUidOwner() != share->account()->davUser()) {
+
+ continue;
+ }
+
+ slotAddShare(share);
+ }
+
+ handlePlaceholderLinkShare();
+}
+
+void ShareModel::slotAddShare(const SharePtr &share)
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ const auto shareId = share->getId();
+ QModelIndex shareModelIndex;
+
+ if (_shareIdIndexHash.contains(shareId)) {
+ const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId);
+ const auto shareIndex = sharePersistentModelIndex.row();
+
+ _shares.replace(shareIndex, share);
+
+ shareModelIndex = index(sharePersistentModelIndex.row());
+ Q_EMIT dataChanged(shareModelIndex, shareModelIndex);
+ } else {
+ const auto shareIndex = _shares.count();
+
+ beginInsertRows({}, _shares.count(), _shares.count());
+ _shares.append(share);
+ endInsertRows();
+
+ slotAddSharee(share->getShareWith());
+
+ shareModelIndex = index(shareIndex);
+ }
+
+ const QPersistentModelIndex sharePersistentIndex(shareModelIndex);
+ _shareIdIndexHash.insert(shareId, sharePersistentIndex);
+
+ connect(share.data(), &Share::serverError, this, &ShareModel::slotServerError);
+ connect(share.data(), &Share::passwordSetError, this, [this, shareId](const int code, const QString &message) {
+ _shareIdRecentlySetPasswords.remove(shareId);
+ slotSharePasswordSet(shareId);
+ Q_EMIT passwordSetError(shareId, code, message);
+ });
+
+ // Passing shareId by reference here will cause crashing, so we pass by value
+ connect(share.data(), &Share::shareDeleted, this, [this, shareId]{ slotRemoveShareWithId(shareId); });
+ connect(share.data(), &Share::permissionsSet, this, [this, shareId]{ slotSharePermissionsSet(shareId); });
+ connect(share.data(), &Share::passwordSet, this, [this, shareId]{ slotSharePasswordSet(shareId); });
+
+ if (const auto linkShare = share.objectCast<LinkShare>()) {
+ connect(linkShare.data(), &LinkShare::noteSet, this, [this, shareId]{ slotShareNoteSet(shareId); });
+ connect(linkShare.data(), &LinkShare::nameSet, this, [this, shareId]{ slotShareNameSet(shareId); });
+ connect(linkShare.data(), &LinkShare::labelSet, this, [this, shareId]{ slotShareLabelSet(shareId); });
+ connect(linkShare.data(), &LinkShare::expireDateSet, this, [this, shareId]{ slotShareExpireDateSet(shareId); });
+ } else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
+ connect(userGroupShare.data(), &UserGroupShare::noteSet, this, [this, shareId]{ slotShareNoteSet(shareId); });
+ connect(userGroupShare.data(), &UserGroupShare::expireDateSet, this, [this, shareId]{ slotShareExpireDateSet(shareId); });
+ }
+
+ if (_manager) {
+ connect(_manager.data(), &ShareManager::serverError, this, &ShareModel::slotServerError);
+ }
+
+ handlePlaceholderLinkShare();
+ Q_EMIT sharesChanged();
+}
+
+void ShareModel::slotRemoveShareWithId(const QString &shareId)
+{
+ if (_shares.empty() || shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) {
+ return;
+ }
+
+ _shareIdRecentlySetPasswords.remove(shareId);
+ const auto shareIndex = _shareIdIndexHash.take(shareId);
+
+ if (!checkIndex(shareIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid)) {
+ qCWarning(lcShareModel) << "Won't remove share with id:" << shareId
+ << ", invalid share index: " << shareIndex;
+ return;
+ }
+
+ const auto share = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ const auto sharee = share->getShareWith();
+ slotRemoveSharee(sharee);
+
+ beginRemoveRows({}, shareIndex.row(), shareIndex.row());
+ _shares.removeAt(shareIndex.row());
+ endRemoveRows();
+
+ handlePlaceholderLinkShare();
+
+ Q_EMIT sharesChanged();
+}
+
+void ShareModel::slotServerError(const int code, const QString &message)
+{
+ qCWarning(lcShareModel) << "Error from server" << code << message;
+ Q_EMIT serverError(code, message);
+}
+
+void ShareModel::slotAddSharee(const ShareePtr &sharee)
+{
+ if(!sharee) {
+ return;
+ }
+
+ _sharees.append(sharee);
+ Q_EMIT shareesChanged();
+}
+
+void ShareModel::slotRemoveSharee(const ShareePtr &sharee)
+{
+ _sharees.removeAll(sharee);
+ Q_EMIT shareesChanged();
+}
+
+QString ShareModel::displayStringForShare(const SharePtr &share) const
+{
+ if (const auto linkShare = share.objectCast<LinkShare>()) {
+ const auto displayString = tr("Share link");
+
+ if (!linkShare->getLabel().isEmpty()) {
+ return QStringLiteral("%1 (%2)").arg(displayString, linkShare->getLabel());
+ }
+
+ return displayString;
+ } else if (share->getShareType() == Share::TypePlaceholderLink) {
+ return tr("Link share");
+ } else if (share->getShareWith()) {
+ return share->getShareWith()->format();
+ }
+
+ qCWarning(lcShareModel) << "Unable to provide good display string for share";
+ return QStringLiteral("Share");
+}
+
+QString ShareModel::iconUrlForShare(const SharePtr &share) const
+{
+ const auto iconsPath = QStringLiteral("image://svgimage-custom-color/");
+
+ switch(share->getShareType()) {
+ case Share::TypePlaceholderLink:
+ case Share::TypeLink:
+ return QString(iconsPath + QStringLiteral("public.svg"));
+ case Share::TypeEmail:
+ return QString(iconsPath + QStringLiteral("email.svg"));
+ case Share::TypeRoom:
+ return QString(iconsPath + QStringLiteral("wizard-talk.svg"));
+ case Share::TypeUser:
+ return QString(iconsPath + QStringLiteral("user.svg"));
+ case Share::TypeGroup:
+ return QString(iconsPath + QStringLiteral("wizard-groupware.svg"));
+ default:
+ return {};
+ }
+}
+
+QString ShareModel::avatarUrlForShare(const SharePtr &share) const
+{
+ if (share->getShareWith() && share->getShareWith()->type() == Sharee::User && _accountState && _accountState->account()) {
+ const auto provider = QStringLiteral("image://tray-image-provider/");
+ const auto userId = share->getShareWith()->shareWith();
+ const auto avatarUrl = Utility::concatUrlPath(_accountState->account()->url(),
+ QString("remote.php/dav/avatars/%1/%2.png").arg(userId, QString::number(64))).toString();
+ return QString(provider + avatarUrl);
+ }
+
+ return {};
+}
+
+long long ShareModel::enforcedMaxExpireDateForShare(const SharePtr &share) const
+{
+ if (!_accountState || !_accountState->account() || !_accountState->account()->capabilities().isValid()) {
+ return {};
+ }
+
+ auto expireDays = -1;
+
+ // Both public links and emails count as "public" shares
+ if ((share->getShareType() == Share::TypeLink || share->getShareType() == Share::TypeEmail)
+ && _accountState->account()->capabilities().sharePublicLinkEnforceExpireDate()) {
+ expireDays = _accountState->account()->capabilities().sharePublicLinkExpireDateDays();
+
+ } else if (share->getShareType() == Share::TypeRemote && _accountState->account()->capabilities().shareRemoteEnforceExpireDate()) {
+ expireDays = _accountState->account()->capabilities().shareRemoteExpireDateDays();
+
+ } else if ((share->getShareType() == Share::TypeUser ||
+ share->getShareType() == Share::TypeGroup ||
+ share->getShareType() == Share::TypeCircle ||
+ share->getShareType() == Share::TypeRoom) &&
+ _accountState->account()->capabilities().shareInternalEnforceExpireDate()) {
+ expireDays = _accountState->account()->capabilities().shareInternalExpireDateDays();
+
+ } else {
+ return {};
+ }
+
+ const auto expireDateTime = QDate::currentDate().addDays(expireDays).startOfDay(QTimeZone::utc());
+ return expireDateTime.toMSecsSinceEpoch();
+}
+
+bool ShareModel::expireDateEnforcedForShare(const SharePtr &share) const
+{
+ if(!_accountState || !_accountState->account() || !_accountState->account()->capabilities().isValid()) {
+ return false;
+ }
+
+ // Both public links and emails count as "public" shares
+ if (share->getShareType() == Share::TypeLink ||
+ share->getShareType() == Share::TypeEmail) {
+ return _accountState->account()->capabilities().sharePublicLinkEnforceExpireDate();
+
+ } else if (share->getShareType() == Share::TypeRemote) {
+ return _accountState->account()->capabilities().shareRemoteEnforceExpireDate();
+
+ } else if (share->getShareType() == Share::TypeUser ||
+ share->getShareType() == Share::TypeGroup ||
+ share->getShareType() == Share::TypeCircle ||
+ share->getShareType() == Share::TypeRoom) {
+ return _accountState->account()->capabilities().shareInternalEnforceExpireDate();
+
+ }
+
+ return false;
+}
+
+// ----------------- Shares modified signal handling slots ----------------- //
+
+void ShareModel::slotSharePermissionsSet(const QString &shareId)
+{
+ if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) {
+ return;
+ }
+
+ const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId);
+ const auto shareModelIndex = index(sharePersistentModelIndex.row());
+ Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { EditingAllowedRole });
+}
+
+void ShareModel::slotSharePasswordSet(const QString &shareId)
+{
+ if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) {
+ return;
+ }
+
+ const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId);
+ const auto shareModelIndex = index(sharePersistentModelIndex.row());
+ Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { PasswordProtectEnabledRole, PasswordRole });
+}
+
+void ShareModel::slotShareNoteSet(const QString &shareId)
+{
+ if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) {
+ return;
+ }
+
+ const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId);
+ const auto shareModelIndex = index(sharePersistentModelIndex.row());
+ Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { NoteEnabledRole, NoteRole });
+}
+
+void ShareModel::slotShareNameSet(const QString &shareId)
+{
+ if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) {
+ return;
+ }
+
+ const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId);
+ const auto shareModelIndex = index(sharePersistentModelIndex.row());
+ Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { LinkShareNameRole });
+}
+
+void ShareModel::slotShareLabelSet(const QString &shareId)
+{
+ if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) {
+ return;
+ }
+
+ const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId);
+ const auto shareModelIndex = index(sharePersistentModelIndex.row());
+ Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { Qt::DisplayRole, LinkShareLabelRole });
+}
+
+void ShareModel::slotShareExpireDateSet(const QString &shareId)
+{
+ if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) {
+ return;
+ }
+
+ const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId);
+ const auto shareModelIndex = index(sharePersistentModelIndex.row());
+ Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { ExpireDateEnabledRole, ExpireDateRole });
+}
+
+// ----------------------- Shares modification slots ----------------------- //
+
+void ShareModel::toggleShareAllowEditing(const SharePtr &share, const bool enable) const
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ auto permissions = share->getPermissions();
+ enable ? permissions |= SharePermissionUpdate : permissions &= ~SharePermissionUpdate;
+
+ share->setPermissions(permissions);
+}
+
+void ShareModel::toggleShareAllowEditingFromQml(const QVariant &share, const bool enable) const
+{
+ const auto ptr = share.value<SharePtr>();
+ toggleShareAllowEditing(ptr, enable);
+}
+
+void ShareModel::toggleShareAllowResharing(const SharePtr &share, const bool enable) const
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ auto permissions = share->getPermissions();
+ enable ? permissions |= SharePermissionShare : permissions &= ~SharePermissionShare;
+
+ share->setPermissions(permissions);
+}
+
+void ShareModel::toggleShareAllowResharingFromQml(const QVariant &share, const bool enable) const
+{
+ const auto ptr = share.value<SharePtr>();
+ toggleShareAllowResharing(ptr, enable);
+}
+
+void ShareModel::toggleSharePasswordProtect(const SharePtr &share, const bool enable)
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ if(!enable) {
+ share->setPassword({});
+ return;
+ }
+
+ const auto randomPassword = createRandomPassword();
+ _shareIdRecentlySetPasswords.insert(share->getId(), randomPassword);
+ share->setPassword(randomPassword);
+}
+
+void ShareModel::toggleSharePasswordProtectFromQml(const QVariant &share, const bool enable)
+{
+ const auto ptr = share.value<SharePtr>();
+ toggleSharePasswordProtect(ptr, enable);
+}
+
+void ShareModel::toggleShareExpirationDate(const SharePtr &share, const bool enable) const
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ const auto expireDate = enable ? QDate::currentDate().addDays(1) : QDate();
+
+ if (const auto linkShare = share.objectCast<LinkShare>()) {
+ linkShare->setExpireDate(expireDate);
+ } else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
+ userGroupShare->setExpireDate(expireDate);
+ }
+}
+
+void ShareModel::toggleShareExpirationDateFromQml(const QVariant &share, const bool enable) const
+{
+ const auto ptr = share.value<SharePtr>();
+ toggleShareExpirationDate(ptr, enable);
+}
+
+void ShareModel::toggleShareNoteToRecipient(const SharePtr &share, const bool enable) const
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ const QString note = enable ? tr("Enter a note for the recipient") : QString();
+ if (const auto linkShare = share.objectCast<LinkShare>()) {
+ linkShare->setNote(note);
+ } else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
+ userGroupShare->setNote(note);
+ }
+}
+
+void ShareModel::toggleShareNoteToRecipientFromQml(const QVariant &share, const bool enable) const
+{
+ const auto ptr = share.value<SharePtr>();
+ toggleShareNoteToRecipient(ptr, enable);
+}
+
+void ShareModel::setLinkShareLabel(const QSharedPointer<LinkShare> &linkShare, const QString &label) const
+{
+ if (linkShare.isNull()) {
+ return;
+ }
+
+ linkShare->setLabel(label);
+}
+
+void ShareModel::setLinkShareLabelFromQml(const QVariant &linkShare, const QString &label) const
+{
+ // All of our internal share pointers are SharePtr, so cast to LinkShare for this method
+ const auto ptr = linkShare.value<SharePtr>().objectCast<LinkShare>();
+ setLinkShareLabel(ptr, label);
+}
+
+void ShareModel::setShareExpireDate(const SharePtr &share, const qint64 milliseconds) const
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ const auto date = QDateTime::fromMSecsSinceEpoch(milliseconds, QTimeZone::utc()).date();
+
+ if (const auto linkShare = share.objectCast<LinkShare>()) {
+ linkShare->setExpireDate(date);
+ } else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
+ userGroupShare->setExpireDate(date);
+ }
+}
+
+void ShareModel::setShareExpireDateFromQml(const QVariant &share, const QVariant milliseconds) const
+{
+ const auto ptr = share.value<SharePtr>();
+ const auto millisecondsLL = milliseconds.toLongLong();
+ setShareExpireDate(ptr, millisecondsLL);
+}
+
+void ShareModel::setSharePassword(const SharePtr &share, const QString &password)
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ _shareIdRecentlySetPasswords.insert(share->getId(), password);
+ share->setPassword(password);
+}
+
+void ShareModel::setSharePasswordFromQml(const QVariant &share, const QString &password)
+{
+ const auto ptr = share.value<SharePtr>();
+ setSharePassword(ptr, password);
+}
+
+void ShareModel::setShareNote(const SharePtr &share, const QString &note) const
+{
+ if (share.isNull()) {
+ return;
+ }
+
+ if (const auto linkShare = share.objectCast<LinkShare>()) {
+ linkShare->setNote(note);
+ } else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
+ userGroupShare->setNote(note);
+ }
+}
+
+void ShareModel::setShareNoteFromQml(const QVariant &share, const QString &note) const
+{
+ const auto ptr = share.value<SharePtr>();
+ setShareNote(ptr, note);
+}
+
+// ------------------- Share creation and deletion slots ------------------- //
+
+void ShareModel::createNewLinkShare() const
+{
+ if (_manager) {
+ const auto askOptionalPassword = _accountState->account()->capabilities().sharePublicLinkAskOptionalPassword();
+ const auto password = askOptionalPassword ? createRandomPassword() : QString();
+ _manager->createLinkShare(_sharePath, QString(), password);
+ }
+}
+
+void ShareModel::createNewLinkShareWithPassword(const QString &password) const
+{
+ if (_manager) {
+ _manager->createLinkShare(_sharePath, QString(), password);
+ }
+}
+
+void ShareModel::createNewUserGroupShare(const ShareePtr &sharee)
+{
+ if (sharee.isNull()) {
+ return;
+ }
+
+ qCInfo(lcShareModel) << "Creating new user/group share for sharee: " << sharee->format();
+
+ if (sharee->type() == Sharee::Email &&
+ _accountState &&
+ !_accountState->account().isNull() &&
+ _accountState->account()->capabilities().isValid() &&
+ _accountState->account()->capabilities().shareEmailPasswordEnforced()) {
+
+ Q_EMIT requestPasswordForEmailSharee(sharee);
+ return;
+ }
+
+ _manager->createShare(_sharePath,
+ Share::ShareType(sharee->type()),
+ sharee->shareWith(),
+ _maxSharingPermissions,
+ {});
+}
+
+void ShareModel::createNewUserGroupShareWithPassword(const ShareePtr &sharee, const QString &password) const
+{
+ if (sharee.isNull()) {
+ return;
+ }
+
+ _manager->createShare(_sharePath,
+ Share::ShareType(sharee->type()),
+ sharee->shareWith(),
+ _maxSharingPermissions,
+ password);
+}
+
+void ShareModel::createNewUserGroupShareFromQml(const QVariant &sharee)
+{
+ const auto ptr = sharee.value<ShareePtr>();
+ createNewUserGroupShare(ptr);
+}
+
+void ShareModel::createNewUserGroupShareWithPasswordFromQml(const QVariant &sharee, const QString &password) const
+{
+ const auto ptr = sharee.value<ShareePtr>();
+ createNewUserGroupShareWithPassword(ptr, password);
+}
+
+void ShareModel::deleteShare(const SharePtr &share) const
+{
+ if(share.isNull()) {
+ return;
+ }
+
+ share->deleteShare();
+}
+
+void ShareModel::deleteShareFromQml(const QVariant &share) const
+{
+ const auto ptr = share.value<SharePtr>();
+ deleteShare(ptr);
+}
+
+// --------------------------- QPROPERTY methods --------------------------- //
+
+QString ShareModel::localPath() const
+{
+ return _localPath;
+}
+
+void ShareModel::setLocalPath(const QString &localPath)
+{
+ if (localPath == _localPath) {
+ return;
+ }
+
+ _localPath = localPath;
+ Q_EMIT localPathChanged();
+ updateData();
+}
+
+AccountState *ShareModel::accountState() const
+{
+ return _accountState;
+}
+
+void ShareModel::setAccountState(AccountState *accountState)
+{
+ if (accountState == _accountState) {
+ return;
+ }
+
+ _accountState = accountState;
+
+ // Change the server and account-related properties
+ connect(_accountState, &AccountState::stateChanged, this, &ShareModel::accountConnectedChanged);
+ connect(_accountState, &AccountState::stateChanged, this, &ShareModel::sharingEnabledChanged);
+ connect(_accountState, &AccountState::stateChanged, this, &ShareModel::publicLinkSharesEnabledChanged);
+ connect(_accountState, &AccountState::stateChanged, this, &ShareModel::userGroupSharingEnabledChanged);
+
+ Q_EMIT accountStateChanged();
+ Q_EMIT accountConnectedChanged();
+ Q_EMIT sharingEnabledChanged();
+ Q_EMIT publicLinkSharesEnabledChanged();
+ Q_EMIT userGroupSharingEnabledChanged();
+ updateData();
+}
+
+bool ShareModel::accountConnected() const
+{
+ return _accountState && _accountState->isConnected();
+}
+
+bool ShareModel::sharingEnabled() const
+{
+ return _accountState &&
+ _accountState->account() &&
+ _accountState->account()->capabilities().isValid() &&
+ _accountState->account()->capabilities().shareAPI();
+}
+
+bool ShareModel::publicLinkSharesEnabled() const
+{
+ return Theme::instance()->linkSharing() &&
+ _accountState &&
+ _accountState->account() &&
+ _accountState->account()->capabilities().isValid() &&
+ _accountState->account()->capabilities().sharePublicLink();
+}
+
+bool ShareModel::userGroupSharingEnabled() const
+{
+ return Theme::instance()->userGroupSharing();
+}
+
+bool ShareModel::fetchOngoing() const
+{
+ return _fetchOngoing;
+}
+
+bool ShareModel::hasInitialShareFetchCompleted() const
+{
+ return _hasInitialShareFetchCompleted;
+}
+
+bool ShareModel::canShare() const
+{
+ return _maxSharingPermissions & SharePermissionShare;
+}
+
+QVariantList ShareModel::sharees() const
+{
+ QVariantList returnSharees;
+ for (const auto &sharee : _sharees) {
+ returnSharees.append(QVariant::fromValue(sharee));
+ }
+ return returnSharees;
+}
+
+} // namespace OCC
diff --git a/src/gui/filedetails/sharemodel.h b/src/gui/filedetails/sharemodel.h
new file mode 100644
index 000000000..abb94978b
--- /dev/null
+++ b/src/gui/filedetails/sharemodel.h
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "accountstate.h"
+#include "folder.h"
+#include "sharemanager.h"
+#include "sharepermissions.h"
+
+namespace OCC {
+
+class ShareModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged)
+ Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged)
+ Q_PROPERTY(bool accountConnected READ accountConnected NOTIFY accountConnectedChanged)
+ Q_PROPERTY(bool sharingEnabled READ sharingEnabled NOTIFY sharingEnabledChanged)
+ Q_PROPERTY(bool publicLinkSharesEnabled READ publicLinkSharesEnabled NOTIFY publicLinkSharesEnabledChanged)
+ Q_PROPERTY(bool userGroupSharingEnabled READ userGroupSharingEnabled NOTIFY userGroupSharingEnabledChanged)
+ Q_PROPERTY(bool canShare READ canShare NOTIFY sharePermissionsChanged)
+ Q_PROPERTY(bool fetchOngoing READ fetchOngoing NOTIFY fetchOngoingChanged)
+ Q_PROPERTY(bool hasInitialShareFetchCompleted READ hasInitialShareFetchCompleted NOTIFY hasInitialShareFetchCompletedChanged)
+ Q_PROPERTY(QVariantList sharees READ sharees NOTIFY shareesChanged)
+
+public:
+ enum Roles {
+ ShareRole = Qt::UserRole + 1,
+ ShareTypeRole,
+ ShareIdRole,
+ IconUrlRole,
+ AvatarUrlRole,
+ LinkRole,
+ LinkShareNameRole,
+ LinkShareLabelRole,
+ NoteEnabledRole,
+ NoteRole,
+ ExpireDateEnabledRole,
+ ExpireDateEnforcedRole,
+ ExpireDateRole,
+ EnforcedMaximumExpireDateRole,
+ PasswordProtectEnabledRole,
+ PasswordRole,
+ PasswordEnforcedRole,
+ EditingAllowedRole,
+ };
+ Q_ENUM(Roles)
+
+ /**
+ * Possible share types
+ * Need to be in sync with Share::ShareType.
+ * We use this in QML.
+ */
+ enum ShareType {
+ ShareTypeUser = Share::TypeUser,
+ ShareTypeGroup = Share::TypeGroup,
+ ShareTypeLink = Share::TypeLink,
+ ShareTypeEmail = Share::TypeEmail,
+ ShareTypeRemote = Share::TypeRemote,
+ ShareTypeCircle = Share::TypeCircle,
+ ShareTypeRoom = Share::TypeRoom,
+ ShareTypePlaceholderLink = Share::TypePlaceholderLink,
+ };
+ Q_ENUM(ShareType);
+
+ explicit ShareModel(QObject *parent = nullptr);
+
+ [[nodiscard]] QVariant data(const QModelIndex &index, const int role) const override;
+ [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+
+ [[nodiscard]] AccountState *accountState() const;
+ [[nodiscard]] QString localPath() const;
+
+ [[nodiscard]] bool accountConnected() const;
+ [[nodiscard]] bool sharingEnabled() const;
+ [[nodiscard]] bool publicLinkSharesEnabled() const;
+ [[nodiscard]] bool userGroupSharingEnabled() const;
+ [[nodiscard]] bool canShare() const;
+
+ [[nodiscard]] bool fetchOngoing() const;
+ [[nodiscard]] bool hasInitialShareFetchCompleted() const;
+
+ [[nodiscard]] QVariantList sharees() const;
+
+signals:
+ void localPathChanged();
+ void accountStateChanged();
+ void accountConnectedChanged();
+ void sharingEnabledChanged();
+ void publicLinkSharesEnabledChanged();
+ void userGroupSharingEnabledChanged();
+ void sharePermissionsChanged();
+ void lockExpireStringChanged();
+ void fetchOngoingChanged();
+ void hasInitialShareFetchCompletedChanged();
+ void shareesChanged();
+
+ void serverError(const int code, const QString &message);
+ void passwordSetError(const QString &shareId, const int code, const QString &message);
+ void requestPasswordForLinkShare();
+ void requestPasswordForEmailSharee(const ShareePtr &sharee);
+
+ void sharesChanged();
+
+public slots:
+ void setAccountState(AccountState *accountState);
+ void setLocalPath(const QString &localPath);
+
+ void createNewLinkShare() const;
+ void createNewLinkShareWithPassword(const QString &password) const;
+ void createNewUserGroupShare(const ShareePtr &sharee);
+ void createNewUserGroupShareFromQml(const QVariant &sharee);
+ void createNewUserGroupShareWithPassword(const ShareePtr &sharee, const QString &password) const;
+ void createNewUserGroupShareWithPasswordFromQml(const QVariant &sharee, const QString &password) const;
+
+ void deleteShare(const SharePtr &share) const;
+ void deleteShareFromQml(const QVariant &share) const;
+
+ void toggleShareAllowEditing(const SharePtr &share, const bool enable) const;
+ void toggleShareAllowEditingFromQml(const QVariant &share, const bool enable) const;
+ void toggleShareAllowResharing(const SharePtr &share, const bool enable) const;
+ void toggleShareAllowResharingFromQml(const QVariant &share, const bool enable) const;
+ void toggleSharePasswordProtect(const SharePtr &share, const bool enable);
+ void toggleSharePasswordProtectFromQml(const QVariant &share, const bool enable);
+ void toggleShareExpirationDate(const SharePtr &share, const bool enable) const;
+ void toggleShareExpirationDateFromQml(const QVariant &share, const bool enable) const;
+ void toggleShareNoteToRecipient(const SharePtr &share, const bool enable) const;
+ void toggleShareNoteToRecipientFromQml(const QVariant &share, const bool enable) const;
+
+ void setLinkShareLabel(const QSharedPointer<LinkShare> &linkShare, const QString &label) const;
+ void setLinkShareLabelFromQml(const QVariant &linkShare, const QString &label) const;
+ void setShareExpireDate(const SharePtr &share, const qint64 milliseconds) const;
+ // Needed as ints in QML are 32 bits so we need to use a QVariant
+ void setShareExpireDateFromQml(const QVariant &share, const QVariant milliseconds) const;
+ void setSharePassword(const SharePtr &share, const QString &password);
+ void setSharePasswordFromQml(const QVariant &share, const QString &password);
+ void setShareNote(const SharePtr &share, const QString &note) const;
+ void setShareNoteFromQml(const QVariant &share, const QString &note) const;
+
+private slots:
+ void resetData();
+ void updateData();
+ void initShareManager();
+ void handlePlaceholderLinkShare();
+
+ void slotPropfindReceived(const QVariantMap &result);
+ void slotServerError(const int code, const QString &message);
+ void slotAddShare(const SharePtr &share);
+ void slotRemoveShareWithId(const QString &shareId);
+ void slotSharesFetched(const QList<SharePtr> &shares);
+ void slotAddSharee(const ShareePtr &sharee);
+ void slotRemoveSharee(const ShareePtr &sharee);
+
+ void slotSharePermissionsSet(const QString &shareId);
+ void slotSharePasswordSet(const QString &shareId);
+ void slotShareNoteSet(const QString &shareId);
+ void slotShareNameSet(const QString &shareId);
+ void slotShareLabelSet(const QString &shareId);
+ void slotShareExpireDateSet(const QString &shareId);
+
+private:
+ [[nodiscard]] QString displayStringForShare(const SharePtr &share) const;
+ [[nodiscard]] QString iconUrlForShare(const SharePtr &share) const;
+ [[nodiscard]] QString avatarUrlForShare(const SharePtr &share) const;
+ [[nodiscard]] long long enforcedMaxExpireDateForShare(const SharePtr &share) const;
+ [[nodiscard]] bool expireDateEnforcedForShare(const SharePtr &share) const;
+
+ bool _fetchOngoing = false;
+ bool _hasInitialShareFetchCompleted = false;
+ SharePtr _placeholderLinkShare;
+
+ // DO NOT USE QSHAREDPOINTERS HERE.
+ // QSharedPointers MUST NOT be used with pointers already assigned to other shared pointers.
+ // This is because they do not share reference counters, and as such are not aware of another
+ // smart pointer's use of the same object.
+ //
+ // We cannot pass objects instantiated in QML using smart pointers through the property interface
+ // so we have to pass the pointer here. If we kill the dialog using a smart pointer then
+ // these objects will be deallocated for the entire application. We do not want that!!
+ AccountState *_accountState;
+ Folder *_folder;
+
+ QString _localPath;
+ QString _sharePath;
+ SharePermissions _maxSharingPermissions;
+ QByteArray _numericFileId;
+ SyncJournalFileLockInfo _filelockState;
+ QString _privateLinkUrl;
+
+ QSharedPointer<ShareManager> _manager;
+
+ QVector<SharePtr> _shares;
+ QHash<QString, QPersistentModelIndex> _shareIdIndexHash;
+ QHash<QString, QString> _shareIdRecentlySetPasswords;
+ QVector<ShareePtr> _sharees;
+};
+
+} // namespace OCC
diff --git a/src/gui/filedetails/sortedsharemodel.cpp b/src/gui/filedetails/sortedsharemodel.cpp
new file mode 100644
index 000000000..9906cfc57
--- /dev/null
+++ b/src/gui/filedetails/sortedsharemodel.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 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 "sortedsharemodel.h"
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcSortedShareModel, "com.nextcloud.sortedsharemodel")
+
+SortedShareModel::SortedShareModel(QObject *parent)
+ : QSortFilterProxyModel(parent)
+{
+}
+
+void SortedShareModel::sortModel()
+{
+ sort(0);
+}
+
+ShareModel *SortedShareModel::shareModel() const
+{
+ return qobject_cast<ShareModel*>(sourceModel());
+}
+
+void SortedShareModel::setShareModel(ShareModel *shareModel)
+{
+ const auto currentSetModel = sourceModel();
+
+ if(currentSetModel) {
+ disconnect(currentSetModel, &ShareModel::rowsInserted, this, &SortedShareModel::sortModel);
+ disconnect(currentSetModel, &ShareModel::rowsMoved, this, &SortedShareModel::sortModel);
+ disconnect(currentSetModel, &ShareModel::rowsRemoved, this, &SortedShareModel::sortModel);
+ disconnect(currentSetModel, &ShareModel::dataChanged, this, &SortedShareModel::sortModel);
+ disconnect(currentSetModel, &ShareModel::modelReset, this, &SortedShareModel::sortModel);
+ }
+
+ // Re-sort model when any changes take place
+ connect(shareModel, &ShareModel::rowsInserted, this, &SortedShareModel::sortModel);
+ connect(shareModel, &ShareModel::rowsMoved, this, &SortedShareModel::sortModel);
+ connect(shareModel, &ShareModel::rowsRemoved, this, &SortedShareModel::sortModel);
+ connect(shareModel, &ShareModel::dataChanged, this, &SortedShareModel::sortModel);
+ connect(shareModel, &ShareModel::modelReset, this, &SortedShareModel::sortModel);
+
+ setSourceModel(shareModel);
+ sortModel();
+ Q_EMIT shareModelChanged();
+}
+
+bool SortedShareModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
+{
+ if (!sourceLeft.isValid() || !sourceRight.isValid()) {
+ return false;
+ }
+
+ const auto leftShare = sourceLeft.data(ShareModel::ShareRole).value<SharePtr>();
+ const auto rightShare = sourceRight.data(ShareModel::ShareRole).value<SharePtr>();
+
+ if (leftShare.isNull() || rightShare.isNull()) {
+ return false;
+ }
+
+ const auto leftShareType = leftShare->getShareType();
+
+ // Placeholder link shares always go at top
+ if(leftShareType == Share::TypePlaceholderLink) {
+ return true;
+ }
+
+ const auto rightShareType = rightShare->getShareType();
+
+ // We want to place link shares at the top
+ if (leftShareType == Share::TypeLink && rightShareType != Share::TypeLink) {
+ return true;
+ } else if (rightShareType == Share::TypeLink && leftShareType != Share::TypeLink) {
+ return false;
+ } else if (leftShareType != rightShareType) {
+ return leftShareType < rightShareType;
+ }
+
+ if (leftShareType == Share::TypeLink) {
+ const auto leftLinkShare = leftShare.objectCast<LinkShare>();
+ const auto rightLinkShare = rightShare.objectCast<LinkShare>();
+
+ if(leftLinkShare.isNull() || rightLinkShare.isNull()) {
+ qCWarning(lcSortedShareModel) << "One of compared shares is a null pointer after conversion despite having same share type. Left link share is null:" << leftLinkShare.isNull()
+ << "Right link share is null: " << rightLinkShare.isNull();
+ return false;
+ }
+
+ return leftLinkShare->getLabel() < rightLinkShare->getLabel();
+
+ } else if (leftShare->getShareWith()) {
+ if(rightShare->getShareWith().isNull()) {
+ return true;
+ }
+
+ return leftShare->getShareWith()->format() < rightShare->getShareWith()->format();
+ }
+
+ return false;
+}
+
+} // namespace OCC
diff --git a/src/gui/filedetails/sortedsharemodel.h b/src/gui/filedetails/sortedsharemodel.h
new file mode 100644
index 000000000..a9acc1d58
--- /dev/null
+++ b/src/gui/filedetails/sortedsharemodel.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <QSortFilterProxyModel>
+#include "sharemodel.h"
+
+namespace OCC {
+
+class SortedShareModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+ Q_PROPERTY(ShareModel* shareModel READ shareModel WRITE setShareModel NOTIFY shareModelChanged)
+
+public:
+ explicit SortedShareModel(QObject *parent = nullptr);
+
+ [[nodiscard]] ShareModel *shareModel() const;
+
+signals:
+ void shareModelChanged();
+
+public slots:
+ void setShareModel(ShareModel *shareModel);
+
+protected:
+ [[nodiscard]] bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
+
+private slots:
+ void sortModel();
+};
+
+} // namespace OCC
diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp
index 8d57a72b8..b71a9593d 100644
--- a/src/gui/folderman.cpp
+++ b/src/gui/folderman.cpp
@@ -24,6 +24,7 @@
#include "filesystem.h"
#include "lockwatcher.h"
#include "common/asserts.h"
+#include "gui/systray.h"
#include <pushnotifications.h>
#include <syncengine.h>
diff --git a/src/gui/folderman.h b/src/gui/folderman.h
index bf7a8e8c8..6409cc8b4 100644
--- a/src/gui/folderman.h
+++ b/src/gui/folderman.h
@@ -27,6 +27,8 @@
class TestFolderMan;
class TestCfApiShellExtensionsIPC;
+class TestShareModel;
+class ShareTestHelper;
namespace OCC {
@@ -379,6 +381,7 @@ private:
friend class OCC::Application;
friend class ::TestFolderMan;
friend class ::TestCfApiShellExtensionsIPC;
+ friend class ::ShareTestHelper;
};
} // namespace OCC
diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp
index ab96fe831..5ef4b7e4c 100644
--- a/src/gui/owncloudgui.cpp
+++ b/src/gui/owncloudgui.cpp
@@ -29,11 +29,12 @@
#include "owncloudsetupwizard.h"
#include "progressdispatcher.h"
#include "settingsdialog.h"
-#include "sharedialog.h"
#include "theme.h"
#include "wheelhandler.h"
-#include "common/syncjournalfilerecord.h"
-#include "creds/abstractcredentials.h"
+#include "filedetails/filedetails.h"
+#include "filedetails/shareemodel.h"
+#include "filedetails/sharemodel.h"
+#include "filedetails/sortedsharemodel.h"
#include "tray/sortedactivitylistmodel.h"
#include "tray/syncstatussummary.h"
#include "tray/unifiedsearchresultslistmodel.h"
@@ -97,11 +98,6 @@ ownCloudGui::ownCloudGui(Application *parent)
connect(_tray.data(), &Systray::shutdown,
this, &ownCloudGui::slotShutdown);
- connect(_tray.data(), &Systray::openShareDialog,
- this, [=](const QString &sharePath, const QString &localPath) {
- slotShowShareDialog(sharePath, localPath, ShareDialogStartPage::UsersAndGroups);
- });
-
ProgressDispatcher *pd = ProgressDispatcher::instance();
connect(pd, &ProgressDispatcher::progressInfo, this,
&ownCloudGui::slotUpdateProgress);
@@ -125,6 +121,10 @@ ownCloudGui::ownCloudGui(Application *parent)
qmlRegisterType<SortedActivityListModel>("com.nextcloud.desktopclient", 1, 0, "SortedActivityListModel");
qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
qmlRegisterType<CallStateChecker>("com.nextcloud.desktopclient", 1, 0, "CallStateChecker");
+ qmlRegisterType<FileDetails>("com.nextcloud.desktopclient", 1, 0, "FileDetails");
+ qmlRegisterType<ShareModel>("com.nextcloud.desktopclient", 1, 0, "ShareModel");
+ qmlRegisterType<ShareeModel>("com.nextcloud.desktopclient", 1, 0, "ShareeModel");
+ qmlRegisterType<SortedShareModel>("com.nextcloud.desktopclient", 1, 0, "SortedShareModel");
qmlRegisterUncreatableType<UnifiedSearchResultsListModel>("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
@@ -134,6 +134,8 @@ ownCloudGui::ownCloudGui(Application *parent)
qRegisterMetaType<ActivityListModel *>("ActivityListModel*");
qRegisterMetaType<UnifiedSearchResultsListModel *>("UnifiedSearchResultsListModel*");
qRegisterMetaType<UserStatus>("UserStatus");
+ qRegisterMetaType<SharePtr>("SharePtr");
+ qRegisterMetaType<ShareePtr>("ShareePtr");
qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserModel", UserModel::instance());
qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserAppsModel", UserAppsModel::instance());
@@ -196,12 +198,8 @@ void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason)
} else if (reason == QSystemTrayIcon::Trigger) {
if (OwncloudSetupWizard::bringWizardToFrontIfVisible()) {
// brought wizard to front
- } else if (_shareDialogs.size() > 0) {
- // Share dialog(s) be hidden by other apps, bring them back
- Q_FOREACH (const QPointer<ShareDialog> &shareDialog, _shareDialogs) {
- Q_ASSERT(shareDialog.data());
- raiseDialog(shareDialog);
- }
+ } else if (_tray->raiseDialogs()) {
+ // Brings dialogs hidden by other apps to front, returns true if any raised
} else if (_tray->isOpen()) {
_tray->hideWindow();
} else {
@@ -652,54 +650,14 @@ void ownCloudGui::raiseDialog(QWidget *raiseWidget)
}
-void ownCloudGui::slotShowShareDialog(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage)
+void ownCloudGui::slotShowShareDialog(const QString &localPath) const
{
- const auto folder = FolderMan::instance()->folderForPath(localPath);
- if (!folder) {
- qCWarning(lcApplication) << "Could not open share dialog for" << localPath << "no responsible folder found";
- return;
- }
-
- const auto accountState = folder->accountState();
-
- const QString file = localPath.mid(folder->cleanPath().length() + 1);
- SyncJournalFileRecord fileRecord;
-
- bool resharingAllowed = true; // lets assume the good
- if (folder->journalDb()->getFileRecord(file, &fileRecord) && fileRecord.isValid()) {
- // check the permission: Is resharing allowed?
- if (!fileRecord._remotePerm.isNull() && !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
- resharingAllowed = false;
- }
- }
-
- auto maxSharingPermissions = resharingAllowed? SharePermissions(accountState->account()->capabilities().shareDefaultPermissions()) : SharePermissions({});
-
- ShareDialog *w = nullptr;
- if (_shareDialogs.contains(localPath) && _shareDialogs[localPath]) {
- qCInfo(lcApplication) << "Raising share dialog" << sharePath << localPath;
- w = _shareDialogs[localPath];
- } else {
- qCInfo(lcApplication) << "Opening share dialog" << sharePath << localPath << maxSharingPermissions;
- w = new ShareDialog(accountState, sharePath, localPath, maxSharingPermissions, fileRecord.numericFileId(), fileRecord._lockstate, startPage);
- w->setAttribute(Qt::WA_DeleteOnClose, true);
-
- _shareDialogs[localPath] = w;
- connect(w, &QObject::destroyed, this, &ownCloudGui::slotRemoveDestroyedShareDialogs);
- }
- raiseDialog(w);
+ _tray->createShareDialog(localPath);
}
-void ownCloudGui::slotRemoveDestroyedShareDialogs()
+void ownCloudGui::slotShowFileActivityDialog(const QString &localPath) const
{
- QMutableMapIterator<QString, QPointer<ShareDialog>> it(_shareDialogs);
- while (it.hasNext()) {
- it.next();
- if (!it.value() || it.value() == sender()) {
- it.remove();
- }
- }
+ _tray->createFileActivityDialog(localPath);
}
-
} // end namespace
diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h
index 3ffa57cd1..7b40d520d 100644
--- a/src/gui/owncloudgui.h
+++ b/src/gui/owncloudgui.h
@@ -100,14 +100,11 @@ public slots:
/**
* Open a share dialog for a file or folder.
*
- * sharePath is the full remote path to the item,
* localPath is the absolute local path to it (so not relative
* to the folder).
*/
- void slotShowShareDialog(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage);
-
- void slotRemoveDestroyedShareDialogs();
-
+ void slotShowShareDialog(const QString &localPath) const;
+ void slotShowFileActivityDialog(const QString &localPath) const;
void slotNewAccountWizard();
private slots:
@@ -123,8 +120,6 @@ private:
QDBusConnection _bus;
#endif
- QMap<QString, QPointer<ShareDialog>> _shareDialogs;
-
QAction *_actionNewAccountWizard;
QAction *_actionSettings;
QAction *_actionEstimate;
diff --git a/src/gui/sharedialog.cpp b/src/gui/sharedialog.cpp
deleted file mode 100644
index 99a0b8356..000000000
--- a/src/gui/sharedialog.cpp
+++ /dev/null
@@ -1,494 +0,0 @@
-/*
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- *
- * 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 "ui_sharedialog.h"
-#include "sharedialog.h"
-#include "sharee.h"
-#include "sharelinkwidget.h"
-#include "internallinkwidget.h"
-#include "shareusergroupwidget.h"
-#include "passwordinputdialog.h"
-
-#include "sharemanager.h"
-
-#include "account.h"
-#include "accountstate.h"
-#include "configfile.h"
-#include "theme.h"
-#include "thumbnailjob.h"
-#include "wordlist.h"
-
-#include <QFileInfo>
-#include <QFileIconProvider>
-#include <QInputDialog>
-#include <QPointer>
-#include <QPushButton>
-#include <QFrame>
-#include <QScrollBar>
-
-namespace {
-QString createRandomPassword()
-{
- const auto words = OCC::WordList::getRandomWords(10);
-
- const auto addFirstLetter = [](const QString &current, const QString &next) -> QString {
- return current + next.at(0);
- };
-
- return std::accumulate(std::cbegin(words), std::cend(words), QString(), addFirstLetter);
-}
-}
-
-
-namespace OCC {
-
-static const int thumbnailSize = 40;
-
-ShareDialog::ShareDialog(QPointer<AccountState> accountState,
- const QString &sharePath,
- const QString &localPath,
- SharePermissions maxSharingPermissions,
- const QByteArray &numericFileId,
- SyncJournalFileLockInfo filelockState,
- ShareDialogStartPage startPage,
- QWidget *parent)
- : QDialog(parent)
- , _ui(new Ui::ShareDialog)
- , _accountState(accountState)
- , _sharePath(sharePath)
- , _localPath(localPath)
- , _maxSharingPermissions(maxSharingPermissions)
- , _filelockState(std::move(filelockState))
- , _privateLinkUrl(accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded))
- , _startPage(startPage)
-{
- setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
- setAttribute(Qt::WA_DeleteOnClose);
- setObjectName("SharingDialog"); // required as group for saveGeometry call
-
- _ui->setupUi(this);
-
- // We want to act on account state changes
- connect(_accountState.data(), &AccountState::stateChanged, this, &ShareDialog::slotAccountStateChanged);
-
- // Set icon
- QFileInfo f_info(_localPath);
- QFileIconProvider icon_provider;
- QIcon icon = icon_provider.icon(f_info);
- auto pixmap = icon.pixmap(thumbnailSize, thumbnailSize);
- if (pixmap.width() > 0) {
- _ui->label_icon->setPixmap(pixmap);
- }
-
- // Set filename
- QString fileName = QFileInfo(_sharePath).fileName();
- _ui->label_name->setText(tr("%1").arg(fileName));
- QFont f(_ui->label_name->font());
- f.setPointSize(qRound(f.pointSize() * 1.4));
- _ui->label_name->setFont(f);
-
- if (_filelockState._locked) {
- static constexpr auto SECONDS_PER_MINUTE = 60;
- const auto lockExpirationTime = _filelockState._lockTime + _filelockState._lockTimeout;
- const auto remainingTime = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime));
- const auto remainingTimeInMinute = static_cast<int>(remainingTime > 0 ? remainingTime / SECONDS_PER_MINUTE : 0);
- _ui->label_lockinfo->setText(tr("Locked by %1 - Expires in %2 minutes", "remaining time before lock expires", remainingTimeInMinute).arg(_filelockState._lockOwnerDisplayName).arg(remainingTimeInMinute));
- } else {
- _ui->label_lockinfo->setVisible(false);
- }
-
- QString ocDir(_sharePath);
- ocDir.truncate(ocDir.length() - fileName.length());
-
- ocDir.replace(QRegularExpression("^/*"), "");
- ocDir.replace(QRegularExpression("/*$"), "");
-
- // Laying this out is complex because sharePath
- // may be in use or not.
- _ui->gridLayout->removeWidget(_ui->label_sharePath);
- _ui->gridLayout->removeWidget(_ui->label_name);
- if (ocDir.isEmpty()) {
- _ui->gridLayout->addWidget(_ui->label_name, 0, 1, 2, 1);
- _ui->label_sharePath->setText(QString());
- } else {
- _ui->gridLayout->addWidget(_ui->label_name, 0, 1, 1, 1);
- _ui->gridLayout->addWidget(_ui->label_sharePath, 1, 1, 1, 1);
- _ui->label_sharePath->setText(tr("Folder: %2").arg(ocDir));
- }
-
- this->setWindowTitle(tr("%1 Sharing").arg(Theme::instance()->appNameGUI()));
-
- if (!accountState->account()->capabilities().shareAPI()) {
- return;
- }
-
- if (QFileInfo(_localPath).isFile()) {
- auto *job = new ThumbnailJob(_sharePath, _accountState->account(), this);
- connect(job, &ThumbnailJob::jobFinished, this, &ShareDialog::slotThumbnailFetched);
- job->start();
- }
-
- auto job = new PropfindJob(accountState->account(), _sharePath);
- job->setProperties(
- QList<QByteArray>()
- << "http://open-collaboration-services.org/ns:share-permissions"
- << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
- << "http://owncloud.org/ns:privatelink");
- job->setTimeout(10 * 1000);
- connect(job, &PropfindJob::result, this, &ShareDialog::slotPropfindReceived);
- connect(job, &PropfindJob::finishedWithError, this, &ShareDialog::slotPropfindError);
- job->start();
-
- initShareManager();
-
- _scrollAreaViewPort = new QWidget(_ui->scrollArea);
- _scrollAreaLayout = new QVBoxLayout(_scrollAreaViewPort);
- _scrollAreaLayout->setContentsMargins(0, 0, 0, 0);
- _ui->scrollArea->setWidget(_scrollAreaViewPort);
-
- _internalLinkWidget = new InternalLinkWidget(localPath, this);
- _ui->verticalLayout->addWidget(_internalLinkWidget);
- _internalLinkWidget->setupUiOptions();
- connect(this, &ShareDialog::styleChanged, _internalLinkWidget, &InternalLinkWidget::slotStyleChanged);
-
- adjustScrollWidget();
-}
-
-ShareLinkWidget *ShareDialog::addLinkShareWidget(const QSharedPointer<LinkShare> &linkShare)
-{
- const auto linkShareWidget = new ShareLinkWidget(_accountState->account(), _sharePath, _localPath, _maxSharingPermissions, _ui->scrollArea);
- _linkWidgetList.append(linkShareWidget);
-
- linkShareWidget->setLinkShare(linkShare);
-
- connect(linkShare.data(), &Share::serverError, linkShareWidget, &ShareLinkWidget::slotServerError);
- connect(linkShare.data(), &Share::shareDeleted, linkShareWidget, &ShareLinkWidget::slotDeleteShareFetched);
-
- if(_manager) {
- connect(_manager, &ShareManager::serverError, linkShareWidget, &ShareLinkWidget::slotServerError);
- }
-
- // Connect all shares signals to gui slots
- connect(this, &ShareDialog::toggleShareLinkAnimation, linkShareWidget, &ShareLinkWidget::slotToggleShareLinkAnimation);
- connect(linkShareWidget, &ShareLinkWidget::createLinkShare, this, &ShareDialog::slotCreateLinkShare);
- connect(linkShareWidget, &ShareLinkWidget::deleteLinkShare, this, &ShareDialog::slotDeleteShare);
- connect(linkShareWidget, &ShareLinkWidget::createPassword, this, &ShareDialog::slotCreatePasswordForLinkShare);
-
- // Connect styleChanged events to our widget, so it can adapt (Dark-/Light-Mode switching)
- connect(this, &ShareDialog::styleChanged, linkShareWidget, &ShareLinkWidget::slotStyleChanged);
-
- _ui->verticalLayout->insertWidget(_linkWidgetList.size() + 1, linkShareWidget);
- _scrollAreaLayout->addWidget(linkShareWidget);
-
- linkShareWidget->setupUiOptions();
- adjustScrollWidget();
-
- return linkShareWidget;
-}
-
-void ShareDialog::initLinkShareWidget()
-{
- if(_linkWidgetList.size() == 0) {
- _emptyShareLinkWidget = new ShareLinkWidget(_accountState->account(), _sharePath, _localPath, _maxSharingPermissions, _ui->scrollArea);
- _linkWidgetList.append(_emptyShareLinkWidget);
-
- _emptyShareLinkWidget->slotStyleChanged(); // Get the initial customizeStyle() to happen
-
- connect(this, &ShareDialog::toggleShareLinkAnimation, _emptyShareLinkWidget, &ShareLinkWidget::slotToggleShareLinkAnimation);
- connect(this, &ShareDialog::styleChanged, _emptyShareLinkWidget, &ShareLinkWidget::slotStyleChanged);
-
- connect(_emptyShareLinkWidget, &ShareLinkWidget::createLinkShare, this, &ShareDialog::slotCreateLinkShare);
- connect(_emptyShareLinkWidget, &ShareLinkWidget::createPassword, this, &ShareDialog::slotCreatePasswordForLinkShare);
-
- _ui->verticalLayout->insertWidget(_linkWidgetList.size()+1, _emptyShareLinkWidget);
- _scrollAreaLayout->addWidget(_emptyShareLinkWidget);
- _emptyShareLinkWidget->show();
- } else if (_emptyShareLinkWidget) {
- _emptyShareLinkWidget->hide();
- _ui->verticalLayout->removeWidget(_emptyShareLinkWidget);
- _linkWidgetList.removeAll(_emptyShareLinkWidget);
- _emptyShareLinkWidget = nullptr;
- }
-
- adjustScrollWidget();
-}
-
-void ShareDialog::slotAddLinkShareWidget(const QSharedPointer<LinkShare> &linkShare)
-{
- emit toggleShareLinkAnimation(true);
- const auto addedLinkShareWidget = addLinkShareWidget(linkShare);
- initLinkShareWidget();
- if (linkShare->isPasswordSet()) {
- addedLinkShareWidget->focusPasswordLineEdit();
- }
- emit toggleShareLinkAnimation(false);
-}
-
-void ShareDialog::slotSharesFetched(const QList<QSharedPointer<Share>> &shares)
-{
- emit toggleShareLinkAnimation(true);
-
- const QString versionString = _accountState->account()->serverVersion();
- qCInfo(lcSharing) << versionString << "Fetched" << shares.count() << "shares";
-
- foreach (auto share, shares) {
- if (share->getShareType() != Share::TypeLink || share->getUidOwner() != share->account()->davUser()) {
- continue;
- }
-
- QSharedPointer<LinkShare> linkShare = qSharedPointerDynamicCast<LinkShare>(share);
- addLinkShareWidget(linkShare);
- }
-
- initLinkShareWidget();
- emit toggleShareLinkAnimation(false);
-}
-
-void ShareDialog::adjustScrollWidget()
-{
- _ui->scrollArea->setVisible(_scrollAreaLayout->count() > 0);
-
- // Sometimes the contentRect returns a height of 0, so we need a backup plan
- const auto scrollAreaContentHeight = _scrollAreaLayout->contentsRect().height();
-
- auto linkWidgetHeights = 0;
-
- if(scrollAreaContentHeight == 0 && !_linkWidgetList.empty()) {
- for (const auto linkWidget : _linkWidgetList) {
- linkWidgetHeights += linkWidget->height() - 10;
- }
- }
-
- const auto overAvailableHeight = scrollAreaContentHeight > _ui->scrollArea->height() ||
- linkWidgetHeights > _ui->scrollArea->height();
-
- _ui->scrollArea->setFrameShape(overAvailableHeight ? QFrame::StyledPanel : QFrame::NoFrame);
- _ui->verticalLayout->setSpacing(overAvailableHeight ? 10 : 0);
-}
-
-ShareDialog::~ShareDialog()
-{
- _linkWidgetList.clear();
- delete _ui;
-}
-
-void ShareDialog::done(int r)
-{
- ConfigFile cfg;
- cfg.saveGeometry(this);
- QDialog::done(r);
-}
-
-void ShareDialog::slotPropfindReceived(const QVariantMap &result)
-{
- const QVariant receivedPermissions = result["share-permissions"];
- if (!receivedPermissions.toString().isEmpty()) {
- _maxSharingPermissions = static_cast<SharePermissions>(receivedPermissions.toInt());
- qCInfo(lcSharing) << "Received sharing permissions for" << _sharePath << _maxSharingPermissions;
- }
- auto privateLinkUrl = result["privatelink"].toString();
- auto numericFileId = result["fileid"].toByteArray();
- if (!privateLinkUrl.isEmpty()) {
- qCInfo(lcSharing) << "Received private link url for" << _sharePath << privateLinkUrl;
- _privateLinkUrl = privateLinkUrl;
- } else if (!numericFileId.isEmpty()) {
- qCInfo(lcSharing) << "Received numeric file id for" << _sharePath << numericFileId;
- _privateLinkUrl = _accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded);
- }
-
- showSharingUi();
-}
-
-void ShareDialog::slotPropfindError()
-{
- // On error show the share ui anyway. The user can still see shares,
- // delete them and so on, even though adding new shares or granting
- // some of the permissions might fail.
-
- showSharingUi();
-}
-
-void ShareDialog::showSharingUi()
-{
- auto theme = Theme::instance();
-
- // There's no difference between being unable to reshare and
- // being unable to reshare with reshare permission.
- bool canReshare = _maxSharingPermissions & SharePermissionShare;
-
- if (!canReshare) {
- auto label = new QLabel(this);
- label->setText(tr("The file cannot be shared because it does not have sharing permission."));
- label->setWordWrap(true);
- _ui->verticalLayout->insertWidget(1, label);
- return;
- }
-
- if (theme->userGroupSharing()) {
- _userGroupWidget = new ShareUserGroupWidget(_accountState->account(), _sharePath, _localPath, _maxSharingPermissions, _privateLinkUrl, _ui->scrollArea);
- _userGroupWidget->getShares();
-
- // Connect styleChanged events to our widget, so it can adapt (Dark-/Light-Mode switching)
- connect(this, &ShareDialog::styleChanged, _userGroupWidget, &ShareUserGroupWidget::slotStyleChanged);
-
- _userGroupWidget->slotStyleChanged();
-
- _ui->verticalLayout->insertWidget(1, _userGroupWidget);
- _scrollAreaLayout->addLayout(_userGroupWidget->shareUserGroupLayout());
- }
-
- initShareManager();
-
- if (theme->linkSharing()) {
- if(_manager) {
- _manager->fetchShares(_sharePath);
- }
- }
-
- adjustScrollWidget();
-}
-
-void ShareDialog::initShareManager()
-{
- bool sharingPossible = true;
- if (!_accountState->account()->capabilities().sharePublicLink()) {
- qCWarning(lcSharing) << "Link shares have been disabled";
- sharingPossible = false;
- } else if (!(_maxSharingPermissions & SharePermissionShare)) {
- qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission.";
- sharingPossible = false;
- }
-
- if (!_manager && sharingPossible) {
- _manager = new ShareManager(_accountState->account(), this);
- connect(_manager, &ShareManager::sharesFetched, this, &ShareDialog::slotSharesFetched);
- connect(_manager, &ShareManager::linkShareCreated, this, &ShareDialog::slotAddLinkShareWidget);
- connect(_manager, &ShareManager::linkShareRequiresPassword, this, &ShareDialog::slotLinkShareRequiresPassword);
- }
-}
-
-void ShareDialog::slotCreateLinkShare()
-{
- if(_manager) {
- const auto askOptionalPassword = _accountState->account()->capabilities().sharePublicLinkAskOptionalPassword();
- const auto password = askOptionalPassword ? createRandomPassword() : QString();
- _manager->createLinkShare(_sharePath, QString(), password);
- }
-}
-
-void ShareDialog::slotCreatePasswordForLinkShare(const QString &password)
-{
- const auto shareLinkWidget = qobject_cast<ShareLinkWidget*>(sender());
- Q_ASSERT(shareLinkWidget);
- if (shareLinkWidget) {
- connect(_manager, &ShareManager::linkShareRequiresPassword, shareLinkWidget, &ShareLinkWidget::slotCreateShareRequiresPassword);
- connect(shareLinkWidget, &ShareLinkWidget::createPasswordProcessed, this, &ShareDialog::slotCreatePasswordForLinkShareProcessed);
- shareLinkWidget->getLinkShare()->setPassword(password);
- } else {
- qCCritical(lcSharing) << "shareLinkWidget is not a sender!";
- }
-}
-
-void ShareDialog::slotCreatePasswordForLinkShareProcessed()
-{
- const auto shareLinkWidget = qobject_cast<ShareLinkWidget*>(sender());
- Q_ASSERT(shareLinkWidget);
- if (shareLinkWidget) {
- disconnect(_manager, &ShareManager::linkShareRequiresPassword, shareLinkWidget, &ShareLinkWidget::slotCreateShareRequiresPassword);
- disconnect(shareLinkWidget, &ShareLinkWidget::createPasswordProcessed, this, &ShareDialog::slotCreatePasswordForLinkShareProcessed);
- } else {
- qCCritical(lcSharing) << "shareLinkWidget is not a sender!";
- }
-}
-
-void ShareDialog::slotLinkShareRequiresPassword(const QString &message)
-{
- const auto passwordInputDialog = new PasswordInputDialog(tr("Please enter a password for your link share:"), message, this);
- passwordInputDialog->setWindowTitle(tr("Password for share required"));
- passwordInputDialog->setAttribute(Qt::WA_DeleteOnClose);
- passwordInputDialog->open();
-
- connect(passwordInputDialog, &QDialog::finished, this, [this, passwordInputDialog](const int result) {
- if (result == QDialog::Accepted && _manager) {
- // Try to create the link share again with the newly entered password
- _manager->createLinkShare(_sharePath, QString(), passwordInputDialog->password());
- return;
- }
- emit toggleShareLinkAnimation(false);
- });
-}
-
-void ShareDialog::slotDeleteShare()
-{
- auto sharelinkWidget = dynamic_cast<ShareLinkWidget*>(sender());
- sharelinkWidget->hide();
- _ui->verticalLayout->removeWidget(sharelinkWidget);
- _scrollAreaLayout->removeWidget(sharelinkWidget);
- _linkWidgetList.removeAll(sharelinkWidget);
- initLinkShareWidget();
-}
-
-void ShareDialog::slotThumbnailFetched(const int &statusCode, const QByteArray &reply)
-{
- if (statusCode != 200) {
- qCWarning(lcSharing) << "Thumbnail status code: " << statusCode;
- return;
- }
-
- QPixmap p;
- p.loadFromData(reply, "PNG");
- p = p.scaledToHeight(thumbnailSize, Qt::SmoothTransformation);
- _ui->label_icon->setPixmap(p);
- _ui->label_icon->show();
-}
-
-void ShareDialog::slotAccountStateChanged(int state)
-{
- bool enabled = (state == AccountState::State::Connected);
- qCDebug(lcSharing) << "Account connected?" << enabled;
-
- if (_userGroupWidget) {
- _userGroupWidget->setEnabled(enabled);
- }
-
- if(_linkWidgetList.size() > 0){
- foreach(ShareLinkWidget *widget, _linkWidgetList){
- widget->setEnabled(state);
- }
- }
-}
-
-void ShareDialog::changeEvent(QEvent *e)
-{
- switch (e->type()) {
- case QEvent::StyleChange:
- case QEvent::PaletteChange:
- case QEvent::ThemeChange:
- // Notify the other widgets (Dark-/Light-Mode switching)
- emit styleChanged();
- break;
- default:
- break;
- }
-
- QDialog::changeEvent(e);
-}
-
-void ShareDialog::resizeEvent(QResizeEvent *event)
-{
- adjustScrollWidget();
- QDialog::resizeEvent(event);
-}
-
-} // namespace OCC
diff --git a/src/gui/sharedialog.h b/src/gui/sharedialog.h
deleted file mode 100644
index 89bb2ef70..000000000
--- a/src/gui/sharedialog.h
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- *
- * 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 SHAREDIALOG_H
-#define SHAREDIALOG_H
-
-#include "accountstate.h"
-#include "sharepermissions.h"
-#include "owncloudgui.h"
-#include "common/syncjournalfilerecord.h"
-
-#include <QSharedPointer>
-#include <QPointer>
-#include <QString>
-#include <QDialog>
-#include <QWidget>
-
-class QProgressIndicator;
-class QVBoxLayout;
-
-namespace OCC {
-
-namespace Ui {
- class ShareDialog;
-}
-
-class ShareLinkWidget;
-class InternalLinkWidget;
-class ShareUserGroupWidget;
-class ShareManager;
-class LinkShare;
-class Share;
-
-class ShareDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit ShareDialog(QPointer<AccountState> accountState,
- const QString &sharePath,
- const QString &localPath,
- SharePermissions maxSharingPermissions,
- const QByteArray &numericFileId,
- SyncJournalFileLockInfo filelockState,
- ShareDialogStartPage startPage,
- QWidget *parent = nullptr);
- ~ShareDialog() override;
-
-private slots:
- void done(int r) override;
- void slotPropfindReceived(const QVariantMap &result);
- void slotPropfindError();
- void slotThumbnailFetched(const int &statusCode, const QByteArray &reply);
- void slotAccountStateChanged(int state);
-
- void slotSharesFetched(const QList<QSharedPointer<Share>> &shares);
- void slotAddLinkShareWidget(const QSharedPointer<LinkShare> &linkShare);
- void slotDeleteShare();
- void slotCreateLinkShare();
- void slotCreatePasswordForLinkShare(const QString &password);
- void slotCreatePasswordForLinkShareProcessed();
- void slotLinkShareRequiresPassword(const QString &message);
-
-signals:
- void toggleShareLinkAnimation(bool start);
- void styleChanged();
-
-protected:
- void changeEvent(QEvent *) override;
- void resizeEvent(QResizeEvent *event) override;
-
-private:
- void showSharingUi();
- void initShareManager();
- ShareLinkWidget *addLinkShareWidget(const QSharedPointer<LinkShare> &linkShare);
- void initLinkShareWidget();
- void adjustScrollWidget();
-
- Ui::ShareDialog *_ui;
-
- QPointer<AccountState> _accountState;
- QString _sharePath;
- QString _localPath;
- SharePermissions _maxSharingPermissions;
- QByteArray _numericFileId;
- SyncJournalFileLockInfo _filelockState;
- QString _privateLinkUrl;
- ShareDialogStartPage _startPage;
- ShareManager *_manager = nullptr;
-
- QList<ShareLinkWidget*> _linkWidgetList;
- ShareLinkWidget* _emptyShareLinkWidget = nullptr;
- InternalLinkWidget* _internalLinkWidget = nullptr;
- ShareUserGroupWidget *_userGroupWidget = nullptr;
- QProgressIndicator *_progressIndicator = nullptr;
-
- QWidget *_scrollAreaViewPort = nullptr;
- QVBoxLayout *_scrollAreaLayout = nullptr;
-};
-
-} // namespace OCC
-
-#endif // SHAREDIALOG_H
diff --git a/src/gui/sharedialog.ui b/src/gui/sharedialog.ui
deleted file mode 100644
index 8e4bbbfd8..000000000
--- a/src/gui/sharedialog.ui
+++ /dev/null
@@ -1,217 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>OCC::ShareDialog</class>
- <widget class="QDialog" name="OCC::ShareDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>385</width>
- <height>400</height>
- </rect>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>320</width>
- <height>240</height>
- </size>
- </property>
- <layout class="QVBoxLayout" name="shareDialogVerticalLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetDefaultConstraint</enum>
- </property>
- <item>
- <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0" columnstretch="0,0">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="spacing">
- <number>2</number>
- </property>
- <item row="0" column="1">
- <widget class="QLabel" name="label_name">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>315</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string>share label</string>
- </property>
- <property name="textFormat">
- <enum>Qt::PlainText</enum>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="3" column="1">
- <widget class="QLabel" name="label_lockinfo">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>315</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string notr="true">TextLabel</string>
- </property>
- <property name="textFormat">
- <enum>Qt::PlainText</enum>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QLabel" name="label_sharePath">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>315</width>
- <height>0</height>
- </size>
- </property>
- <property name="font">
- <font>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Nextcloud Path:</string>
- </property>
- <property name="textFormat">
- <enum>Qt::PlainText</enum>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="0" column="0" rowspan="4">
- <widget class="QLabel" name="label_icon">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>40</width>
- <height>40</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string>Icon</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QScrollArea" name="scrollArea">
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="frameShape">
- <enum>QFrame::NoFrame</enum>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Plain</enum>
- </property>
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAsNeeded</enum>
- </property>
- <property name="horizontalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOff</enum>
- </property>
- <property name="sizeAdjustPolicy">
- <enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
- </property>
- <property name="widgetResizable">
- <bool>true</bool>
- </property>
- <widget class="QWidget" name="scrollAreaWidgetContents">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>359</width>
- <height>320</height>
- </rect>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- </widget>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/gui/sharee.cpp b/src/gui/sharee.cpp
index ed447e2a8..206a9d0e6 100644
--- a/src/gui/sharee.cpp
+++ b/src/gui/sharee.cpp
@@ -66,160 +66,4 @@ Sharee::Type Sharee::type() const
return _type;
}
-ShareeModel::ShareeModel(const AccountPtr &account, const QString &type, QObject *parent)
- : QAbstractListModel(parent)
- , _account(account)
- , _type(type)
-{
-}
-
-void ShareeModel::fetch(const QString &search, const ShareeSet &blacklist, LookupMode lookupMode)
-{
- _search = search;
- _shareeBlacklist = blacklist;
- auto *job = new OcsShareeJob(_account);
- connect(job, &OcsShareeJob::shareeJobFinished, this, &ShareeModel::shareesFetched);
- connect(job, &OcsJob::ocsError, this, &ShareeModel::displayErrorMessage);
- job->getSharees(_search, _type, 1, 50, lookupMode == GlobalSearch ? true : false);
-}
-
-void ShareeModel::shareesFetched(const QJsonDocument &reply)
-{
- QVector<QSharedPointer<Sharee>> newSharees;
-
- {
- const QStringList shareeTypes {"users", "groups", "emails", "remotes", "circles", "rooms"};
-
- const auto appendSharees = [this, &shareeTypes](const QJsonObject &data, QVector<QSharedPointer<Sharee>>& out) {
- for (const auto &shareeType : shareeTypes) {
- const auto category = data.value(shareeType).toArray();
- for (const auto &sharee : category) {
- out.append(parseSharee(sharee.toObject()));
- }
- }
- };
-
- appendSharees(reply.object().value("ocs").toObject().value("data").toObject(), newSharees);
- appendSharees(reply.object().value("ocs").toObject().value("data").toObject().value("exact").toObject(), newSharees);
- }
-
- // Filter sharees that we have already shared with
- QVector<QSharedPointer<Sharee>> filteredSharees;
- foreach (const auto &sharee, newSharees) {
- bool found = false;
- foreach (const auto &blacklistSharee, _shareeBlacklist) {
- if (sharee->type() == blacklistSharee->type() && sharee->shareWith() == blacklistSharee->shareWith()) {
- found = true;
- break;
- }
- }
-
- if (found == false) {
- filteredSharees.append(sharee);
- }
- }
-
- setNewSharees(filteredSharees);
- shareesReady();
-}
-
-QSharedPointer<Sharee> ShareeModel::parseSharee(const QJsonObject &data)
-{
- QString displayName = data.value("label").toString();
- const QString shareWith = data.value("value").toObject().value("shareWith").toString();
- Sharee::Type type = (Sharee::Type)data.value("value").toObject().value("shareType").toInt();
- const QString additionalInfo = data.value("value").toObject().value("shareWithAdditionalInfo").toString();
- if (!additionalInfo.isEmpty()) {
- displayName = tr("%1 (%2)", "sharee (shareWithAdditionalInfo)").arg(displayName, additionalInfo);
- }
-
- return QSharedPointer<Sharee>(new Sharee(shareWith, displayName, type));
-}
-
-
-// Helper function for setNewSharees (could be a lambda when we can use them)
-static QSharedPointer<Sharee> shareeFromModelIndex(const QModelIndex &idx)
-{
- return idx.data(Qt::UserRole).value<QSharedPointer<Sharee>>();
-}
-
-struct FindShareeHelper
-{
- const QSharedPointer<Sharee> &sharee;
- bool operator()(const QSharedPointer<Sharee> &s2) const
- {
- return s2->format() == sharee->format() && s2->displayName() == sharee->format();
- }
-};
-
-/* Set the new sharee
-
- Do that while preserving the model index so the selection stays
-*/
-void ShareeModel::setNewSharees(const QVector<QSharedPointer<Sharee>> &newSharees)
-{
- layoutAboutToBeChanged();
- const auto persistent = persistentIndexList();
- QVector<QSharedPointer<Sharee>> oldPersistantSharee;
- oldPersistantSharee.reserve(persistent.size());
-
- std::transform(persistent.begin(), persistent.end(), std::back_inserter(oldPersistantSharee),
- shareeFromModelIndex);
-
- _sharees = newSharees;
-
- QModelIndexList newPersistant;
- newPersistant.reserve(persistent.size());
- foreach (const QSharedPointer<Sharee> &sharee, oldPersistantSharee) {
- FindShareeHelper helper = { sharee };
- auto it = std::find_if(_sharees.constBegin(), _sharees.constEnd(), helper);
- if (it == _sharees.constEnd()) {
- newPersistant << QModelIndex();
- } else {
- newPersistant << index(std::distance(_sharees.constBegin(), it));
- }
- }
-
- changePersistentIndexList(persistent, newPersistant);
- layoutChanged();
-}
-
-
-int ShareeModel::rowCount(const QModelIndex &) const
-{
- return _sharees.size();
-}
-
-QVariant ShareeModel::data(const QModelIndex &index, int role) const
-{
- if (index.row() < 0 || index.row() > _sharees.size()) {
- return QVariant();
- }
-
- const auto &sharee = _sharees.at(index.row());
- if (role == Qt::DisplayRole) {
- return sharee->format();
-
- } else if (role == Qt::EditRole) {
- // This role is used by the completer - it should match
- // the full name and the user name and thus we include both
- // in the output here. But we need to take care this string
- // doesn't leak to the user.
- return QString(sharee->displayName() + " (" + sharee->shareWith() + ")");
-
- } else if (role == Qt::UserRole) {
- return QVariant::fromValue(sharee);
- }
-
- return QVariant();
-}
-
-QSharedPointer<Sharee> ShareeModel::getSharee(int at)
-{
- if (at < 0 || at > _sharees.size()) {
- return QSharedPointer<Sharee>(nullptr);
- }
-
- return _sharees.at(at);
-}
}
diff --git a/src/gui/sharee.h b/src/gui/sharee.h
index b1aa8f2c2..2139a9117 100644
--- a/src/gui/sharee.h
+++ b/src/gui/sharee.h
@@ -61,47 +61,9 @@ private:
Type _type;
};
-
-class ShareeModel : public QAbstractListModel
-{
- Q_OBJECT
-public:
- enum LookupMode {
- LocalSearch = 0,
- GlobalSearch = 1
- };
-
- explicit ShareeModel(const AccountPtr &account, const QString &type, QObject *parent = nullptr);
-
- using ShareeSet = QVector<QSharedPointer<Sharee>>; // FIXME: make it a QSet<Sharee> when Sharee can be compared
- void fetch(const QString &search, const ShareeSet &blacklist, LookupMode lookupMode);
- [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
- [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
-
- QSharedPointer<Sharee> getSharee(int at);
-
- [[nodiscard]] QString currentSearch() const { return _search; }
-
-signals:
- void shareesReady();
- void displayErrorMessage(int code, const QString &);
-
-private slots:
- void shareesFetched(const QJsonDocument &reply);
-
-private:
- QSharedPointer<Sharee> parseSharee(const QJsonObject &data);
- void setNewSharees(const QVector<QSharedPointer<Sharee>> &newSharees);
-
- AccountPtr _account;
- QString _search;
- QString _type;
-
- QVector<QSharedPointer<Sharee>> _sharees;
- QVector<QSharedPointer<Sharee>> _shareeBlacklist;
-};
+using ShareePtr = QSharedPointer<OCC::Sharee>;
}
-Q_DECLARE_METATYPE(QSharedPointer<OCC::Sharee>)
+Q_DECLARE_METATYPE(OCC::ShareePtr)
#endif //SHAREE_H
diff --git a/src/gui/sharelinkwidget.cpp b/src/gui/sharelinkwidget.cpp
deleted file mode 100644
index 3f6b437a6..000000000
--- a/src/gui/sharelinkwidget.cpp
+++ /dev/null
@@ -1,625 +0,0 @@
-/*
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- * Copyright (C) 2015 by Klaas Freitag <freitag@owncloud.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 "ui_sharelinkwidget.h"
-#include "sharelinkwidget.h"
-#include "account.h"
-#include "capabilities.h"
-#include "guiutility.h"
-#include "sharemanager.h"
-#include "theme.h"
-#include "elidedlabel.h"
-
-#include "QProgressIndicator.h"
-#include <QBuffer>
-#include <QClipboard>
-#include <QFileInfo>
-#include <QDesktopServices>
-#include <QMessageBox>
-#include <QMenu>
-#include <QTextEdit>
-#include <QToolButton>
-#include <QPropertyAnimation>
-
-namespace {
- const char *passwordIsSetPlaceholder = "●●●●●●●●";
-}
-
-namespace OCC {
-
-Q_LOGGING_CATEGORY(lcShareLink, "nextcloud.gui.sharelink", QtInfoMsg)
-
-ShareLinkWidget::ShareLinkWidget(AccountPtr account,
- const QString &sharePath,
- const QString &localPath,
- SharePermissions maxSharingPermissions,
- QWidget *parent)
- : QWidget(parent)
- , _ui(new Ui::ShareLinkWidget)
- , _account(account)
- , _sharePath(sharePath)
- , _localPath(localPath)
- , _linkShare(nullptr)
- , _passwordRequired(false)
- , _expiryRequired(false)
- , _namesSupported(true)
- , _noteRequired(false)
- , _linkContextMenu(nullptr)
- , _readOnlyLinkAction(nullptr)
- , _allowEditingLinkAction(nullptr)
- , _allowUploadEditingLinkAction(nullptr)
- , _allowUploadLinkAction(nullptr)
- , _passwordProtectLinkAction(nullptr)
- , _expirationDateLinkAction(nullptr)
- , _unshareLinkAction(nullptr)
- , _noteLinkAction(nullptr)
-{
- _ui->setupUi(this);
-
- _ui->shareLinkToolButton->hide();
-
- //Is this a file or folder?
- QFileInfo fi(localPath);
- _isFile = fi.isFile();
-
- connect(_ui->enableShareLink, &QPushButton::clicked, this, &ShareLinkWidget::slotCreateShareLink);
- connect(_ui->lineEdit_password, &QLineEdit::returnPressed, this, &ShareLinkWidget::slotCreatePassword);
- connect(_ui->confirmPassword, &QAbstractButton::clicked, this, &ShareLinkWidget::slotCreatePassword);
- connect(_ui->confirmNote, &QAbstractButton::clicked, this, &ShareLinkWidget::slotCreateNote);
- connect(_ui->confirmExpirationDate, &QAbstractButton::clicked, this, &ShareLinkWidget::slotSetExpireDate);
-
- _ui->errorLabel->hide();
-
- if (!_account->capabilities().sharePublicLink()) {
- qCWarning(lcShareLink) << "Link shares have been disabled";
- } else if (!(maxSharingPermissions & SharePermissionShare)) {
- qCWarning(lcShareLink) << "The file can not be shared because it was shared without sharing permission.";
- }
-
- _ui->enableShareLink->setChecked(false);
- _ui->shareLinkToolButton->setEnabled(false);
- _ui->shareLinkToolButton->hide();
-
- // Older servers don't support multiple public link shares
- if (!_account->capabilities().sharePublicLinkMultiple()) {
- _namesSupported = false;
- }
-
- togglePasswordOptions(false);
- toggleExpireDateOptions(false);
- toggleNoteOptions(false);
-
- _ui->noteProgressIndicator->setVisible(false);
- _ui->passwordProgressIndicator->setVisible(false);
- _ui->expirationDateProgressIndicator->setVisible(false);
- _ui->sharelinkProgressIndicator->setVisible(false);
-
- // check if the file is already inside of a synced folder
- if (sharePath.isEmpty()) {
- qCWarning(lcShareLink) << "Unable to share files not in a sync folder.";
- return;
- }
-}
-
-ShareLinkWidget::~ShareLinkWidget()
-{
- delete _ui;
-}
-
-void ShareLinkWidget::slotToggleShareLinkAnimation(const bool start)
-{
- _ui->sharelinkProgressIndicator->setVisible(start);
- if (start) {
- if (!_ui->sharelinkProgressIndicator->isAnimated()) {
- _ui->sharelinkProgressIndicator->startAnimation();
- }
- } else {
- _ui->sharelinkProgressIndicator->stopAnimation();
- }
-}
-
-void ShareLinkWidget::toggleButtonAnimation(QToolButton *button, QProgressIndicator *progressIndicator, const QAction *checkedAction) const
-{
- auto startAnimation = false;
- const auto actionIsChecked = checkedAction->isChecked();
- if (!progressIndicator->isAnimated() && actionIsChecked) {
- progressIndicator->startAnimation();
- startAnimation = true;
- } else {
- progressIndicator->stopAnimation();
- }
-
- button->setVisible(!startAnimation && actionIsChecked);
- progressIndicator->setVisible(startAnimation && actionIsChecked);
-}
-
-void ShareLinkWidget::setLinkShare(QSharedPointer<LinkShare> linkShare)
-{
- _linkShare = linkShare;
-}
-
-QSharedPointer<LinkShare> ShareLinkWidget::getLinkShare()
-{
- return _linkShare;
-}
-
-void ShareLinkWidget::focusPasswordLineEdit()
-{
- _ui->lineEdit_password->setFocus();
-}
-
-void ShareLinkWidget::setupUiOptions()
-{
- connect(_linkShare.data(), &LinkShare::noteSet, this, &ShareLinkWidget::slotNoteSet);
- connect(_linkShare.data(), &LinkShare::passwordSet, this, &ShareLinkWidget::slotPasswordSet);
- connect(_linkShare.data(), &LinkShare::passwordSetError, this, &ShareLinkWidget::slotPasswordSetError);
- connect(_linkShare.data(), &LinkShare::labelSet, this, &ShareLinkWidget::slotLabelSet);
-
- // Prepare permissions check and create group action
- const QDate expireDate = _linkShare.data()->getExpireDate().isValid() ? _linkShare.data()->getExpireDate() : QDate();
- const SharePermissions perm = _linkShare.data()->getPermissions();
- auto checked = false;
- auto *permissionsGroup = new QActionGroup(this);
-
- // Prepare sharing menu
- _linkContextMenu = new QMenu(this);
-
- // radio button style
- permissionsGroup->setExclusive(true);
-
- if (_isFile) {
- checked = (perm & SharePermissionRead) && (perm & SharePermissionUpdate);
- _allowEditingLinkAction = _linkContextMenu->addAction(tr("Allow editing"));
- _allowEditingLinkAction->setCheckable(true);
- _allowEditingLinkAction->setChecked(checked);
-
- } else {
- checked = (perm == SharePermissionRead);
- _readOnlyLinkAction = permissionsGroup->addAction(tr("View only"));
- _readOnlyLinkAction->setCheckable(true);
- _readOnlyLinkAction->setChecked(checked);
-
- checked = (perm & SharePermissionRead) && (perm & SharePermissionCreate)
- && (perm & SharePermissionUpdate) && (perm & SharePermissionDelete);
- _allowUploadEditingLinkAction = permissionsGroup->addAction(tr("Allow upload and editing"));
- _allowUploadEditingLinkAction->setCheckable(true);
- _allowUploadEditingLinkAction->setChecked(checked);
-
- checked = (perm == SharePermissionCreate);
- _allowUploadLinkAction = permissionsGroup->addAction(tr("File drop (upload only)"));
- _allowUploadLinkAction->setCheckable(true);
- _allowUploadLinkAction->setChecked(checked);
- }
-
- _shareLinkElidedLabel = new OCC::ElidedLabel(this);
- _shareLinkElidedLabel->setElideMode(Qt::ElideRight);
- displayShareLinkLabel();
- _ui->horizontalLayout->insertWidget(2, _shareLinkElidedLabel);
-
- _shareLinkLayout = new QHBoxLayout(this);
-
- _shareLinkLabel = new QLabel(this);
- _shareLinkLabel->setPixmap(QString(":/client/theme/black/edit.svg"));
- _shareLinkLayout->addWidget(_shareLinkLabel);
-
- _shareLinkEdit = new QLineEdit(this);
- connect(_shareLinkEdit, &QLineEdit::returnPressed, this, &ShareLinkWidget::slotCreateLabel);
- _shareLinkEdit->setPlaceholderText(tr("Link name"));
- _shareLinkEdit->setText(_linkShare.data()->getLabel());
- _shareLinkLayout->addWidget(_shareLinkEdit);
-
- _shareLinkButton = new QToolButton(this);
- connect(_shareLinkButton, &QToolButton::clicked, this, &ShareLinkWidget::slotCreateLabel);
- _shareLinkButton->setIcon(QIcon(":/client/theme/confirm.svg"));
- _shareLinkButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
- _shareLinkLayout->addWidget(_shareLinkButton);
-
- _shareLinkProgressIndicator = new QProgressIndicator(this);
- _shareLinkProgressIndicator->setVisible(false);
- _shareLinkLayout->addWidget(_shareLinkProgressIndicator);
-
- _shareLinkDefaultWidget = new QWidget(this);
- _shareLinkDefaultWidget->setLayout(_shareLinkLayout);
-
- _shareLinkWidgetAction = new QWidgetAction(this);
- _shareLinkWidgetAction->setDefaultWidget(_shareLinkDefaultWidget);
- _shareLinkWidgetAction->setCheckable(true);
- _linkContextMenu->addAction(_shareLinkWidgetAction);
-
- // Adds permissions actions (radio button style)
- if (_isFile) {
- _linkContextMenu->addAction(_allowEditingLinkAction);
- } else {
- _linkContextMenu->addAction(_readOnlyLinkAction);
- _linkContextMenu->addAction(_allowUploadEditingLinkAction);
- _linkContextMenu->addAction(_allowUploadLinkAction);
- }
-
- // Adds action to display note widget (check box)
- _noteLinkAction = _linkContextMenu->addAction(tr("Note to recipient"));
- _noteLinkAction->setCheckable(true);
-
- if (_linkShare->getNote().isSimpleText() && !_linkShare->getNote().isEmpty()) {
- _ui->textEdit_note->setText(_linkShare->getNote());
- _noteLinkAction->setChecked(true);
- toggleNoteOptions();
- }
-
- // Adds action to display password widget (check box)
- _passwordProtectLinkAction = _linkContextMenu->addAction(tr("Password protect"));
- _passwordProtectLinkAction->setCheckable(true);
-
- if (_linkShare.data()->isPasswordSet()) {
- _passwordProtectLinkAction->setChecked(true);
- _ui->lineEdit_password->setPlaceholderText(QString::fromUtf8(passwordIsSetPlaceholder));
- togglePasswordOptions();
- }
-
- // If password is enforced then don't allow users to disable it
- if (_account->capabilities().sharePublicLinkEnforcePassword()) {
- if (_linkShare.data()->isPasswordSet()) {
- _passwordProtectLinkAction->setChecked(true);
- _passwordProtectLinkAction->setEnabled(false);
- }
- _passwordRequired = true;
- }
-
- // Adds action to display expiration date widget (check box)
- _expirationDateLinkAction = _linkContextMenu->addAction(tr("Set expiration date"));
- _expirationDateLinkAction->setCheckable(true);
- if (!expireDate.isNull()) {
- _ui->calendar->setDate(expireDate);
- _expirationDateLinkAction->setChecked(true);
- toggleExpireDateOptions();
- }
- connect(_ui->calendar, &QDateTimeEdit::dateChanged, this, &ShareLinkWidget::slotSetExpireDate);
- connect(_linkShare.data(), &LinkShare::expireDateSet, this, &ShareLinkWidget::slotExpireDateSet);
-
-
- // If expiredate is enforced do not allow disable and set max days
- if (_account->capabilities().sharePublicLinkEnforceExpireDate()) {
- _ui->calendar->setMaximumDate(QDate::currentDate().addDays(
- _account->capabilities().sharePublicLinkExpireDateDays()));
- _expirationDateLinkAction->setChecked(true);
- _expirationDateLinkAction->setEnabled(false);
- _expiryRequired = true;
- }
-
- // Adds action to unshare widget (check box)
- _unshareLinkAction.reset(_linkContextMenu->addAction(QIcon(":/client/theme/delete.svg"),
- tr("Delete link")));
-
- _linkContextMenu->addSeparator();
-
- _addAnotherLinkAction.reset(_linkContextMenu->addAction(QIcon(":/client/theme/add.svg"),
- tr("Add another link")));
-
- _ui->enableShareLink->setIcon(QIcon(":/client/theme/copy.svg"));
- disconnect(_ui->enableShareLink, &QPushButton::clicked, this, &ShareLinkWidget::slotCreateShareLink);
- connect(_ui->enableShareLink, &QPushButton::clicked, this, &ShareLinkWidget::slotCopyLinkShare);
-
- connect(_linkContextMenu, &QMenu::triggered,
- this, &ShareLinkWidget::slotLinkContextMenuActionTriggered);
-
- _ui->shareLinkToolButton->setMenu(_linkContextMenu);
- _ui->shareLinkToolButton->setEnabled(true);
- _ui->enableShareLink->setEnabled(true);
- _ui->enableShareLink->setChecked(true);
-
- // show sharing options
- _ui->shareLinkToolButton->show();
-
- customizeStyle();
-}
-
-void ShareLinkWidget::slotCreateNote()
-{
- const auto note = _ui->textEdit_note->toPlainText();
- if (!_linkShare || _linkShare->getNote() == note || note.isEmpty()) {
- return;
- }
-
- toggleButtonAnimation(_ui->confirmNote, _ui->noteProgressIndicator, _noteLinkAction);
- _ui->errorLabel->hide();
- _linkShare->setNote(note);
-}
-
-void ShareLinkWidget::slotNoteSet()
-{
- toggleButtonAnimation(_ui->confirmNote, _ui->noteProgressIndicator, _noteLinkAction);
-}
-
-void ShareLinkWidget::slotCopyLinkShare(const bool clicked) const
-{
- Q_UNUSED(clicked);
-
- QApplication::clipboard()->setText(_linkShare->getLink().toString());
-}
-
-void ShareLinkWidget::slotExpireDateSet()
-{
- toggleButtonAnimation(_ui->confirmExpirationDate, _ui->expirationDateProgressIndicator, _expirationDateLinkAction);
-}
-
-void ShareLinkWidget::slotSetExpireDate()
-{
- if (!_linkShare) {
- return;
- }
-
- toggleButtonAnimation(_ui->confirmExpirationDate, _ui->expirationDateProgressIndicator, _expirationDateLinkAction);
- _ui->errorLabel->hide();
- _linkShare->setExpireDate(_ui->calendar->date());
-}
-
-void ShareLinkWidget::slotCreatePassword()
-{
- if (!_linkShare || _ui->lineEdit_password->text().isEmpty()) {
- return;
- }
-
- toggleButtonAnimation(_ui->confirmPassword, _ui->passwordProgressIndicator, _passwordProtectLinkAction);
- _ui->errorLabel->hide();
- emit createPassword(_ui->lineEdit_password->text());
-}
-
-void ShareLinkWidget::slotCreateShareLink(const bool clicked)
-{
- Q_UNUSED(clicked);
- slotToggleShareLinkAnimation(true);
- emit createLinkShare();
-}
-
-void ShareLinkWidget::slotPasswordSet()
-{
- toggleButtonAnimation(_ui->confirmPassword, _ui->passwordProgressIndicator, _passwordProtectLinkAction);
-
- _ui->lineEdit_password->setText({});
-
- if (_linkShare->isPasswordSet()) {
- _ui->lineEdit_password->setEnabled(true);
- _ui->lineEdit_password->setPlaceholderText(QString::fromUtf8(passwordIsSetPlaceholder));
- } else {
- _ui->lineEdit_password->setPlaceholderText({});
- }
-
- emit createPasswordProcessed();
-}
-
-void ShareLinkWidget::slotPasswordSetError(const int code, const QString &message)
-{
- toggleButtonAnimation(_ui->confirmPassword, _ui->passwordProgressIndicator, _passwordProtectLinkAction);
-
- slotServerError(code, message);
- togglePasswordOptions();
- _ui->lineEdit_password->setFocus();
- emit createPasswordProcessed();
-}
-
-void ShareLinkWidget::slotDeleteShareFetched()
-{
- slotToggleShareLinkAnimation(false);
-
- _linkShare.clear();
- togglePasswordOptions(false);
- toggleNoteOptions(false);
- toggleExpireDateOptions(false);
- emit deleteLinkShare();
-}
-
-void ShareLinkWidget::toggleNoteOptions(const bool enable)
-{
- _ui->noteLabel->setVisible(enable);
- _ui->textEdit_note->setVisible(enable);
- _ui->confirmNote->setVisible(enable);
- _ui->textEdit_note->setText(enable && _linkShare ? _linkShare->getNote() : QString());
-
- if (!enable && _linkShare && !_linkShare->getNote().isEmpty()) {
- _linkShare->setNote({});
- }
-}
-
-void ShareLinkWidget::slotCreateLabel()
-{
- const auto labelText = _shareLinkEdit->text();
- if (!_linkShare || _linkShare->getLabel() == labelText || labelText.isEmpty()) {
- return;
- }
- _shareLinkWidgetAction->setChecked(true);
- toggleButtonAnimation(_shareLinkButton, _shareLinkProgressIndicator, _shareLinkWidgetAction);
- _ui->errorLabel->hide();
- _linkShare->setLabel(_shareLinkEdit->text());
-}
-
-void ShareLinkWidget::slotLabelSet()
-{
- toggleButtonAnimation(_shareLinkButton, _shareLinkProgressIndicator, _shareLinkWidgetAction);
- displayShareLinkLabel();
-}
-
-void ShareLinkWidget::slotCreateShareRequiresPassword(const QString &message)
-{
- slotToggleShareLinkAnimation(message.isEmpty());
-
- if (!message.isEmpty()) {
- _ui->errorLabel->setText(message);
- _ui->errorLabel->show();
- }
-
- _passwordRequired = true;
-
- togglePasswordOptions();
-}
-
-void ShareLinkWidget::togglePasswordOptions(const bool enable)
-{
- _ui->passwordLabel->setVisible(enable);
- _ui->lineEdit_password->setVisible(enable);
- _ui->confirmPassword->setVisible(enable);
- _ui->lineEdit_password->setFocus();
-
- if (!enable && _linkShare && _linkShare->isPasswordSet()) {
- _linkShare->setPassword({});
- }
-}
-
-void ShareLinkWidget::toggleExpireDateOptions(const bool enable)
-{
- _ui->expirationLabel->setVisible(enable);
- _ui->calendar->setVisible(enable);
- _ui->confirmExpirationDate->setVisible(enable);
-
- const auto date = enable ? _linkShare->getExpireDate() : QDate::currentDate().addDays(1);
- _ui->calendar->setDate(date);
- _ui->calendar->setMinimumDate(QDate::currentDate().addDays(1));
-
- if(_account->capabilities().sharePublicLinkEnforceExpireDate()) {
- _ui->calendar->setMaximumDate(QDate::currentDate().addDays(_account->capabilities().sharePublicLinkExpireDateDays()));
- }
-
- _ui->calendar->setFocus();
-
- if (!enable && _linkShare && _linkShare->getExpireDate().isValid()) {
- _linkShare->setExpireDate({});
- }
-}
-
-void ShareLinkWidget::confirmAndDeleteShare()
-{
- auto messageBox = new QMessageBox(
- QMessageBox::Question,
- tr("Confirm Link Share Deletion"),
- tr("<p>Do you really want to delete the public link share <i>%1</i>?</p>"
- "<p>Note: This action cannot be undone.</p>")
- .arg(shareName()),
- QMessageBox::NoButton,
- this);
- QPushButton *yesButton =
- messageBox->addButton(tr("Delete"), QMessageBox::YesRole);
- messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
-
- connect(messageBox, &QMessageBox::finished, this,
- [messageBox, yesButton, this]() {
- if (messageBox->clickedButton() == yesButton) {
- this->slotToggleShareLinkAnimation(true);
- this->_linkShare->deleteShare();
- }
- });
- messageBox->open();
-}
-
-QString ShareLinkWidget::shareName() const
-{
- QString name = _linkShare->getName();
- if (!name.isEmpty())
- return name;
- if (!_namesSupported)
- return tr("Public link");
- return _linkShare->getToken();
-}
-
-void ShareLinkWidget::slotContextMenuButtonClicked()
-{
- _linkContextMenu->exec(QCursor::pos());
-}
-
-void ShareLinkWidget::slotLinkContextMenuActionTriggered(QAction *action)
-{
- const auto state = action->isChecked();
- SharePermissions perm = SharePermissionRead;
-
- if (action == _addAnotherLinkAction.data()) {
- emit createLinkShare();
-
- } else if (action == _readOnlyLinkAction && state) {
- _linkShare->setPermissions(perm);
-
- } else if (action == _allowEditingLinkAction && state) {
- perm |= SharePermissionUpdate;
- _linkShare->setPermissions(perm);
-
- } else if (action == _allowUploadEditingLinkAction && state) {
- perm |= SharePermissionCreate | SharePermissionUpdate | SharePermissionDelete;
- _linkShare->setPermissions(perm);
-
- } else if (action == _allowUploadLinkAction && state) {
- perm = SharePermissionCreate;
- _linkShare->setPermissions(perm);
-
- } else if (action == _passwordProtectLinkAction) {
- togglePasswordOptions(state);
-
- } else if (action == _expirationDateLinkAction) {
- toggleExpireDateOptions(state);
-
- } else if (action == _noteLinkAction) {
- toggleNoteOptions(state);
-
- } else if (action == _unshareLinkAction.data()) {
- confirmAndDeleteShare();
- }
-}
-
-void ShareLinkWidget::slotServerError(const int code, const QString &message)
-{
- slotToggleShareLinkAnimation(false);
-
- qCWarning(lcSharing) << "Error from server" << code << message;
- displayError(message);
-}
-
-void ShareLinkWidget::displayError(const QString &errMsg)
-{
- _ui->errorLabel->setText(errMsg);
- _ui->errorLabel->show();
-}
-
-void ShareLinkWidget::slotStyleChanged()
-{
- customizeStyle();
-}
-
-void ShareLinkWidget::customizeStyle()
-{
- if(_unshareLinkAction) {
- _unshareLinkAction->setIcon(Theme::createColorAwareIcon(":/client/theme/delete.svg"));
- }
-
- if(_addAnotherLinkAction) {
- _addAnotherLinkAction->setIcon(Theme::createColorAwareIcon(":/client/theme/add.svg"));
- }
-
- _ui->enableShareLink->setIcon(Theme::createColorAwareIcon(":/client/theme/copy.svg"));
-
- _ui->shareLinkIconLabel->setPixmap(Theme::createColorAwarePixmap(":/client/theme/public.svg"));
-
- _ui->shareLinkToolButton->setIcon(Theme::createColorAwareIcon(":/client/theme/more.svg"));
-
- _ui->confirmNote->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
- _ui->confirmPassword->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
- _ui->confirmExpirationDate->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
-
- _ui->passwordProgressIndicator->setColor(QGuiApplication::palette().color(QPalette::Text));
-}
-
-void ShareLinkWidget::displayShareLinkLabel()
-{
- _shareLinkElidedLabel->clear();
- if (!_linkShare->getLabel().isEmpty()) {
- _shareLinkElidedLabel->setText(QString("(%1)").arg(_linkShare->getLabel()));
- }
-}
-
-}
diff --git a/src/gui/sharelinkwidget.h b/src/gui/sharelinkwidget.h
deleted file mode 100644
index 7ecd9691e..000000000
--- a/src/gui/sharelinkwidget.h
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- * Copyright (C) 2015 by Klaas Freitag <freitag@owncloud.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 SHARELINKWIDGET_H
-#define SHARELINKWIDGET_H
-
-#include "accountfwd.h"
-#include "sharepermissions.h"
-#include "QProgressIndicator.h"
-#include <QDialog>
-#include <QSharedPointer>
-#include <QList>
-#include <QToolButton>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QLineEdit>
-#include <QWidgetAction>
-
-class QMenu;
-class QTableWidgetItem;
-
-namespace OCC {
-
-namespace Ui {
- class ShareLinkWidget;
-}
-
-class AbstractCredentials;
-class SyncResult;
-class LinkShare;
-class Share;
-class ElidedLabel;
-
-/**
- * @brief The ShareDialog class
- * @ingroup gui
- */
-class ShareLinkWidget : public QWidget
-{
- Q_OBJECT
-
-public:
- explicit ShareLinkWidget(AccountPtr account,
- const QString &sharePath,
- const QString &localPath,
- SharePermissions maxSharingPermissions,
- QWidget *parent = nullptr);
- ~ShareLinkWidget() override;
-
- void toggleButton(bool show);
- void setupUiOptions();
-
- void setLinkShare(QSharedPointer<LinkShare> linkShare);
- QSharedPointer<LinkShare> getLinkShare();
-
- void focusPasswordLineEdit();
-
-public slots:
- void slotDeleteShareFetched();
- void slotToggleShareLinkAnimation(const bool start);
- void slotServerError(const int code, const QString &message);
- void slotCreateShareRequiresPassword(const QString &message);
- void slotStyleChanged();
-
-private slots:
- void slotCreateShareLink(const bool clicked);
- void slotCopyLinkShare(const bool clicked) const;
-
- void slotCreatePassword();
- void slotPasswordSet();
- void slotPasswordSetError(const int code, const QString &message);
-
- void slotCreateNote();
- void slotNoteSet();
-
- void slotSetExpireDate();
- void slotExpireDateSet();
-
- void slotContextMenuButtonClicked();
- void slotLinkContextMenuActionTriggered(QAction *action);
-
- void slotCreateLabel();
- void slotLabelSet();
-
-signals:
- void createLinkShare();
- void deleteLinkShare();
- void visualDeletionDone();
- void createPassword(const QString &password);
- void createPasswordProcessed();
-
-private:
- void displayError(const QString &errMsg);
-
- void togglePasswordOptions(const bool enable = true);
- void toggleNoteOptions(const bool enable = true);
- void toggleExpireDateOptions(const bool enable = true);
- void toggleButtonAnimation(QToolButton *button, QProgressIndicator *progressIndicator, const QAction *checkedAction) const;
-
- /** Confirm with the user and then delete the share */
- void confirmAndDeleteShare();
-
- /** Retrieve a share's name, accounting for _namesSupported */
- [[nodiscard]] QString shareName() const;
-
- void customizeStyle();
-
- void displayShareLinkLabel();
-
- Ui::ShareLinkWidget *_ui;
- AccountPtr _account;
- QString _sharePath;
- QString _localPath;
- QString _shareUrl;
-
- QSharedPointer<LinkShare> _linkShare;
-
- bool _isFile;
- bool _passwordRequired;
- bool _expiryRequired;
- bool _namesSupported;
- bool _noteRequired;
-
- QMenu *_linkContextMenu;
- QAction *_readOnlyLinkAction;
- QAction *_allowEditingLinkAction;
- QAction *_allowUploadEditingLinkAction;
- QAction *_allowUploadLinkAction;
- QAction *_passwordProtectLinkAction;
- QAction *_expirationDateLinkAction;
- QScopedPointer<QAction> _unshareLinkAction;
- QScopedPointer<QAction> _addAnotherLinkAction;
- QAction *_noteLinkAction;
- QHBoxLayout *_shareLinkLayout{};
- QLabel *_shareLinkLabel{};
- ElidedLabel *_shareLinkElidedLabel{};
- QLineEdit *_shareLinkEdit{};
- QToolButton *_shareLinkButton{};
- QProgressIndicator *_shareLinkProgressIndicator{};
- QWidget *_shareLinkDefaultWidget{};
- QWidgetAction *_shareLinkWidgetAction{};
-};
-}
-
-#endif // SHARELINKWIDGET_H
diff --git a/src/gui/sharelinkwidget.ui b/src/gui/sharelinkwidget.ui
deleted file mode 100644
index a0d5f3df9..000000000
--- a/src/gui/sharelinkwidget.ui
+++ /dev/null
@@ -1,439 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>OCC::ShareLinkWidget</class>
- <widget class="QWidget" name="OCC::ShareLinkWidget">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>400</width>
- <height>238</height>
- </rect>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="leftMargin">
- <number>12</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>20</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <property name="spacing">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QLabel" name="shareLinkIconLabel">
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="pixmap">
- <pixmap resource="../../theme.qrc">:/client/theme/public.svg</pixmap>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="shareLinkLabel">
- <property name="text">
- <string>Share link</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>25</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QProgressIndicator" name="sharelinkProgressIndicator" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>28</width>
- <height>27</height>
- </size>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>25</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="enableShareLink">
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="../../theme.qrc">
- <normaloff>:/client/theme/add.svg</normaloff>:/client/theme/add.svg</iconset>
- </property>
- <property name="checkable">
- <bool>false</bool>
- </property>
- <property name="flat">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="shareLinkToolButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="icon">
- <iconset resource="../../theme.qrc">
- <normaloff>:/client/theme/more.svg</normaloff>:/client/theme/more.svg</iconset>
- </property>
- <property name="popupMode">
- <enum>QToolButton::InstantPopup</enum>
- </property>
- <property name="autoRaise">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QGridLayout" name="gridLayout">
- <property name="leftMargin">
- <number>22</number>
- </property>
- <item row="0" column="0">
- <widget class="QLabel" name="noteLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Minimum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>78</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string>Note</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
- </property>
- <property name="indent">
- <number>0</number>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QTextEdit" name="textEdit_note">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>60</height>
- </size>
- </property>
- <property name="sizeAdjustPolicy">
- <enum>QAbstractScrollArea::AdjustToContents</enum>
- </property>
- </widget>
- </item>
- <item row="0" column="2">
- <widget class="QToolButton" name="confirmNote">
- <property name="minimumSize">
- <size>
- <width>28</width>
- <height>27</height>
- </size>
- </property>
- <property name="icon">
- <iconset resource="../../theme.qrc">
- <normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
- </property>
- <property name="autoRaise">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="0" column="2">
- <widget class="QProgressIndicator" name="noteProgressIndicator" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>28</width>
- <height>27</height>
- </size>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="passwordLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>78</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string>Set password</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
- </property>
- <property name="indent">
- <number>0</number>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QLineEdit" name="lineEdit_password">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
- <horstretch>1</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item row="1" column="2">
- <widget class="QToolButton" name="confirmPassword">
- <property name="minimumSize">
- <size>
- <width>28</width>
- <height>27</height>
- </size>
- </property>
- <property name="icon">
- <iconset resource="../../theme.qrc">
- <normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
- </property>
- <property name="autoRaise">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="1" column="2">
- <widget class="QProgressIndicator" name="passwordProgressIndicator" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>28</width>
- <height>27</height>
- </size>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QLabel" name="expirationLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>78</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string>Expires</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
- </property>
- <property name="indent">
- <number>0</number>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="QDateEdit" name="calendar">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
- <horstretch>1</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- </widget>
- </item>
- <item row="2" column="2">
- <widget class="QToolButton" name="confirmExpirationDate">
- <property name="minimumSize">
- <size>
- <width>28</width>
- <height>27</height>
- </size>
- </property>
- <property name="icon">
- <iconset resource="../../theme.qrc">
- <normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
- </property>
- <property name="autoRaise">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="2" column="2">
- <widget class="QProgressIndicator" name="expirationDateProgressIndicator" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>28</width>
- <height>27</height>
- </size>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QLabel" name="errorLabel">
- <property name="palette">
- <palette>
- <active>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </active>
- <inactive>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </inactive>
- <disabled>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>123</red>
- <green>121</green>
- <blue>134</blue>
- </color>
- </brush>
- </colorrole>
- </disabled>
- </palette>
- </property>
- <property name="text">
- <string notr="true">TextLabel</string>
- </property>
- <property name="textFormat">
- <enum>Qt::PlainText</enum>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <layoutdefault spacing="6" margin="11"/>
- <customwidgets>
- <customwidget>
- <class>QProgressIndicator</class>
- <extends>QWidget</extends>
- <header>QProgressIndicator.h</header>
- <container>1</container>
- </customwidget>
- </customwidgets>
- <resources>
- <include location="../../theme.qrc"/>
- </resources>
- <connections/>
-</ui>
diff --git a/src/gui/sharemanager.cpp b/src/gui/sharemanager.cpp
index 154bcbec3..621ab6c47 100644
--- a/src/gui/sharemanager.cpp
+++ b/src/gui/sharemanager.cpp
@@ -58,7 +58,7 @@ Share::Share(AccountPtr account,
const ShareType shareType,
bool isPasswordSet,
const Permissions permissions,
- const QSharedPointer<Sharee> shareWith)
+ const ShareePtr shareWith)
: _account(account)
, _id(id)
, _uidowner(uidowner)
@@ -101,7 +101,7 @@ Share::ShareType Share::getShareType() const
return _shareType;
}
-QSharedPointer<Sharee> Share::getShareWith() const
+ShareePtr Share::getShareWith() const
{
return _shareWith;
}
@@ -316,7 +316,7 @@ UserGroupShare::UserGroupShare(AccountPtr account,
const ShareType shareType,
bool isPasswordSet,
const Permissions permissions,
- const QSharedPointer<Sharee> shareWith,
+ const ShareePtr shareWith,
const QDate &expireDate,
const QString &note)
: Share(account, id, owner, ownerDisplayName, path, shareType, isPasswordSet, permissions, shareWith)
@@ -461,7 +461,7 @@ void ShareManager::slotShareCreated(const QJsonDocument &reply)
{
//Parse share
auto data = reply.object().value("ocs").toObject().value("data").toObject();
- QSharedPointer<Share> share(parseShare(data));
+ SharePtr share(parseShare(data));
emit shareCreated(share);
@@ -478,18 +478,19 @@ void ShareManager::fetchShares(const QString &path)
void ShareManager::slotSharesFetched(const QJsonDocument &reply)
{
+ qDebug() << reply;
auto tmpShares = reply.object().value("ocs").toObject().value("data").toArray();
const QString versionString = _account->serverVersion();
qCDebug(lcSharing) << versionString << "Fetched" << tmpShares.count() << "shares";
- QList<QSharedPointer<Share>> shares;
+ QList<SharePtr> shares;
foreach (const auto &share, tmpShares) {
auto data = share.toObject();
auto shareType = data.value("share_type").toInt();
- QSharedPointer<Share> newShare;
+ SharePtr newShare;
if (shareType == Share::TypeLink) {
newShare = parseLinkShare(data);
@@ -499,7 +500,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply)
newShare = parseShare(data);
}
- shares.append(QSharedPointer<Share>(newShare));
+ shares.append(SharePtr(newShare));
}
qCDebug(lcSharing) << "Sending " << shares.count() << "shares";
@@ -508,7 +509,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply)
QSharedPointer<UserGroupShare> ShareManager::parseUserGroupShare(const QJsonObject &data)
{
- QSharedPointer<Sharee> sharee(new Sharee(data.value("share_with").toString(),
+ ShareePtr sharee(new Sharee(data.value("share_with").toString(),
data.value("share_with_displayname").toString(),
static_cast<Sharee::Type>(data.value("share_type").toInt())));
@@ -577,13 +578,13 @@ QSharedPointer<LinkShare> ShareManager::parseLinkShare(const QJsonObject &data)
data.value("label").toString()));
}
-QSharedPointer<Share> ShareManager::parseShare(const QJsonObject &data)
+SharePtr ShareManager::parseShare(const QJsonObject &data) const
{
- QSharedPointer<Sharee> sharee(new Sharee(data.value("share_with").toString(),
+ ShareePtr sharee(new Sharee(data.value("share_with").toString(),
data.value("share_with_displayname").toString(),
(Sharee::Type)data.value("share_type").toInt()));
- return QSharedPointer<Share>(new Share(_account,
+ return SharePtr(new Share(_account,
data.value("id").toVariant().toString(), // "id" used to be an integer, support both
data.value("uid_owner").toVariant().toString(),
data.value("displayname_owner").toVariant().toString(),
diff --git a/src/gui/sharemanager.h b/src/gui/sharemanager.h
index 16d2e47c3..d5cff7c46 100644
--- a/src/gui/sharemanager.h
+++ b/src/gui/sharemanager.h
@@ -36,6 +36,15 @@ class OcsShareJob;
class Share : public QObject
{
Q_OBJECT
+ Q_PROPERTY(AccountPtr account READ account CONSTANT)
+ Q_PROPERTY(QString path READ path CONSTANT)
+ Q_PROPERTY(QString id READ getId CONSTANT)
+ Q_PROPERTY(QString uidOwner READ getUidOwner CONSTANT)
+ Q_PROPERTY(QString ownerDisplayName READ getOwnerDisplayName CONSTANT)
+ Q_PROPERTY(ShareType shareType READ getShareType CONSTANT)
+ Q_PROPERTY(ShareePtr shareWith READ getShareWith CONSTANT)
+ Q_PROPERTY(Permissions permissions READ getPermissions WRITE setPermissions NOTIFY permissionsSet)
+ Q_PROPERTY(bool isPasswordSet READ isPasswordSet NOTIFY passwordSet)
public:
/**
@@ -43,14 +52,16 @@ public:
* Need to be in sync with Sharee::Type
*/
enum ShareType {
+ TypePlaceholderLink = -1,
TypeUser = Sharee::User,
TypeGroup = Sharee::Group,
TypeLink = 3,
TypeEmail = Sharee::Email,
TypeRemote = Sharee::Federated,
TypeCircle = Sharee::Circle,
- TypeRoom = Sharee::Room
+ TypeRoom = Sharee::Room,
};
+ Q_ENUM(ShareType);
using Permissions = SharePermissions;
@@ -65,7 +76,7 @@ public:
const ShareType shareType,
bool isPasswordSet = false,
const Permissions permissions = SharePermissionDefault,
- const QSharedPointer<Sharee> shareWith = QSharedPointer<Sharee>(nullptr));
+ const ShareePtr shareWith = ShareePtr(nullptr));
/**
* The account the share is defined on.
@@ -97,7 +108,7 @@ public:
/*
* Get the shareWith
*/
- [[nodiscard]] QSharedPointer<Sharee> getShareWith() const;
+ [[nodiscard]] ShareePtr getShareWith() const;
/*
* Get permissions
@@ -105,23 +116,23 @@ public:
[[nodiscard]] Permissions getPermissions() const;
/*
- * Set the permissions of a share
- *
- * On success the permissionsSet signal is emitted
- * In case of a server error the serverError signal is emitted.
+ * Get whether the share has a password set
*/
- void setPermissions(Permissions permissions);
+ [[nodiscard]] Q_REQUIRED_RESULT bool isPasswordSet() const;
- /*
- * Set the password for remote share
- *
- * On success the passwordSet signal is emitted
- * In case of a server error the passwordSetError signal is emitted.
+ /*
+ * Is it a share with a user or group (local or remote)
*/
- void setPassword(const QString &password);
+ [[nodiscard]] static bool isShareTypeUserGroupEmailRoomOrRemote(const ShareType type);
- [[nodiscard]] bool isPasswordSet() const;
+signals:
+ void permissionsSet();
+ void shareDeleted();
+ void serverError(int code, const QString &message);
+ void passwordSet();
+ void passwordSetError(int statusCode, const QString &message);
+public slots:
/*
* Deletes a share
*
@@ -130,17 +141,21 @@ public:
*/
void deleteShare();
- /*
- * Is it a share with a user or group (local or remote)
+ /*
+ * Set the permissions of a share
+ *
+ * On success the permissionsSet signal is emitted
+ * In case of a server error the serverError signal is emitted.
*/
- static bool isShareTypeUserGroupEmailRoomOrRemote(const ShareType type);
+ void setPermissions(Permissions permissions);
-signals:
- void permissionsSet();
- void shareDeleted();
- void serverError(int code, const QString &message);
- void passwordSet();
- void passwordSetError(int statusCode, const QString &message);
+ /*
+ * Set the password for remote share
+ *
+ * On success the passwordSet signal is emitted
+ * In case of a server error the passwordSetError signal is emitted.
+ */
+ void setPassword(const QString &password);
protected:
AccountPtr _account;
@@ -151,7 +166,7 @@ protected:
ShareType _shareType;
bool _isPasswordSet;
Permissions _permissions;
- QSharedPointer<Sharee> _shareWith;
+ ShareePtr _shareWith;
protected slots:
void slotOcsError(int statusCode, const QString &message);
@@ -163,6 +178,8 @@ private slots:
void slotPermissionsSet(const QJsonDocument &, const QVariant &value);
};
+using SharePtr = QSharedPointer<Share>;
+
/**
* A Link share is just like a regular share but then slightly different.
* There are several methods in the API that either work differently for
@@ -171,6 +188,16 @@ private slots:
class LinkShare : public Share
{
Q_OBJECT
+ Q_PROPERTY(QUrl link READ getLink CONSTANT)
+ Q_PROPERTY(QUrl directDownloadLink READ getDirectDownloadLink CONSTANT)
+ Q_PROPERTY(bool publicCanUpload READ getPublicUpload CONSTANT)
+ Q_PROPERTY(bool publicCanReadDirectory READ getShowFileListing CONSTANT)
+ Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameSet)
+ Q_PROPERTY(QString note READ getNote WRITE setNote NOTIFY noteSet)
+ Q_PROPERTY(QString label READ getLabel WRITE setLabel NOTIFY labelSet)
+ Q_PROPERTY(QDate expireDate READ getExpireDate WRITE setExpireDate NOTIFY expireDateSet)
+ Q_PROPERTY(QString token READ getToken CONSTANT)
+
public:
explicit LinkShare(AccountPtr account,
const QString &id,
@@ -222,6 +249,23 @@ public:
[[nodiscard]] QString getLabel() const;
/*
+ * Returns the token of the link share.
+ */
+ [[nodiscard]] QString getToken() const;
+
+ /*
+ * Get the expiration date
+ */
+ [[nodiscard]] QDate getExpireDate() const;
+
+ /*
+ * Create OcsShareJob and connect to signal/slots
+ */
+ template <typename LinkShareSlot>
+ OcsShareJob *createShareJob(const LinkShareSlot slotFunction);
+
+public slots:
+ /*
* Set the name of the link share.
*
* Emits either nameSet() or serverError().
@@ -234,35 +278,18 @@ public:
void setNote(const QString &note);
/*
- * Returns the token of the link share.
- */
- [[nodiscard]] QString getToken() const;
-
- /*
- * Get the expiration date
- */
- [[nodiscard]] QDate getExpireDate() const;
-
- /*
* Set the expiration date
*
* On success the expireDateSet signal is emitted
* In case of a server error the serverError signal is emitted.
*/
void setExpireDate(const QDate &expireDate);
-
+
/*
* Set the label of the share link.
*/
void setLabel(const QString &label);
- /*
- * Create OcsShareJob and connect to signal/slots
- */
- template <typename LinkShareSlot>
- OcsShareJob *createShareJob(const LinkShareSlot slotFunction);
-
-
signals:
void expireDateSet();
void noteSet();
@@ -287,6 +314,8 @@ private:
class UserGroupShare : public Share
{
Q_OBJECT
+ Q_PROPERTY(QString note READ getNote WRITE setNote NOTIFY noteSet)
+ Q_PROPERTY(QDate expireDate READ getExpireDate WRITE setExpireDate NOTIFY expireDateSet)
public:
UserGroupShare(AccountPtr account,
const QString &id,
@@ -296,27 +325,26 @@ public:
const ShareType shareType,
bool isPasswordSet,
const Permissions permissions,
- const QSharedPointer<Sharee> shareWith,
+ const ShareePtr shareWith,
const QDate &expireDate,
const QString &note);
- void setNote(const QString &note);
-
[[nodiscard]] QString getNote() const;
-
- void slotNoteSet(const QJsonDocument &, const QVariant &note);
-
- void setExpireDate(const QDate &date);
-
[[nodiscard]] QDate getExpireDate() const;
- void slotExpireDateSet(const QJsonDocument &reply, const QVariant &value);
+public slots:
+ void setNote(const QString &note);
+ void setExpireDate(const QDate &date);
signals:
void noteSet();
void noteSetError();
void expireDateSet();
+private slots:
+ void slotNoteSet(const QJsonDocument &json, const QVariant &note);
+ void slotExpireDateSet(const QJsonDocument &reply, const QVariant &value);
+
private:
QString _note;
QDate _expireDate;
@@ -375,9 +403,9 @@ public:
void fetchShares(const QString &path);
signals:
- void shareCreated(const QSharedPointer<Share> &share);
+ void shareCreated(const SharePtr &share);
void linkShareCreated(const QSharedPointer<LinkShare> &share);
- void sharesFetched(const QList<QSharedPointer<Share>> &shares);
+ void sharesFetched(const QList<SharePtr> &shares);
void serverError(int code, const QString &message);
/** Emitted when creating a link share with password fails.
@@ -396,10 +424,12 @@ private slots:
private:
QSharedPointer<LinkShare> parseLinkShare(const QJsonObject &data);
QSharedPointer<UserGroupShare> parseUserGroupShare(const QJsonObject &data);
- QSharedPointer<Share> parseShare(const QJsonObject &data);
+ SharePtr parseShare(const QJsonObject &data) const;
AccountPtr _account;
};
}
+Q_DECLARE_METATYPE(OCC::SharePtr);
+
#endif // SHAREMANAGER_H
diff --git a/src/gui/sharepermissions.h b/src/gui/sharepermissions.h
index 92ce95766..28028e9dd 100644
--- a/src/gui/sharepermissions.h
+++ b/src/gui/sharepermissions.h
@@ -35,4 +35,6 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(SharePermissions)
} // namespace OCC
+Q_DECLARE_METATYPE(OCC::SharePermission)
+
#endif
diff --git a/src/gui/shareusergroupwidget.cpp b/src/gui/shareusergroupwidget.cpp
deleted file mode 100644
index 024e1266e..000000000
--- a/src/gui/shareusergroupwidget.cpp
+++ /dev/null
@@ -1,1129 +0,0 @@
-/*
- * Copyright (C) by Roeland Jago Douma <roeland@owncloud.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 "ocsprofileconnector.h"
-#include "sharee.h"
-#include "tray/usermodel.h"
-#include "ui_shareusergroupwidget.h"
-#include "ui_shareuserline.h"
-#include "shareusergroupwidget.h"
-#include "account.h"
-#include "folderman.h"
-#include "folder.h"
-#include "accountmanager.h"
-#include "theme.h"
-#include "configfile.h"
-#include "capabilities.h"
-#include "guiutility.h"
-#include "thumbnailjob.h"
-#include "sharemanager.h"
-#include "theme.h"
-#include "iconutils.h"
-
-#include "QProgressIndicator.h"
-#include <QBuffer>
-#include <QFileIconProvider>
-#include <QClipboard>
-#include <QFileInfo>
-#include <QAbstractProxyModel>
-#include <QCompleter>
-#include <QBoxLayout>
-#include <QIcon>
-#include <QLayout>
-#include <QPropertyAnimation>
-#include <QMenu>
-#include <QAction>
-#include <QDesktopServices>
-#include <QInputDialog>
-#include <QMessageBox>
-#include <QCryptographicHash>
-#include <QColor>
-#include <QPainter>
-#include <QListWidget>
-#include <QSvgRenderer>
-#include <QPushButton>
-#include <QContextMenuEvent>
-
-#include <cstring>
-
-namespace {
-const char *passwordIsSetPlaceholder = "●●●●●●●●";
-
-}
-
-namespace OCC {
-
-AvatarEventFilter::AvatarEventFilter(QObject *parent)
- : QObject(parent)
-{
-}
-
-
-bool AvatarEventFilter::eventFilter(QObject *obj, QEvent *event)
-{
- if (event->type() == QEvent::ContextMenu) {
- const auto contextMenuEvent = dynamic_cast<QContextMenuEvent *>(event);
- if (!contextMenuEvent) {
- return false;
- }
- emit contextMenu(contextMenuEvent->globalPos());
- return true;
- }
- return QObject::eventFilter(obj, event);
-}
-
-ShareUserGroupWidget::ShareUserGroupWidget(AccountPtr account,
- const QString &sharePath,
- const QString &localPath,
- SharePermissions maxSharingPermissions,
- const QString &privateLinkUrl,
- QWidget *parent)
- : QWidget(parent)
- , _ui(new Ui::ShareUserGroupWidget)
- , _account(account)
- , _sharePath(sharePath)
- , _localPath(localPath)
- , _maxSharingPermissions(maxSharingPermissions)
- , _privateLinkUrl(privateLinkUrl)
- , _disableCompleterActivated(false)
-{
- setAttribute(Qt::WA_DeleteOnClose);
- setObjectName("SharingDialogUG"); // required as group for saveGeometry call
-
- _ui->setupUi(this);
-
- //Is this a file or folder?
- _isFile = QFileInfo(localPath).isFile();
-
- _completer = new QCompleter(this);
- _completerModel = new ShareeModel(_account,
- _isFile ? QLatin1String("file") : QLatin1String("folder"),
- _completer);
- connect(_completerModel, &ShareeModel::shareesReady, this, &ShareUserGroupWidget::slotShareesReady);
- connect(_completerModel, &ShareeModel::displayErrorMessage, this, &ShareUserGroupWidget::displayError);
-
- _completer->setModel(_completerModel);
- _completer->setCaseSensitivity(Qt::CaseInsensitive);
- _completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
- _ui->shareeLineEdit->setCompleter(_completer);
-
- _searchGloballyAction.reset(new QAction(_ui->shareeLineEdit));
- _searchGloballyAction->setIcon(Theme::createColorAwareIcon(":/client/theme/magnifying-glass.svg"));
- _searchGloballyAction->setToolTip(tr("Search globally"));
-
- connect(_searchGloballyAction.data(), &QAction::triggered, this, [this]() {
- searchForSharees(ShareeModel::GlobalSearch);
- });
-
- _ui->shareeLineEdit->addAction(_searchGloballyAction.data(), QLineEdit::LeadingPosition);
-
- _manager = new ShareManager(_account, this);
- connect(_manager, &ShareManager::sharesFetched, this, &ShareUserGroupWidget::slotSharesFetched);
- connect(_manager, &ShareManager::shareCreated, this, &ShareUserGroupWidget::slotShareCreated);
- connect(_manager, &ShareManager::serverError, this, &ShareUserGroupWidget::displayError);
- connect(_ui->shareeLineEdit, &QLineEdit::returnPressed, this, &ShareUserGroupWidget::slotLineEditReturn);
- connect(_ui->confirmShare, &QAbstractButton::clicked, this, &ShareUserGroupWidget::slotLineEditReturn);
- //TODO connect(_ui->privateLinkText, &QLabel::linkActivated, this, &ShareUserGroupWidget::slotPrivateLinkShare);
-
- // By making the next two QueuedConnections we can override
- // the strings the completer sets on the line edit.
- connect(_completer, SIGNAL(activated(QModelIndex)), SLOT(slotCompleterActivated(QModelIndex)),
- Qt::QueuedConnection);
- connect(_completer, SIGNAL(highlighted(QModelIndex)), SLOT(slotCompleterHighlighted(QModelIndex)),
- Qt::QueuedConnection);
-
- // Queued connection so this signal is recieved after textChanged
- connect(_ui->shareeLineEdit, &QLineEdit::textEdited,
- this, &ShareUserGroupWidget::slotLineEditTextEdited, Qt::QueuedConnection);
- _ui->shareeLineEdit->installEventFilter(this);
- connect(&_completionTimer, &QTimer::timeout, this, [this]() {
- searchForSharees(ShareeModel::LocalSearch);
- });
- _completionTimer.setSingleShot(true);
- _completionTimer.setInterval(600);
-
- _ui->errorLabel->hide();
-
- _parentScrollArea = parentWidget()->findChild<QScrollArea*>("scrollArea");
- _shareUserGroup = new QVBoxLayout(_parentScrollArea);
- _shareUserGroup->setContentsMargins(0, 0, 0, 0);
- customizeStyle();
-}
-
-QVBoxLayout *ShareUserGroupWidget::shareUserGroupLayout()
-{
- return _shareUserGroup;
-}
-
-ShareUserGroupWidget::~ShareUserGroupWidget()
-{
- delete _ui;
-}
-
-void ShareUserGroupWidget::on_shareeLineEdit_textChanged(const QString &)
-{
- _completionTimer.stop();
- emit togglePublicLinkShare(false);
-}
-
-void ShareUserGroupWidget::slotLineEditTextEdited(const QString &text)
-{
- _disableCompleterActivated = false;
- // First textChanged is called first and we stopped the timer when the text is changed, programatically or not
- // Then we restart the timer here if the user touched a key
- if (!text.isEmpty()) {
- _completionTimer.start();
- emit togglePublicLinkShare(true);
- }
-}
-
-void ShareUserGroupWidget::slotLineEditReturn()
-{
- _disableCompleterActivated = false;
- // did the user type in one of the options?
- const auto text = _ui->shareeLineEdit->text();
- for (int i = 0; i < _completerModel->rowCount(); ++i) {
- const auto sharee = _completerModel->getSharee(i);
- if (sharee->format() == text
- || sharee->displayName() == text
- || sharee->shareWith() == text) {
- slotCompleterActivated(_completerModel->index(i));
- // make sure we do not send the same item twice (because return is called when we press
- // return to activate an item inthe completer)
- _disableCompleterActivated = true;
- return;
- }
- }
-
- // nothing found? try to refresh completion
- _completionTimer.start();
-}
-
-void ShareUserGroupWidget::searchForSharees(ShareeModel::LookupMode lookupMode)
-{
- if (_ui->shareeLineEdit->text().isEmpty()) {
- return;
- }
-
- _ui->shareeLineEdit->setEnabled(false);
- _completionTimer.stop();
- _pi_sharee.startAnimation();
- ShareeModel::ShareeSet blacklist;
-
- // Add the current user to _sharees since we can't share with ourself
- QSharedPointer<Sharee> currentUser(new Sharee(_account->credentials()->user(), "", Sharee::Type::User));
- blacklist << currentUser;
-
- foreach (auto sw, _parentScrollArea->findChildren<ShareUserLine *>()) {
- blacklist << sw->share()->getShareWith();
- }
- _ui->errorLabel->hide();
- _completerModel->fetch(_ui->shareeLineEdit->text(), blacklist, lookupMode);
-}
-
-void ShareUserGroupWidget::getShares()
-{
- _manager->fetchShares(_sharePath);
-}
-
-void ShareUserGroupWidget::slotShareCreated(const QSharedPointer<Share> &share)
-{
- if (share && _account->capabilities().shareEmailPasswordEnabled() && !_account->capabilities().shareEmailPasswordEnforced()) {
- // remember this share Id so we can set it's password Line Edit to focus later
- _lastCreatedShareId = share->getId();
- }
- // fetch all shares including the one we've just created
- getShares();
-}
-
-void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>> &shares)
-{
- int x = 0;
- QList<QString> linkOwners({});
-
- ShareUserLine *justCreatedShareThatNeedsPassword = nullptr;
-
- while (QLayoutItem *shareUserLine = _shareUserGroup->takeAt(0)) {
- delete shareUserLine->widget();
- delete shareUserLine;
- }
-
- foreach (const auto &share, shares) {
- // We don't handle link shares, only TypeUser or TypeGroup
- if (share->getShareType() == Share::TypeLink) {
- if(!share->getUidOwner().isEmpty() &&
- share->getUidOwner() != share->account()->davUser()){
- linkOwners.append(share->getOwnerDisplayName());
- }
- continue;
- }
-
- // the owner of the file that shared it first
- // leave out if it's the current user
- if(x == 0 && !share->getUidOwner().isEmpty() && !(share->getUidOwner() == _account->credentials()->user())) {
- _ui->mainOwnerLabel->setText(QString("Shared with you by ").append(share->getOwnerDisplayName()));
- }
-
-
- Q_ASSERT(Share::isShareTypeUserGroupEmailRoomOrRemote(share->getShareType()));
- auto userGroupShare = qSharedPointerDynamicCast<UserGroupShare>(share);
- auto *s = new ShareUserLine(_account, userGroupShare, _maxSharingPermissions, _isFile, _parentScrollArea);
- connect(s, &ShareUserLine::visualDeletionDone, this, &ShareUserGroupWidget::getShares);
- s->setBackgroundRole(_shareUserGroup->count() % 2 == 0 ? QPalette::Base : QPalette::AlternateBase);
-
- // Connect styleChanged events to our widget, so it can adapt (Dark-/Light-Mode switching)
- connect(this, &ShareUserGroupWidget::styleChanged, s, &ShareUserLine::slotStyleChanged);
- _shareUserGroup->addWidget(s);
-
- if (!_lastCreatedShareId.isEmpty() && share->getId() == _lastCreatedShareId) {
- _lastCreatedShareId = QString();
- if (_account->capabilities().shareEmailPasswordEnabled() && !_account->capabilities().shareEmailPasswordEnforced()) {
- justCreatedShareThatNeedsPassword = s;
- }
- }
-
- x++;
- }
-
- foreach (const QString &owner, linkOwners) {
- auto ownerLabel = new QLabel(QString(owner + " shared via link"));
- _shareUserGroup->addWidget(ownerLabel);
- ownerLabel->setVisible(true);
- }
-
- _disableCompleterActivated = false;
- activateShareeLineEdit();
-
- if (justCreatedShareThatNeedsPassword) {
- // always set focus to a password Line Edit when the new email share is created on a server with optional passwords enabled for email shares
- justCreatedShareThatNeedsPassword->focusPasswordLineEdit();
- }
-}
-
-void ShareUserGroupWidget::slotPrivateLinkShare()
-{
- auto menu = new QMenu(this);
- menu->setAttribute(Qt::WA_DeleteOnClose);
-
- // this icon is not handled by slotStyleChanged() -> customizeStyle but we can live with that
- menu->addAction(Theme::createColorAwareIcon(":/client/theme/copy.svg"),
- tr("Copy link"),
- this, SLOT(slotPrivateLinkCopy()));
-
- menu->exec(QCursor::pos());
-}
-
-void ShareUserGroupWidget::slotShareesReady()
-{
- activateShareeLineEdit();
-
- _pi_sharee.stopAnimation();
- if (_completerModel->rowCount() == 0) {
- displayError(0, tr("No results for \"%1\"").arg(_completerModel->currentSearch()));
- }
-
- // if no rows are present in the model - complete() will hide the completer
- _completer->complete();
-}
-
-void ShareUserGroupWidget::slotCompleterActivated(const QModelIndex &index)
-{
- if (_disableCompleterActivated)
- return;
- // The index is an index from the QCompletion model which is itelf a proxy
- // model proxying the _completerModel
- auto sharee = qvariant_cast<QSharedPointer<Sharee>>(index.data(Qt::UserRole));
- if (sharee.isNull()) {
- return;
- }
-
- /*
- * Don't send the reshare permissions for federated shares for servers <9.1
- * https://github.com/owncloud/core/issues/22122#issuecomment-185637344
- * https://github.com/owncloud/client/issues/4996
- */
- _lastCreatedShareId = QString();
-
- QString password;
- if (sharee->type() == Sharee::Email && _account->capabilities().shareEmailPasswordEnforced()) {
- _ui->shareeLineEdit->clear();
- // always show a dialog for password-enforced email shares
- bool ok = false;
-
- do {
- password = QInputDialog::getText(
- this,
- tr("Password for share required"),
- tr("Please enter a password for your email share:"),
- QLineEdit::Password,
- QString(),
- &ok);
- } while (password.isEmpty() && ok);
-
- if (!ok) {
- return;
- }
- }
-
- _manager->createShare(_sharePath, Share::ShareType(sharee->type()),
- sharee->shareWith(), _maxSharingPermissions, password);
-
- _ui->shareeLineEdit->setEnabled(false);
- _ui->shareeLineEdit->clear();
-}
-
-void ShareUserGroupWidget::slotCompleterHighlighted(const QModelIndex &index)
-{
- // By default the completer would set the text to EditRole,
- // override that here.
- _ui->shareeLineEdit->setText(index.data(Qt::DisplayRole).toString());
-}
-
-void ShareUserGroupWidget::displayError(int code, const QString &message)
-{
- _pi_sharee.stopAnimation();
-
- // Also remove the spinner in the widget list, if any
- foreach (auto pi, _parentScrollArea->findChildren<QProgressIndicator *>()) {
- delete pi;
- }
-
- qCWarning(lcSharing) << "Sharing error from server" << code << message;
- _ui->errorLabel->setText(message);
- _ui->errorLabel->show();
- activateShareeLineEdit();
-}
-
-void ShareUserGroupWidget::slotPrivateLinkOpenBrowser()
-{
- Utility::openBrowser(_privateLinkUrl, this);
-}
-
-void ShareUserGroupWidget::slotPrivateLinkCopy()
-{
- QApplication::clipboard()->setText(_privateLinkUrl);
-}
-
-void ShareUserGroupWidget::slotPrivateLinkEmail()
-{
- Utility::openEmailComposer(
- tr("I shared something with you"),
- _privateLinkUrl,
- this);
-}
-
-void ShareUserGroupWidget::slotStyleChanged()
-{
- customizeStyle();
-
- // Notify the other widgets (ShareUserLine in this case, Dark-/Light-Mode switching)
- emit styleChanged();
-}
-
-void ShareUserGroupWidget::customizeStyle()
-{
- _searchGloballyAction->setIcon(Theme::createColorAwareIcon(":/client/theme/magnifying-glass.svg"));
-
- _ui->confirmShare->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
-
- _pi_sharee.setColor(QGuiApplication::palette().color(QPalette::Text));
-
- foreach (auto pi, _parentScrollArea->findChildren<QProgressIndicator *>()) {
- pi->setColor(QGuiApplication::palette().color(QPalette::Text));;
- }
-}
-
-void ShareUserGroupWidget::activateShareeLineEdit()
-{
- _ui->shareeLineEdit->setEnabled(true);
- _ui->shareeLineEdit->setFocus();
-}
-
-ShareUserLine::ShareUserLine(AccountPtr account, QSharedPointer<UserGroupShare> share,
- SharePermissions maxSharingPermissions, bool isFile, QWidget *parent)
- : QWidget(parent)
- , _ui(new Ui::ShareUserLine)
- , _account(account)
- , _share(share)
- , _isFile(isFile)
- , _profilePageMenu(account, share->getShareWith()->shareWith())
-{
- Q_ASSERT(_share);
- _ui->setupUi(this);
-
- _ui->sharedWith->setElideMode(Qt::ElideRight);
- _ui->sharedWith->setText(share->getShareWith()->format());
-
- // adds permissions
- // can edit permission
- bool enabled = (maxSharingPermissions & SharePermissionUpdate);
- if(!_isFile) enabled = enabled && (maxSharingPermissions & SharePermissionCreate &&
- maxSharingPermissions & SharePermissionDelete);
- _ui->permissionsEdit->setEnabled(enabled);
- connect(_ui->permissionsEdit, &QAbstractButton::clicked, this, &ShareUserLine::slotEditPermissionsChanged);
- connect(_ui->noteConfirmButton, &QAbstractButton::clicked, this, &ShareUserLine::onNoteConfirmButtonClicked);
- connect(_ui->calendar, &QDateTimeEdit::dateChanged, this, &ShareUserLine::setExpireDate);
-
- connect(_share.data(), &UserGroupShare::noteSet, this, &ShareUserLine::disableProgessIndicatorAnimation);
- connect(_share.data(), &UserGroupShare::noteSetError, this, &ShareUserLine::disableProgessIndicatorAnimation);
- connect(_share.data(), &UserGroupShare::expireDateSet, this, &ShareUserLine::disableProgessIndicatorAnimation);
-
- connect(_ui->confirmPassword, &QToolButton::clicked, this, &ShareUserLine::slotConfirmPasswordClicked);
- connect(_ui->lineEdit_password, &QLineEdit::returnPressed, this, &ShareUserLine::slotLineEditPasswordReturnPressed);
-
- // create menu with checkable permissions
- auto *menu = new QMenu(this);
- _permissionReshare= new QAction(tr("Can reshare"), this);
- _permissionReshare->setCheckable(true);
- _permissionReshare->setEnabled(maxSharingPermissions & SharePermissionShare);
- menu->addAction(_permissionReshare);
- connect(_permissionReshare, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged);
-
- showNoteOptions(false);
-
- const bool isNoteSupported = _share->getShareType() != Share::ShareType::TypeEmail && _share->getShareType() != Share::ShareType::TypeRoom;
-
- if (isNoteSupported) {
- _noteLinkAction = new QAction(tr("Note to recipient"));
- _noteLinkAction->setCheckable(true);
- menu->addAction(_noteLinkAction);
- connect(_noteLinkAction, &QAction::triggered, this, &ShareUserLine::toggleNoteOptions);
- if (!_share->getNote().isEmpty()) {
- _noteLinkAction->setChecked(true);
- showNoteOptions(true);
- }
- }
-
- showExpireDateOptions(false);
-
- const bool isExpirationDateSupported = _share->getShareType() != Share::ShareType::TypeEmail;
-
- if (isExpirationDateSupported) {
- // email shares do not support expiration dates
- _expirationDateLinkAction = new QAction(tr("Set expiration date"));
- _expirationDateLinkAction->setCheckable(true);
- menu->addAction(_expirationDateLinkAction);
- connect(_expirationDateLinkAction, &QAction::triggered, this, &ShareUserLine::toggleExpireDateOptions);
- const auto expireDate = _share->getExpireDate().isValid() ? share.data()->getExpireDate() : QDate();
- if (!expireDate.isNull()) {
- _expirationDateLinkAction->setChecked(true);
- showExpireDateOptions(true, expireDate);
- }
- }
-
- menu->addSeparator();
-
- // Adds action to delete share widget
- QIcon deleteicon = QIcon::fromTheme(QLatin1String("user-trash"),QIcon(QLatin1String(":/client/theme/delete.svg")));
- _deleteShareButton= new QAction(deleteicon,tr("Unshare"), this);
-
- menu->addAction(_deleteShareButton);
- connect(_deleteShareButton, &QAction::triggered, this, &ShareUserLine::on_deleteShareButton_clicked);
-
- /*
- * Files can't have create or delete permissions
- */
- if (!_isFile) {
- _permissionCreate = new QAction(tr("Can create"), this);
- _permissionCreate->setCheckable(true);
- _permissionCreate->setEnabled(maxSharingPermissions & SharePermissionCreate);
- menu->addAction(_permissionCreate);
- connect(_permissionCreate, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged);
-
- _permissionChange = new QAction(tr("Can change"), this);
- _permissionChange->setCheckable(true);
- _permissionChange->setEnabled(maxSharingPermissions & SharePermissionUpdate);
- menu->addAction(_permissionChange);
- connect(_permissionChange, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged);
-
- _permissionDelete = new QAction(tr("Can delete"), this);
- _permissionDelete->setCheckable(true);
- _permissionDelete->setEnabled(maxSharingPermissions & SharePermissionDelete);
- menu->addAction(_permissionDelete);
- connect(_permissionDelete, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged);
- }
-
- // Adds action to display password widget (check box)
- if (_share->getShareType() == Share::TypeEmail && (_share->isPasswordSet() || _account->capabilities().shareEmailPasswordEnabled())) {
- _passwordProtectLinkAction = new QAction(tr("Password protect"), this);
- _passwordProtectLinkAction->setCheckable(true);
- _passwordProtectLinkAction->setChecked(_share->isPasswordSet());
- // checkbox can be checked/unchedkec if the password is not yet set or if it's not enforced
- _passwordProtectLinkAction->setEnabled(!_share->isPasswordSet() || !_account->capabilities().shareEmailPasswordEnforced());
-
- menu->addAction(_passwordProtectLinkAction);
- connect(_passwordProtectLinkAction, &QAction::triggered, this, &ShareUserLine::slotPasswordCheckboxChanged);
-
- refreshPasswordLineEditPlaceholder();
-
- connect(_share.data(), &Share::passwordSet, this, &ShareUserLine::slotPasswordSet);
- connect(_share.data(), &Share::passwordSetError, this, &ShareUserLine::slotPasswordSetError);
- }
-
- refreshPasswordOptions();
-
- _ui->errorLabel->hide();
-
- _ui->permissionToolButton->setMenu(menu);
- _ui->permissionToolButton->setPopupMode(QToolButton::InstantPopup);
-
- _ui->passwordProgressIndicator->setVisible(false);
-
- // Set the permissions checkboxes
- displayPermissions();
-
- /*
- * We don't show permission share for federated shares with server <9.1
- * https://github.com/owncloud/core/issues/22122#issuecomment-185637344
- * https://github.com/owncloud/client/issues/4996
- */
- if (share->getShareType() == Share::TypeRemote
- && share->account()->serverVersionInt() < Account::makeServerVersion(9, 1, 0)) {
- _permissionReshare->setVisible(false);
- _ui->permissionToolButton->setVisible(false);
- }
-
- connect(share.data(), &Share::permissionsSet, this, &ShareUserLine::slotPermissionsSet);
- connect(share.data(), &Share::shareDeleted, this, &ShareUserLine::slotShareDeleted);
-
- if (!share->account()->capabilities().shareResharing()) {
- _permissionReshare->setVisible(false);
- }
-
- const auto avatarEventFilter = new AvatarEventFilter(_ui->avatar);
- connect(avatarEventFilter, &AvatarEventFilter::contextMenu, this, &ShareUserLine::onAvatarContextMenu);
- _ui->avatar->installEventFilter(avatarEventFilter);
-
- loadAvatar();
-
- customizeStyle();
-}
-
-void ShareUserLine::onAvatarContextMenu(const QPoint &globalPosition)
-{
- if (_share->getShareType() == Share::TypeUser) {
- _profilePageMenu.exec(globalPosition);
- }
-}
-
-void ShareUserLine::loadAvatar()
-{
- const int avatarSize = 36;
-
- // Set size of the placeholder
- _ui->avatar->setMinimumHeight(avatarSize);
- _ui->avatar->setMinimumWidth(avatarSize);
- _ui->avatar->setMaximumHeight(avatarSize);
- _ui->avatar->setMaximumWidth(avatarSize);
- _ui->avatar->setAlignment(Qt::AlignCenter);
-
- setDefaultAvatar(avatarSize);
-
- /* Start the network job to fetch the avatar data.
- *
- * Currently only regular users can have avatars.
- */
- if (_share->getShareWith()->type() == Sharee::User) {
- auto *job = new AvatarJob(_share->account(), _share->getShareWith()->shareWith(), avatarSize, this);
- connect(job, &AvatarJob::avatarPixmap, this, &ShareUserLine::slotAvatarLoaded);
- job->start();
- }
-}
-
-void ShareUserLine::setDefaultAvatar(int avatarSize)
-{
- /* Create the fallback avatar.
- *
- * This will be shown until the avatar image data arrives.
- */
-
- // See core/js/placeholder.js for details on colors and styling
- const auto backgroundColor = backgroundColorForShareeType(_share->getShareWith()->type());
- const QString style = QString(R"(* {
- color: #fff;
- background-color: %1;
- border-radius: %2px;
- text-align: center;
- line-height: %2px;
- font-size: %2px;
- })").arg(backgroundColor.name(), QString::number(avatarSize / 2));
- _ui->avatar->setStyleSheet(style);
-
- const auto pixmap = pixmapForShareeType(_share->getShareWith()->type(), backgroundColor);
-
- if (!pixmap.isNull()) {
- _ui->avatar->setPixmap(pixmap);
- } else {
- qCDebug(lcSharing) << "pixmap is null for share type: " << _share->getShareWith()->type();
-
- // The avatar label is the first character of the user name.
- const auto text = _share->getShareWith()->displayName();
- _ui->avatar->setText(text.at(0).toUpper());
- }
-}
-
-void ShareUserLine::slotAvatarLoaded(QImage avatar)
-{
- if (avatar.isNull())
- return;
-
- avatar = AvatarJob::makeCircularAvatar(avatar);
- _ui->avatar->setPixmap(QPixmap::fromImage(avatar));
-
- // Remove the stylesheet for the fallback avatar
- _ui->avatar->setStyleSheet("");
-}
-
-void ShareUserLine::on_deleteShareButton_clicked()
-{
- setEnabled(false);
- _share->deleteShare();
-}
-
-ShareUserLine::~ShareUserLine()
-{
- delete _ui;
-}
-
-void ShareUserLine::slotEditPermissionsChanged()
-{
- setEnabled(false);
-
- // Can never manually be set to "partial".
- // This works because the state cycle for clicking is
- // unchecked -> partial -> checked -> unchecked.
- if (_ui->permissionsEdit->checkState() == Qt::PartiallyChecked) {
- _ui->permissionsEdit->setCheckState(Qt::Checked);
- }
-
- Share::Permissions permissions = SharePermissionRead;
-
- // folders edit = CREATE, READ, UPDATE, DELETE
- // files edit = READ + UPDATE
- if (_ui->permissionsEdit->checkState() == Qt::Checked) {
-
- /*
- * Files can't have create or delete permisisons
- */
- if (!_isFile) {
- if (_permissionChange->isEnabled())
- permissions |= SharePermissionUpdate;
- if (_permissionCreate->isEnabled())
- permissions |= SharePermissionCreate;
- if (_permissionDelete->isEnabled())
- permissions |= SharePermissionDelete;
- } else {
- permissions |= SharePermissionUpdate;
- }
- }
-
- if(_isFile && _permissionReshare->isEnabled() && _permissionReshare->isChecked())
- permissions |= SharePermissionShare;
-
- _share->setPermissions(permissions);
-}
-
-void ShareUserLine::slotPermissionsChanged()
-{
- setEnabled(false);
-
- Share::Permissions permissions = SharePermissionRead;
-
- if (_permissionReshare->isChecked())
- permissions |= SharePermissionShare;
-
- if (!_isFile) {
- if (_permissionChange->isChecked())
- permissions |= SharePermissionUpdate;
- if (_permissionCreate->isChecked())
- permissions |= SharePermissionCreate;
- if (_permissionDelete->isChecked())
- permissions |= SharePermissionDelete;
- } else {
- if (_ui->permissionsEdit->isChecked())
- permissions |= SharePermissionUpdate;
- }
-
- _share->setPermissions(permissions);
-}
-
-void ShareUserLine::slotPasswordCheckboxChanged()
-{
- if (!_passwordProtectLinkAction->isChecked()) {
- _ui->errorLabel->hide();
- _ui->errorLabel->clear();
-
- if (!_share->isPasswordSet()) {
- _ui->lineEdit_password->clear();
- refreshPasswordOptions();
- } else {
- // do not call refreshPasswordOptions here, as it will be called after the network request is complete
- togglePasswordSetProgressAnimation(true);
- _share->setPassword(QString());
- }
- } else {
- refreshPasswordOptions();
-
- if (_ui->lineEdit_password->isVisible() && _ui->lineEdit_password->isEnabled()) {
- focusPasswordLineEdit();
- }
- }
-}
-
-void ShareUserLine::slotDeleteAnimationFinished()
-{
- emit resizeRequested();
- emit visualDeletionDone();
- deleteLater();
-
- // There is a painting bug where a small line of this widget isn't
- // properly cleared. This explicit repaint() call makes sure any trace of
- // the share widget is removed once it's destroyed. #4189
- connect(this, SIGNAL(destroyed(QObject *)), parentWidget(), SLOT(repaint()));
-}
-
-void ShareUserLine::refreshPasswordOptions()
-{
- const bool isPasswordEnabled = _share->getShareType() == Share::TypeEmail && _passwordProtectLinkAction->isChecked();
-
- _ui->passwordLabel->setVisible(isPasswordEnabled);
- _ui->lineEdit_password->setEnabled(isPasswordEnabled);
- _ui->lineEdit_password->setVisible(isPasswordEnabled);
- _ui->confirmPassword->setVisible(isPasswordEnabled);
-
- emit resizeRequested();
-}
-
-void ShareUserLine::refreshPasswordLineEditPlaceholder()
-{
- if (_share->isPasswordSet()) {
- _ui->lineEdit_password->setPlaceholderText(QString::fromUtf8(passwordIsSetPlaceholder));
- } else {
- _ui->lineEdit_password->setPlaceholderText("");
- }
-}
-
-void ShareUserLine::slotPasswordSet()
-{
- togglePasswordSetProgressAnimation(false);
- _ui->lineEdit_password->setEnabled(true);
- _ui->confirmPassword->setEnabled(true);
-
- _ui->lineEdit_password->setText("");
-
- _passwordProtectLinkAction->setEnabled(!_share->isPasswordSet() || !_account->capabilities().shareEmailPasswordEnforced());
-
- refreshPasswordLineEditPlaceholder();
-
- refreshPasswordOptions();
-}
-
-void ShareUserLine::slotPasswordSetError(int statusCode, const QString &message)
-{
- qCWarning(lcSharing) << "Error from server" << statusCode << message;
-
- togglePasswordSetProgressAnimation(false);
-
- _ui->lineEdit_password->setEnabled(true);
- _ui->confirmPassword->setEnabled(true);
-
- refreshPasswordLineEditPlaceholder();
-
- refreshPasswordOptions();
-
- focusPasswordLineEdit();
-
- _ui->errorLabel->show();
- _ui->errorLabel->setText(message);
-
- emit resizeRequested();
-}
-
-void ShareUserLine::slotShareDeleted()
-{
- auto *animation = new QPropertyAnimation(this, "maximumHeight", this);
-
- animation->setDuration(500);
- animation->setStartValue(height());
- animation->setEndValue(0);
-
- connect(animation, &QAbstractAnimation::finished, this, &ShareUserLine::slotDeleteAnimationFinished);
- connect(animation, &QVariantAnimation::valueChanged, this, &ShareUserLine::resizeRequested);
-
- animation->start();
-}
-
-void ShareUserLine::slotPermissionsSet()
-{
- displayPermissions();
- setEnabled(true);
-}
-
-QSharedPointer<Share> ShareUserLine::share() const
-{
- return _share;
-}
-
-void ShareUserLine::displayPermissions()
-{
- auto perm = _share->getPermissions();
-
-// folders edit = CREATE, READ, UPDATE, DELETE
-// files edit = READ + UPDATE
- if (perm & SharePermissionUpdate && (_isFile ||
- (perm & SharePermissionCreate && perm & SharePermissionDelete))) {
- _ui->permissionsEdit->setCheckState(Qt::Checked);
- } else if (!_isFile && perm & (SharePermissionUpdate | SharePermissionCreate | SharePermissionDelete)) {
- _ui->permissionsEdit->setCheckState(Qt::PartiallyChecked);
- } else if(perm & SharePermissionRead) {
- _ui->permissionsEdit->setCheckState(Qt::Unchecked);
- }
-
-// edit is independent of reshare
- if (perm & SharePermissionShare)
- _permissionReshare->setChecked(true);
-
- if(!_isFile){
- _permissionCreate->setChecked(perm & SharePermissionCreate);
- _permissionChange->setChecked(perm & SharePermissionUpdate);
- _permissionDelete->setChecked(perm & SharePermissionDelete);
- }
-}
-
-void ShareUserLine::slotStyleChanged()
-{
- customizeStyle();
-}
-
-void ShareUserLine::focusPasswordLineEdit()
-{
- _ui->lineEdit_password->setFocus();
-}
-
-void ShareUserLine::customizeStyle()
-{
- _ui->permissionToolButton->setIcon(Theme::createColorAwareIcon(":/client/theme/more.svg"));
-
- QIcon deleteicon = QIcon::fromTheme(QLatin1String("user-trash"),Theme::createColorAwareIcon(QLatin1String(":/client/theme/delete.svg")));
- _deleteShareButton->setIcon(deleteicon);
-
- _ui->noteConfirmButton->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
- _ui->progressIndicator->setColor(QGuiApplication::palette().color(QPalette::WindowText));
-
- // make sure to force BackgroundRole to QPalette::WindowText for a lable, because it's parent always has a different role set that applies to children unless customized
- _ui->errorLabel->setBackgroundRole(QPalette::WindowText);
-}
-
-QPixmap ShareUserLine::pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor) const
-{
- switch (type) {
- case Sharee::Room:
- return Ui::IconUtils::pixmapForBackground(QStringLiteral("talk-app.svg"), backgroundColor);
- case Sharee::Email:
- return Ui::IconUtils::pixmapForBackground(QStringLiteral("email.svg"), backgroundColor);
- case Sharee::Group:
- case Sharee::Federated:
- case Sharee::Circle:
- case Sharee::User:
- break;
- }
-
- return {};
-}
-
-QColor ShareUserLine::backgroundColorForShareeType(Sharee::Type type) const
-{
- switch (type) {
- case Sharee::Room:
- return Theme::instance()->wizardHeaderBackgroundColor();
- case Sharee::Email:
- return Theme::instance()->wizardHeaderTitleColor();
- case Sharee::Group:
- case Sharee::Federated:
- case Sharee::Circle:
- case Sharee::User:
- break;
- }
-
- const auto calculateBackgroundBasedOnText = [this]() {
- const auto hash = QCryptographicHash::hash(_ui->sharedWith->text().toUtf8(), QCryptographicHash::Md5);
- Q_ASSERT(hash.size() > 0);
- if (hash.size() == 0) {
- qCWarning(lcSharing) << "Failed to calculate hash color for share:" << _share->path();
- return QColor{};
- }
- const double hue = static_cast<quint8>(hash[0]) / 255.;
- return QColor::fromHslF(hue, 0.7, 0.68);
- };
-
- return calculateBackgroundBasedOnText();
-}
-
-void ShareUserLine::showNoteOptions(bool show)
-{
- _ui->noteLabel->setVisible(show);
- _ui->noteTextEdit->setVisible(show);
- _ui->noteConfirmButton->setVisible(show);
-
- if (show) {
- const auto note = _share->getNote();
- _ui->noteTextEdit->setText(note);
- _ui->noteTextEdit->setFocus();
- }
-
- emit resizeRequested();
-}
-
-
-void ShareUserLine::toggleNoteOptions(bool enable)
-{
- showNoteOptions(enable);
-
- if (!enable) {
- // Delete note
- _share->setNote(QString());
- }
-}
-
-void ShareUserLine::onNoteConfirmButtonClicked()
-{
- setNote(_ui->noteTextEdit->toPlainText());
-}
-
-void ShareUserLine::setNote(const QString &note)
-{
- enableProgessIndicatorAnimation(true);
- _share->setNote(note);
-}
-
-void ShareUserLine::toggleExpireDateOptions(bool enable)
-{
- showExpireDateOptions(enable);
-
- if (!enable) {
- _share->setExpireDate(QDate());
- }
-}
-
-void ShareUserLine::showExpireDateOptions(bool show, const QDate &initialDate)
-{
- _ui->expirationLabel->setVisible(show);
- _ui->calendar->setVisible(show);
-
- if (show) {
- _ui->calendar->setMinimumDate(QDate::currentDate().addDays(1));
- _ui->calendar->setDate(initialDate.isValid() ? initialDate : _ui->calendar->minimumDate());
- _ui->calendar->setFocus();
-
- if (enforceExpirationDateForShare(_share->getShareType())) {
- _ui->calendar->setMaximumDate(maxExpirationDateForShare(_share->getShareType(), _ui->calendar->maximumDate()));
- _expirationDateLinkAction->setChecked(true);
- _expirationDateLinkAction->setEnabled(false);
- }
- }
-
- emit resizeRequested();
-}
-
-void ShareUserLine::setExpireDate()
-{
- enableProgessIndicatorAnimation(true);
- _share->setExpireDate(_ui->calendar->date());
-}
-
-void ShareUserLine::enableProgessIndicatorAnimation(bool enable)
-{
- if (enable) {
- if (!_ui->progressIndicator->isAnimated()) {
- _ui->progressIndicator->startAnimation();
- }
- } else {
- _ui->progressIndicator->stopAnimation();
- }
-}
-
-void ShareUserLine::togglePasswordSetProgressAnimation(bool show)
-{
- // button and progress indicator are interchanged depending on if the network request is in progress or not
- _ui->confirmPassword->setVisible(!show && _passwordProtectLinkAction->isChecked());
- _ui->passwordProgressIndicator->setVisible(show);
- if (show) {
- if (!_ui->passwordProgressIndicator->isAnimated()) {
- _ui->passwordProgressIndicator->startAnimation();
- }
- } else {
- _ui->passwordProgressIndicator->stopAnimation();
- }
-}
-
-void ShareUserLine::disableProgessIndicatorAnimation()
-{
- enableProgessIndicatorAnimation(false);
-}
-
-QDate ShareUserLine::maxExpirationDateForShare(const Share::ShareType type, const QDate &fallbackDate) const
-{
- auto daysToExpire = 0;
- if (type == Share::ShareType::TypeRemote) {
- daysToExpire = _account->capabilities().shareRemoteExpireDateDays();
- } else if (type == Share::ShareType::TypeEmail) {
- daysToExpire = _account->capabilities().sharePublicLinkExpireDateDays();
- } else {
- daysToExpire = _account->capabilities().shareInternalExpireDateDays();
- }
-
- if (daysToExpire > 0) {
- return QDate::currentDate().addDays(daysToExpire);
- }
-
- return fallbackDate;
-}
-
-bool ShareUserLine::enforceExpirationDateForShare(const Share::ShareType type) const
-{
- if (type == Share::ShareType::TypeRemote) {
- return _account->capabilities().shareRemoteEnforceExpireDate();
- } else if (type == Share::ShareType::TypeEmail) {
- return _account->capabilities().sharePublicLinkEnforceExpireDate();
- }
-
- return _account->capabilities().shareInternalEnforceExpireDate();
-}
-
-void ShareUserLine::setPasswordConfirmed()
-{
- if (_ui->lineEdit_password->text().isEmpty()) {
- return;
- }
-
- _ui->lineEdit_password->setEnabled(false);
- _ui->confirmPassword->setEnabled(false);
-
- _ui->errorLabel->hide();
- _ui->errorLabel->clear();
-
- togglePasswordSetProgressAnimation(true);
- _share->setPassword(_ui->lineEdit_password->text());
-}
-
-void ShareUserLine::slotLineEditPasswordReturnPressed()
-{
- setPasswordConfirmed();
-}
-
-void ShareUserLine::slotConfirmPasswordClicked()
-{
- setPasswordConfirmed();
-}
-}
diff --git a/src/gui/shareusergroupwidget.h b/src/gui/shareusergroupwidget.h
deleted file mode 100644
index 96132005e..000000000
--- a/src/gui/shareusergroupwidget.h
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright (C) by Roeland Jago Douma <roeland@owncloud.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 SHAREUSERGROUPWIDGET_H
-#define SHAREUSERGROUPWIDGET_H
-
-#include "accountfwd.h"
-#include "sharemanager.h"
-#include "sharepermissions.h"
-#include "sharee.h"
-#include "profilepagewidget.h"
-#include "QProgressIndicator.h"
-#include <QDialog>
-#include <QWidget>
-#include <QSharedPointer>
-#include <QList>
-#include <QVector>
-#include <QTimer>
-#include <qpushbutton.h>
-#include <qscrollarea.h>
-
-class QAction;
-class QCompleter;
-class QModelIndex;
-
-namespace OCC {
-
-namespace Ui {
- class ShareUserGroupWidget;
- class ShareUserLine;
-}
-
-class AbstractCredentials;
-class SyncResult;
-class Share;
-class ShareManager;
-
-class AvatarEventFilter : public QObject
-{
- Q_OBJECT
-
-public:
- explicit AvatarEventFilter(QObject *parent = nullptr);
-
-signals:
- void clicked();
- void contextMenu(const QPoint &globalPosition);
-
-protected:
- bool eventFilter(QObject *obj, QEvent *event) override;
-};
-
-/**
- * @brief The ShareDialog (user/group) class
- * @ingroup gui
- */
-class ShareUserGroupWidget : public QWidget
-{
- Q_OBJECT
-
-public:
- explicit ShareUserGroupWidget(AccountPtr account,
- const QString &sharePath,
- const QString &localPath,
- SharePermissions maxSharingPermissions,
- const QString &privateLinkUrl,
- QWidget *parent = nullptr);
- ~ShareUserGroupWidget() override;
-
- QVBoxLayout *shareUserGroupLayout();
-
-signals:
- void togglePublicLinkShare(bool);
- void styleChanged();
-
-public slots:
- void getShares();
- void slotShareCreated(const QSharedPointer<Share> &share);
- void slotStyleChanged();
-
-private slots:
- void slotSharesFetched(const QList<QSharedPointer<Share>> &shares);
-
- void on_shareeLineEdit_textChanged(const QString &text);
- void searchForSharees(ShareeModel::LookupMode lookupMode);
- void slotLineEditTextEdited(const QString &text);
-
- void slotLineEditReturn();
- void slotCompleterActivated(const QModelIndex &index);
- void slotCompleterHighlighted(const QModelIndex &index);
- void slotShareesReady();
- void slotPrivateLinkShare();
- void displayError(int code, const QString &message);
-
- void slotPrivateLinkOpenBrowser();
- void slotPrivateLinkCopy();
- void slotPrivateLinkEmail();
-
-private:
- void customizeStyle();
-
- void activateShareeLineEdit();
-
- Ui::ShareUserGroupWidget *_ui;
- QScopedPointer<QAction> _searchGloballyAction;
- QScrollArea *_parentScrollArea;
- QVBoxLayout *_shareUserGroup;
- AccountPtr _account;
- QString _sharePath;
- QString _localPath;
- SharePermissions _maxSharingPermissions;
- QString _privateLinkUrl;
-
- QCompleter *_completer;
- ShareeModel *_completerModel;
- QTimer _completionTimer;
-
- bool _isFile;
- bool _disableCompleterActivated; // in order to avoid that we share the contents twice
- ShareManager *_manager;
-
- QProgressIndicator _pi_sharee;
-
- QString _lastCreatedShareId;
-};
-
-/**
- * The widget displayed for each user/group share
- */
-class ShareUserLine : public QWidget
-{
- Q_OBJECT
-
-public:
- explicit ShareUserLine(AccountPtr account,
- QSharedPointer<UserGroupShare> Share,
- SharePermissions maxSharingPermissions,
- bool isFile,
- QWidget *parent = nullptr);
- ~ShareUserLine() override;
-
- [[nodiscard]] QSharedPointer<Share> share() const;
-
-signals:
- void visualDeletionDone();
- void resizeRequested();
-
-public slots:
- void slotStyleChanged();
-
- void focusPasswordLineEdit();
-
-private slots:
- void on_deleteShareButton_clicked();
- void slotPermissionsChanged();
- void slotEditPermissionsChanged();
- void slotPasswordCheckboxChanged();
- void slotDeleteAnimationFinished();
-
- void refreshPasswordOptions();
-
- void refreshPasswordLineEditPlaceholder();
-
- void slotPasswordSet();
- void slotPasswordSetError(int statusCode, const QString &message);
-
- void slotShareDeleted();
- void slotPermissionsSet();
-
- void slotAvatarLoaded(QImage avatar);
-
- void setPasswordConfirmed();
-
- void slotLineEditPasswordReturnPressed();
-
- void slotConfirmPasswordClicked();
-
- void onAvatarContextMenu(const QPoint &globalPosition);
-
-private:
- void displayPermissions();
- void loadAvatar();
- void setDefaultAvatar(int avatarSize);
- void customizeStyle();
-
- [[nodiscard]] QPixmap pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor = QColor()) const;
- [[nodiscard]] QColor backgroundColorForShareeType(Sharee::Type type) const;
-
- void showNoteOptions(bool show);
- void toggleNoteOptions(bool enable);
- void onNoteConfirmButtonClicked();
- void setNote(const QString &note);
-
- void toggleExpireDateOptions(bool enable);
- void showExpireDateOptions(bool show, const QDate &initialDate = QDate());
- void setExpireDate();
-
- void togglePasswordSetProgressAnimation(bool show);
-
- void enableProgessIndicatorAnimation(bool enable);
- void disableProgessIndicatorAnimation();
-
- [[nodiscard]] QDate maxExpirationDateForShare(const Share::ShareType type, const QDate &fallbackDate) const;
- [[nodiscard]] bool enforceExpirationDateForShare(const Share::ShareType type) const;
-
- Ui::ShareUserLine *_ui;
- AccountPtr _account;
- QSharedPointer<UserGroupShare> _share;
- bool _isFile;
-
- ProfilePageMenu _profilePageMenu;
-
- // _permissionEdit is a checkbox
- QAction *_permissionReshare;
- QAction *_deleteShareButton;
- QAction *_permissionCreate;
- QAction *_permissionChange;
- QAction *_permissionDelete;
- QAction *_noteLinkAction;
- QAction *_expirationDateLinkAction;
- QAction *_passwordProtectLinkAction;
-};
-}
-
-#endif // SHAREUSERGROUPWIDGET_H
diff --git a/src/gui/shareusergroupwidget.ui b/src/gui/shareusergroupwidget.ui
deleted file mode 100644
index 38c45a23d..000000000
--- a/src/gui/shareusergroupwidget.ui
+++ /dev/null
@@ -1,154 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>OCC::ShareUserGroupWidget</class>
- <widget class="QWidget" name="OCC::ShareUserGroupWidget">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>350</width>
- <height>106</height>
- </rect>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="spacing">
- <number>6</number>
- </property>
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <widget class="QLabel" name="mainOwnerLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="shareeHorizontalLayout" stretch="0,0">
- <property name="spacing">
- <number>6</number>
- </property>
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QLineEdit" name="shareeLineEdit">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="placeholderText">
- <string>Share with users or groups …</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="confirmShare">
- <property name="icon">
- <iconset resource="../../theme.qrc">
- <normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
- </property>
- <property name="autoRaise">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QLabel" name="errorLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="palette">
- <palette>
- <active>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </active>
- <inactive>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </inactive>
- <disabled>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>123</red>
- <green>121</green>
- <blue>134</blue>
- </color>
- </brush>
- </colorrole>
- </disabled>
- </palette>
- </property>
- <property name="text">
- <string notr="true">Placeholder for Error text</string>
- </property>
- <property name="textFormat">
- <enum>Qt::PlainText</enum>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <layoutdefault spacing="6" margin="11"/>
- <resources>
- <include location="../../theme.qrc"/>
- </resources>
- <connections/>
-</ui>
diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp
index 4de3317a2..e023dd375 100644
--- a/src/gui/socketapi/socketapi.cpp
+++ b/src/gui/socketapi/socketapi.cpp
@@ -497,10 +497,10 @@ void SocketApi::broadcastMessage(const QString &msg, bool doWait)
void SocketApi::processFileActivityRequest(const QString &localFile)
{
const auto fileData = FileData::get(localFile);
- emit fileActivityCommandReceived(fileData.serverRelativePath, fileData.journalRecord().numericFileId().toInt());
+ emit fileActivityCommandReceived(fileData.localPath);
}
-void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage)
+void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener)
{
auto theme = Theme::instance();
@@ -537,7 +537,7 @@ void SocketApi::processShareRequest(const QString &localFile, SocketListener *li
const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile);
listener->sendMessage(message);
- emit shareCommandReceived(remotePath, fileData.localPath, startPage);
+ emit shareCommandReceived(fileData.localPath);
}
}
@@ -581,7 +581,7 @@ void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketList
void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener)
{
- processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups);
+ processShareRequest(localFile, listener);
}
void SocketApi::command_ACTIVITY(const QString &localFile, SocketListener *listener)
@@ -593,7 +593,7 @@ void SocketApi::command_ACTIVITY(const QString &localFile, SocketListener *liste
void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener)
{
- processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks);
+ processShareRequest(localFile, listener);
}
void SocketApi::command_VERSION(const QString &, SocketListener *listener)
@@ -673,7 +673,7 @@ public:
}
private slots:
- void sharesFetched(const QList<QSharedPointer<Share>> &shares)
+ void sharesFetched(const QList<SharePtr> &shares)
{
auto shareName = SocketApi::tr("Context menu share");
@@ -783,7 +783,7 @@ void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListene
connect(job, &GetOrCreatePublicLinkShare::done, this,
[](const QString &url) { copyUrlToClipboard(url); });
connect(job, &GetOrCreatePublicLinkShare::error, this,
- [=]() { emit shareCommandReceived(fileData.serverRelativePath, fileData.localPath, ShareDialogStartPage::PublicLinks); });
+ [=]() { emit shareCommandReceived(fileData.localPath); });
job->run();
}
diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h
index f3529f870..5f16a00fc 100644
--- a/src/gui/socketapi/socketapi.h
+++ b/src/gui/socketapi/socketapi.h
@@ -17,7 +17,6 @@
#include "syncfileitem.h"
#include "common/syncfilestatus.h"
-#include "sharedialog.h" // for the ShareDialogStartPage
#include "common/syncjournalfilerecord.h"
#include "config.h"
@@ -63,8 +62,8 @@ public slots:
void broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus);
signals:
- void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage);
- void fileActivityCommandReceived(const QString &objectName, const int objectId);
+ void shareCommandReceived(const QString &localPath);
+ void fileActivityCommandReceived(const QString &localPath);
private slots:
void slotNewConnection();
@@ -102,7 +101,7 @@ private:
void broadcastMessage(const QString &msg, bool doWait = false);
// opens share dialog, sends reply
- void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage);
+ void processShareRequest(const QString &localFile, SocketListener *listener);
void processFileActivityRequest(const QString &localFile);
Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener);
diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp
index e52f62bc8..d524b0dba 100644
--- a/src/gui/systray.cpp
+++ b/src/gui/systray.cpp
@@ -285,6 +285,103 @@ void Systray::destroyEditFileLocallyLoadingDialog()
_editFileLocallyLoadingDialog = nullptr;
}
+bool Systray::raiseDialogs()
+{
+ return raiseFileDetailDialogs();
+}
+
+bool Systray::raiseFileDetailDialogs(const QString &localPath)
+{
+ if(_fileDetailDialogs.empty()) {
+ return false;
+ }
+
+ auto it = _fileDetailDialogs.begin();
+ while (it != _fileDetailDialogs.end()) {
+ const auto dialog = *it;
+ auto nullDialog = dialog == nullptr;
+
+ if (!nullDialog && !dialog->isVisible()) {
+ destroyDialog(dialog);
+ nullDialog = true;
+ }
+
+ if (!nullDialog && (localPath.isEmpty() || dialog->property("localPath").toString() == localPath)) {
+ dialog->show();
+ dialog->raise();
+ dialog->requestActivate();
+
+ ++it;
+ continue;
+ }
+
+ it = _fileDetailDialogs.erase(it);
+ continue;
+ }
+
+ // If it is empty then we have raised no dialogs, so return false (and viceversa)
+ return !_fileDetailDialogs.empty();
+}
+
+void Systray::createFileDetailsDialog(const QString &localPath)
+{
+ if (raiseFileDetailDialogs(localPath)) {
+ qCDebug(lcSystray) << "Reopening an existing file details dialog for " << localPath;
+ return;
+ }
+
+ qCDebug(lcSystray) << "Opening new file details dialog for " << localPath;
+
+ if (!_trayEngine) {
+ qCWarning(lcSystray) << "Could not open file details dialog for" << localPath << "as no tray engine was available";
+ return;
+ }
+
+ const auto folder = FolderMan::instance()->folderForPath(localPath);
+ if (!folder) {
+ qCWarning(lcSystray) << "Could not open file details dialog for" << localPath << "no responsible folder found";
+ return;
+ }
+
+ const QVariantMap initialProperties{
+ {"accountState", QVariant::fromValue(folder->accountState())},
+ {"localPath", localPath},
+ };
+
+ QQmlComponent fileDetailsDialog(_trayEngine, QStringLiteral("qrc:/qml/src/gui/filedetails/FileDetailsWindow.qml"));
+
+ if (!fileDetailsDialog.isError()) {
+ const auto createdDialog = fileDetailsDialog.createWithInitialProperties(initialProperties);
+ const auto dialog = qobject_cast<QQuickWindow*>(createdDialog);
+
+ if(!dialog) {
+ qCWarning(lcSystray) << "File details dialog window resulted in creation of object that was not a window!";
+ return;
+ }
+
+ _fileDetailDialogs.append(dialog);
+
+ dialog->show();
+ dialog->raise();
+ dialog->requestActivate();
+
+ } else {
+ qCWarning(lcSystray) << fileDetailsDialog.errorString();
+ }
+}
+
+void Systray::createShareDialog(const QString &localPath)
+{
+ createFileDetailsDialog(localPath);
+ Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Sharing);
+}
+
+void Systray::createFileActivityDialog(const QString &localPath)
+{
+ createFileDetailsDialog(localPath);
+ Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Activity);
+}
+
void Systray::slotCurrentUserChanged()
{
if (_trayEngine) {
diff --git a/src/gui/systray.h b/src/gui/systray.h
index 7350011fc..d28b66290 100644
--- a/src/gui/systray.h
+++ b/src/gui/systray.h
@@ -82,12 +82,17 @@ public:
enum class WindowPosition { Default, Center };
Q_ENUM(WindowPosition);
+ enum class FileDetailsPage { Activity, Sharing };
+ Q_ENUM(FileDetailsPage);
+
Q_REQUIRED_RESULT QString windowTitle() const;
Q_REQUIRED_RESULT bool useNormalWindow() const;
Q_REQUIRED_RESULT bool syncIsPaused() const;
Q_REQUIRED_RESULT bool isOpen() const;
+ bool raiseDialogs();
+
signals:
void currentUserChanged();
void openAccountWizard();
@@ -95,8 +100,7 @@ signals:
void openHelp();
void shutdown();
- void openShareDialog(const QString &sharePath, const QString &localPath);
- void showFileActivityDialog(const QString &objectName, const int objectId);
+ void showFileDetailsPage(const QString &fileLocalPath, const FileDetailsPage page);
void sendChatMessage(const QString &token, const QString &message, const QString &replyTo);
void showErrorMessageDialog(const QString &error);
@@ -132,17 +136,23 @@ public slots:
void setSyncIsPaused(const bool syncIsPaused);
void setIsOpen(const bool isOpen);
+ void createShareDialog(const QString &localPath);
+ void createFileActivityDialog(const QString &localPath);
+
private slots:
void slotUnpauseAllFolders();
void slotPauseAllFolders();
private:
+ // Argument allows user to specify a specific dialog to be raised
+ bool raiseFileDetailDialogs(const QString &localPath = {});
void setPauseOnAllFoldersHelper(bool pause);
static Systray *_instance;
Systray();
void setupContextMenu();
+ void createFileDetailsDialog(const QString &localPath);
[[nodiscard]] QScreen *currentScreen() const;
[[nodiscard]] QRect currentScreenRect() const;
@@ -164,8 +174,8 @@ private:
AccessManagerFactory _accessManagerFactory;
QSet<qlonglong> _callsAlreadyNotified;
-
QPointer<QObject> _editFileLocallyLoadingDialog;
+ QVector<QQuickWindow*> _fileDetailDialogs;
};
} // namespace OCC
diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml
index 020555ed3..eb06c7213 100644
--- a/src/gui/tray/ActivityItem.qml
+++ b/src/gui/tray/ActivityItem.qml
@@ -10,6 +10,8 @@ ItemDelegate {
property Flickable flickable
+ property int iconSize: Style.trayListItemIconSize
+
property bool isFileActivityList: false
readonly property bool isChatActivity: model.objectType === "chat" || model.objectType === "room" || model.objectType === "call"
@@ -45,9 +47,11 @@ ItemDelegate {
showDismissButton: model.links.length > 0
+ iconSize: root.iconSize
+
activityData: model
- onShareButtonClicked: Systray.openShareDialog(model.displayPath, model.path)
+ onShareButtonClicked: Systray.createShareDialog(model.openablePath)
onDismissButtonClicked: activityModel.slotTriggerDismiss(model.activityIndex)
}
diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml
index 51c653312..80bc931fc 100644
--- a/src/gui/tray/ActivityItemContent.qml
+++ b/src/gui/tray/ActivityItemContent.qml
@@ -17,6 +17,8 @@ RowLayout {
property bool childHovered: shareButton.hovered || dismissActionButton.hovered
+ property int iconSize: Style.trayListItemIconSize
+
signal dismissButtonClicked()
signal shareButtonClicked()
@@ -25,8 +27,8 @@ RowLayout {
Item {
id: thumbnailItem
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
- Layout.preferredWidth: Style.trayListItemIconSize
- Layout.preferredHeight: model.thumbnail && model.thumbnail.isMimeTypeIcon ? Style.trayListItemIconSize * 0.9 : Style.trayListItemIconSize
+ Layout.preferredWidth: root.iconSize
+ Layout.preferredHeight: model.thumbnail && model.thumbnail.isMimeTypeIcon ? root.iconSize * 0.9 : root.iconSize
readonly property int imageWidth: width * (1 - Style.thumbnailImageSizeReduction)
readonly property int imageHeight: height * (1 - Style.thumbnailImageSizeReduction)
readonly property int thumbnailRadius: model.thumbnail && model.thumbnail.isUserAvatar ? width / 2 : 3
diff --git a/src/gui/tray/ActivityList.qml b/src/gui/tray/ActivityList.qml
index b108f3cca..18e15e663 100644
--- a/src/gui/tray/ActivityList.qml
+++ b/src/gui/tray/ActivityList.qml
@@ -1,6 +1,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
+import Style 1.0
import com.nextcloud.desktopclient 1.0 as NC
import Style 1.0
@@ -9,6 +10,8 @@ ScrollView {
property alias model: sortedActivityList.activityListModel
property bool isFileActivityList: false
+ property int iconSize: Style.trayListItemIconSize
+ property int delegateHorizontalPadding: 0
signal openFile(string filePath)
signal activityItemClicked(int index)
@@ -36,8 +39,9 @@ ScrollView {
highlight: Rectangle {
id: activityHover
- width: activityList.currentItem.width
- height: activityList.currentItem.height
+
+ anchors.fill: activityList.currentItem
+
color: Style.lightHover
visible: activityList.activeFocus
}
@@ -54,8 +58,13 @@ ScrollView {
}
delegate: ActivityItem {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: controlRoot.delegateHorizontalPadding
+ anchors.rightMargin: controlRoot.delegateHorizontalPadding
+
isFileActivityList: controlRoot.isFileActivityList
- width: activityList.contentWidth
+ iconSize: controlRoot.iconSize
flickable: activityList
onHoveredChanged: if (hovered) {
// When we set the currentIndex the list view will scroll...
diff --git a/src/gui/tray/CustomButton.qml b/src/gui/tray/CustomButton.qml
index c132830be..f07acc31c 100644
--- a/src/gui/tray/CustomButton.qml
+++ b/src/gui/tray/CustomButton.qml
@@ -7,7 +7,8 @@ Button {
id: root
property string imageSource: ""
- property string imageSourceHover: ""
+ property string imageSourceHover: imageSource
+ property var iconItem: icon
property string toolTipText: ""
diff --git a/src/gui/tray/FileActivityDialog.qml b/src/gui/tray/FileActivityDialog.qml
deleted file mode 100644
index 1d75ff222..000000000
--- a/src/gui/tray/FileActivityDialog.qml
+++ /dev/null
@@ -1,40 +0,0 @@
-import QtQml 2.15
-import QtQuick 2.15
-import QtQuick.Window 2.15
-
-import Style 1.0
-import com.nextcloud.desktopclient 1.0 as NC
-
-Window {
- id: dialog
-
- property alias model: activityModel
-
- NC.FileActivityListModel {
- id: activityModel
- }
-
- width: 500
- height: 500
-
- Rectangle {
- id: background
- anchors.fill: parent
- color: Style.backgroundColor
- }
-
- ActivityList {
- isFileActivityList: true
- anchors.fill: parent
- model: dialog.model
- }
-
- Component.onCompleted: {
- dialog.show();
- dialog.raise();
- dialog.requestActivate();
-
- Systray.forceWindowInit(dialog);
- Systray.positionWindowAtScreenCenter(dialog);
- }
-}
diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml
index 94611c647..c462b305f 100644
--- a/src/gui/tray/Window.qml
+++ b/src/gui/tray/Window.qml
@@ -21,16 +21,8 @@ ApplicationWindow {
color: "transparent"
flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint
- property int fileActivityDialogObjectId: -1
-
readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth
- function openFileActivityDialog(objectName, objectId) {
- fileActivityDialogLoader.objectName = objectName;
- fileActivityDialogLoader.objectId = objectId;
- fileActivityDialogLoader.refresh();
- }
-
Component.onCompleted: Systray.forceWindowInit(trayWindow)
// Close tray window when focus is lost (e.g. click somewhere else on the screen)
@@ -91,10 +83,6 @@ ApplicationWindow {
}
}
- function onShowFileActivityDialog(objectName, objectId) {
- openFileActivityDialog(objectName, objectId)
- }
-
function onShowErrorMessageDialog(error) {
var newErrorDialog = errorMessageDialog.createObject(trayWindow)
newErrorDialog.text = error
@@ -819,26 +807,5 @@ ApplicationWindow {
model.slotTriggerDefaultAction(index)
}
}
-
- Loader {
- id: fileActivityDialogLoader
-
- property string objectName: ""
- property int objectId: -1
-
- function refresh() {
- active = true
- item.model.load(activityModel.accountState, objectId)
- item.show()
- }
-
- active: false
- sourceComponent: FileActivityDialog {
- title: qsTr("%1 - File activity").arg(fileActivityDialogLoader.objectName)
- onClosing: fileActivityDialogLoader.active = false
- }
-
- onLoaded: refresh()
- }
} // Item trayWindowMainItem
}
diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp
index 8ebdcfef5..352f23a55 100644
--- a/src/gui/tray/activitylistmodel.cpp
+++ b/src/gui/tray/activitylistmodel.cpp
@@ -93,6 +93,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
void ActivityListModel::setAccountState(AccountState *state)
{
_accountState = state;
+ Q_EMIT accountStateChanged();
}
void ActivityListModel::setCurrentItem(const int currentItem)
diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h
index 1c070487d..e030445ed 100644
--- a/src/gui/tray/activitylistmodel.h
+++ b/src/gui/tray/activitylistmodel.h
@@ -39,9 +39,8 @@ class InvalidFilenameDialog;
class ActivityListModel : public QAbstractListModel
{
Q_OBJECT
-
Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT)
- Q_PROPERTY(AccountState *accountState READ accountState CONSTANT)
+ Q_PROPERTY(AccountState *accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged)
public:
enum DataRole {
@@ -123,6 +122,8 @@ public slots:
void setCurrentItem(const int currentItem);
signals:
+ void accountStateChanged();
+
void activityJobStatusCode(int statusCode);
void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
diff --git a/src/gui/tray/asyncimageresponse.cpp b/src/gui/tray/asyncimageresponse.cpp
index f393c837a..6bcee6778 100644
--- a/src/gui/tray/asyncimageresponse.cpp
+++ b/src/gui/tray/asyncimageresponse.cpp
@@ -63,17 +63,30 @@ void AsyncImageResponse::processNextImage()
return;
}
- if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) {
- setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage());
+ const auto imagePath = _imagePaths.at(_index);
+ if (imagePath.startsWith(QStringLiteral(":/client"))) {
+ setImageAndEmitFinished(QIcon(imagePath).pixmap(_requestedImageSize).toImage());
return;
+ } else if (imagePath.startsWith(QStringLiteral(":/fileicon"))) {
+ const auto filePath = imagePath.mid(10);
+ const auto fileInfo = QFileInfo(filePath);
+ setImageAndEmitFinished(_fileIconProvider.icon(fileInfo).pixmap(_requestedImageSize).toImage());
+ return;
+ }
+
+ OCC::AccountPtr accountInRequestedServer;
+
+ for (const auto &account : OCC::AccountManager::instance()->accounts()) {
+ if (account && account->account() && imagePath.startsWith(account->account()->url().toString())) {
+ accountInRequestedServer = account->account();
+ }
}
- const auto currentUser = OCC::UserModel::instance()->currentUser();
- if (currentUser && currentUser->account()) {
+ if (accountInRequestedServer) {
const QUrl iconUrl(_imagePaths.at(_index));
if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) {
// fetch the remote resource
- const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl);
+ const auto reply = accountInRequestedServer->sendRawRequest(QByteArrayLiteral("GET"), iconUrl);
connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply);
++_index;
return;
diff --git a/src/gui/tray/asyncimageresponse.h b/src/gui/tray/asyncimageresponse.h
index e27c7b99c..9d2d60003 100644
--- a/src/gui/tray/asyncimageresponse.h
+++ b/src/gui/tray/asyncimageresponse.h
@@ -16,6 +16,7 @@
#include <QImage>
#include <QQuickImageProvider>
+#include <QFileIconProvider>
class AsyncImageResponse : public QQuickImageResponse
{
@@ -34,5 +35,6 @@ private slots:
QStringList _imagePaths;
QSize _requestedImageSize;
QColor _svgRecolor;
+ QFileIconProvider _fileIconProvider;
int _index = 0;
};
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index c3fe37a99..a66133905 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -10,6 +10,7 @@ add_library(testutils
pushnotificationstestutils.cpp
themeutils.cpp
testhelper.cpp
+ sharetestutils.cpp
)
target_link_libraries(testutils PUBLIC Nextcloud::sync Qt5::Test)
@@ -64,6 +65,9 @@ nextcloud_add_test(ActivityListModel)
nextcloud_add_test(ActivityData)
nextcloud_add_test(TalkReply)
nextcloud_add_test(LockFile)
+nextcloud_add_test(ShareModel)
+nextcloud_add_test(ShareeModel)
+nextcloud_add_test(SortedShareModel)
if( UNIX AND NOT APPLE )
nextcloud_add_test(InotifyWatcher)
diff --git a/test/sharetestutils.cpp b/test/sharetestutils.cpp
new file mode 100644
index 000000000..7f481a612
--- /dev/null
+++ b/test/sharetestutils.cpp
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2022 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 "sharetestutils.h"
+
+#include "testhelper.h"
+
+using namespace OCC;
+
+FakeShareDefinition::FakeShareDefinition(ShareTestHelper *helper,
+ const Share::ShareType type,
+ const QString &shareWith,
+ const QString &displayString,
+ const QString &password,
+ const QString &note,
+ const QString &expiration)
+{
+ ++helper->latestShareId;
+ const auto idString = QString::number(helper->latestShareId);
+
+
+ fileDefinition = helper->fakeFileDefinition;
+ shareId = idString;
+ shareCanDelete = true;
+ shareCanEdit = true;
+ shareUidOwner = helper->account->davUser();;
+ shareDisplayNameOwner = helper->account->davDisplayName();
+ sharePassword = password;
+ sharePermissions = static_cast<int>(SharePermissions(SharePermissionRead |
+ SharePermissionUpdate |
+ SharePermissionCreate |
+ SharePermissionDelete |
+ SharePermissionShare));
+ shareNote = note;
+ shareHideDownload = 0;
+ shareExpiration = expiration;
+ shareSendPasswordByTalk = false;
+ shareType = type;
+
+ const auto token = QString(QStringLiteral("GQ4aLrZEdJJkopW-") + idString);
+ // Weird, but it's what the server does
+ const auto finalShareWith = type == Share::TypeLink ? password : shareWith;
+ const auto shareWithDisplayName = type == Share::TypeLink ? QStringLiteral("(Shared Link)") : displayString;
+ const auto linkLabel = type == Share::TypeLink ? displayString : QString();
+ const auto linkName = linkShareLabel;
+ const auto linkUrl = type == Share::TypeLink ? QString(helper->account->davUrl().toString() + QStringLiteral("/s/") + token) : QString();
+
+ shareShareWith = finalShareWith;
+ shareShareWithDisplayName = shareWithDisplayName;
+ shareToken = token;
+ linkShareName = linkName;
+ linkShareLabel = linkLabel;
+ linkShareUrl = linkUrl;
+}
+
+QJsonObject FakeShareDefinition::toShareJsonObject() const
+{
+ QJsonObject newShareJson;
+ newShareJson.insert("uid_file_owner", fileDefinition.fileOwnerUid);
+ newShareJson.insert("displayname_file_owner", fileDefinition.fileOwnerDisplayName);
+ newShareJson.insert("file_target", fileDefinition.fileTarget);
+ newShareJson.insert("has_preview", fileDefinition.fileHasPreview);
+ newShareJson.insert("file_parent", fileDefinition.fileFileParent);
+ newShareJson.insert("file_source", fileDefinition.fileSource);
+ newShareJson.insert("item_source", fileDefinition.fileItemSource);
+ newShareJson.insert("item_type", fileDefinition.fileItemType);
+ newShareJson.insert("mail_send", fileDefinition.fileMailSend);
+ newShareJson.insert("mimetype", fileDefinition.fileMimeType);
+ newShareJson.insert("parent", fileDefinition.fileParent);
+ newShareJson.insert("path", fileDefinition.filePath);
+ newShareJson.insert("storage", fileDefinition.fileStorage);
+ newShareJson.insert("storage_id", fileDefinition.fileStorageId);
+ newShareJson.insert("id", shareId);
+ newShareJson.insert("can_delete", shareCanDelete);
+ newShareJson.insert("can_edit", shareCanEdit);
+ newShareJson.insert("uid_owner", shareUidOwner);
+ newShareJson.insert("displayname_owner", shareDisplayNameOwner);
+ newShareJson.insert("password", sharePassword);
+ newShareJson.insert("permissions", sharePermissions);
+ newShareJson.insert("note", shareNote);
+ newShareJson.insert("hide_download", shareHideDownload);
+ newShareJson.insert("expiration", shareExpiration);
+ newShareJson.insert("send_password_by_talk", shareSendPasswordByTalk);
+ newShareJson.insert("share_type", shareType);
+ newShareJson.insert("share_with", shareShareWith);
+ newShareJson.insert("share_with_displayname", shareShareWithDisplayName);
+ newShareJson.insert("token", shareToken);
+ newShareJson.insert("name", linkShareName);
+ newShareJson.insert("label", linkShareLabel);
+ newShareJson.insert("url", linkShareUrl);
+
+ return newShareJson;
+}
+
+QByteArray FakeShareDefinition::toRequestReply() const
+{
+ const auto shareJson = toShareJsonObject();
+ return jsonValueToOccReply(shareJson);
+}
+
+// Below is ShareTestHelper
+ShareTestHelper::ShareTestHelper(QObject *parent)
+ : QObject(parent)
+{
+}
+
+ShareTestHelper::~ShareTestHelper()
+{
+ const auto folder = FolderMan::instance()->folder(fakeFolder.localPath());
+ if (folder) {
+ FolderMan::instance()->removeFolder(folder);
+ }
+ AccountManager::instance()->deleteAccount(accountState.data());
+}
+
+void ShareTestHelper::setup()
+{
+ _fakeQnam.reset(new FakeQNAM({}));
+ _fakeQnam->setOverride([this](const QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
+ return qnamOverride(op, req, device);
+ });
+
+ account = Account::create();
+ account->setCredentials(new FakeCredentials{_fakeQnam.data()});
+ account->setUrl(QUrl(("owncloud://somehost/owncloud")));
+ account->setCapabilities(_fakeCapabilities);
+ accountState = new AccountState(account);
+ AccountManager::instance()->addAccount(account);
+
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ fakeFolder.localModifier().insert(testFileName);
+
+ const auto folderMan = FolderMan::instance();
+ QCOMPARE(folderMan, &fm);
+ QVERIFY(folderMan->addFolder(accountState.data(), folderDefinition(fakeFolder.localPath())));
+ const auto folder = FolderMan::instance()->folder(fakeFolder.localPath());
+ QVERIFY(folder);
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ const auto fakeFileInfo = fakeFolder.remoteModifier().find(testFileName);
+ QVERIFY(fakeFileInfo);
+ fakeFileInfo->permissions.setPermission(RemotePermissions::CanReshare);
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QVERIFY(fakeFileInfo->permissions.CanReshare);
+
+ _fakeCapabilities = QVariantMap {
+ {QStringLiteral("files_sharing"), QVariantMap {
+ {QStringLiteral("api_enabled"), true},
+ {QStringLiteral("default_permissions"), 19},
+ {QStringLiteral("public"), QVariantMap {
+ {QStringLiteral("enabled"), true},
+ {QStringLiteral("expire_date"), QVariantMap {
+ {QStringLiteral("days"), 30},
+ {QStringLiteral("enforced"), false},
+ }},
+ {QStringLiteral("expire_date_internal"), QVariantMap {
+ {QStringLiteral("days"), 30},
+ {QStringLiteral("enforced"), false},
+ }},
+ {QStringLiteral("expire_date_remote"), QVariantMap {
+ {QStringLiteral("days"), 30},
+ {QStringLiteral("enforced"), false},
+ }},
+ {QStringLiteral("password"), QVariantMap {
+ {QStringLiteral("enforced"), false},
+ }},
+ }},
+ {QStringLiteral("sharebymail"), QVariantMap {
+ {QStringLiteral("enabled"), true},
+ {QStringLiteral("password"), QVariantMap {
+ {QStringLiteral("enforced"), false},
+ }},
+ }},
+ }},
+ };
+
+ // Generate test data
+ // Properties that apply to the file generally
+ const auto fileOwnerUid = account->davUser();
+ const auto fileOwnerDisplayName = account->davDisplayName();
+ const auto fileTarget = QString(QStringLiteral("/") + fakeFileInfo->name);
+ const auto fileHasPreview = true;
+ const auto fileFileParent = QString(fakeFolder.remoteModifier().fileId);
+ const auto fileSource = QString(fakeFileInfo->fileId);
+ const auto fileItemSource = fileSource;
+ const auto fileItemType = QStringLiteral("file");
+ const auto fileMailSend = 0;
+ const auto fileMimeType = QStringLiteral("text/markdown");
+ const auto fileParent = QString();
+ const auto filePath = fakeFileInfo->path();
+ const auto fileStorage = 3;
+ const auto fileStorageId = QString(QStringLiteral("home::") + account->davUser());
+
+ fakeFileDefinition = FakeFileReplyDefinition {
+ fileOwnerUid,
+ fileOwnerDisplayName,
+ fileTarget,
+ fileHasPreview,
+ fileFileParent,
+ fileSource,
+ fileItemSource,
+ fileItemType,
+ fileMailSend,
+ fileMimeType,
+ fileParent,
+ filePath,
+ fileStorage,
+ fileStorageId,
+ };
+
+ emit setupSucceeded();
+}
+
+QNetworkReply *ShareTestHelper::qnamOverride(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device)
+{
+ QNetworkReply *reply = nullptr;
+
+ const auto reqUrl = req.url();
+ const auto reqRawPath = reqUrl.path();
+ const auto reqPath = reqRawPath.startsWith("/owncloud/") ? reqRawPath.mid(10) : reqRawPath;
+ qDebug() << req.url() << reqPath << op;
+
+ // Properly formatted PROPFIND URL goes something like:
+ // https://cloud.nextcloud.com/remote.php/dav/files/claudio/Readme.md
+ if(reqPath.endsWith(testFileName) && req.attribute(QNetworkRequest::CustomVerbAttribute) == "PROPFIND") {
+
+ reply = new FakePropfindReply(fakeFolder.remoteModifier(), op, req, this);
+
+ } else if (req.url().toString().startsWith(accountState->account()->url().toString()) &&
+ reqPath.startsWith(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares"))) {
+
+ if (op == QNetworkAccessManager::PostOperation) {
+ reply = handleSharePostOperation(op, req, device);
+
+ } else if(req.attribute(QNetworkRequest::CustomVerbAttribute) == "DELETE") {
+ reply = handleShareDeleteOperation(op, req, reqPath);
+
+ } else if(op == QNetworkAccessManager::PutOperation) {
+ reply = handleSharePutOperation(op, req, reqPath, device);
+
+ } else if(req.attribute(QNetworkRequest::CustomVerbAttribute) == "GET") {
+ reply = handleShareGetOperation(op, req, reqPath);
+ }
+ } else {
+ reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
+ }
+
+ return reply;
+}
+
+QNetworkReply *ShareTestHelper::handleSharePostOperation(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device)
+{
+ QNetworkReply *reply = nullptr;
+
+ // POST https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json
+ // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 1527752d-e147-4da7-89b8-fb06315a5fad, }
+ // Data: [path=file.md&shareType=3]"
+ const QUrlQuery urlQuery(req.url());
+ const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
+
+ if (formatParam == QStringLiteral("json")) {
+ device->open(QIODevice::ReadOnly);
+ const auto requestBody = device->readAll();
+ device->close();
+
+ const auto requestData = requestBody.split('&');
+ // We don't care about path since we know the file we are testing with
+ auto requestShareType = -10; // Just in case
+ QString requestShareWith;
+ QString requestName;
+ QString requestPassword;
+
+ for(const auto &data : requestData) {
+ const auto requestDataUrl = QUrl::fromPercentEncoding(data);
+ const QString requestDataUrlString(requestDataUrl);
+
+ if (data.contains("shareType=")) {
+ const auto shareTypeString = requestDataUrlString.mid(10);
+ requestShareType = Share::ShareType(shareTypeString.toInt());
+ } else if (data.contains("shareWith=")) {
+ requestShareWith = data.mid(10);
+ } else if (data.contains("name=")) {
+ requestName = data.mid(5);
+ } else if (data.contains("password=")) {
+ requestPassword = data.mid(9);
+ }
+ }
+
+ if (requestPassword.isEmpty() &&
+ ((requestShareType == Share::TypeEmail && account->capabilities().shareEmailPasswordEnforced()) ||
+ (requestShareType == Share::TypeLink && account->capabilities().sharePublicLinkEnforcePassword()))) {
+
+ reply = new FakePayloadReply(op, req, _fake403Response, searchResultsReplyDelay, _fakeQnam.data());
+
+ } else if (requestShareType >= 0) {
+ const auto shareType = Share::ShareType(requestShareType);
+ reply = new FakePayloadReply(op, req, createNewShare(shareType, requestShareWith, requestPassword), searchResultsReplyDelay, _fakeQnam.data());
+ }
+ }
+
+ return reply;
+}
+
+QNetworkReply *ShareTestHelper::handleSharePutOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath, QIODevice *device)
+{
+ QNetworkReply *reply = nullptr;
+
+ const auto splitUrlPath = reqPath.split('/');
+ const auto shareId = splitUrlPath.last();
+
+ const QUrlQuery urlQuery(req.url());
+ const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
+
+ if (formatParam == QStringLiteral("json")) {
+ device->open(QIODevice::ReadOnly);
+ const auto requestBody = device->readAll();
+ device->close();
+
+ const auto requestData = requestBody.split('&');
+
+ const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
+ return value.toObject().value("id").toString() == shareId;
+ });
+
+ if (existingShareIterator == _sharesReplyData.cend()) {
+ reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
+ } else {
+ const auto existingShareValue = *existingShareIterator;
+ auto shareObject = existingShareValue.toObject();
+
+ for (const auto &requestDataItem : requestData) {
+ const auto requestSplit = requestDataItem.split('=');
+ auto requestKey = requestSplit.first();
+ auto requestValue = requestSplit.last();
+
+ // We send expireDate without time but the server returns with time at 00:00:00
+ if (requestKey == "expireDate") {
+ requestKey = "expiration";
+ requestValue.append(" 00:00:00");
+ }
+
+ shareObject.insert(QString(requestKey), QString(requestValue));
+ }
+
+ _sharesReplyData.replace(existingShareIterator - _sharesReplyData.cbegin(), shareObject);
+ reply = new FakePayloadReply(op, req, jsonValueToOccReply(shareObject), searchResultsReplyDelay, _fakeQnam.data());
+ }
+ }
+
+ return reply;
+}
+
+
+QNetworkReply *ShareTestHelper::handleShareDeleteOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath)
+{
+ QNetworkReply *reply = nullptr;
+
+ const auto splitUrlPath = reqPath.split('/');
+ const auto shareId = splitUrlPath.last();
+
+ const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
+ return value.toObject().value("id").toString() == shareId;
+ });
+
+ if (existingShareIterator == _sharesReplyData.cend()) {
+ reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
+ } else {
+ _sharesReplyData.removeAt(existingShareIterator - _sharesReplyData.cbegin());
+ reply = new FakePayloadReply(op, req, _fake200JsonResponse, searchResultsReplyDelay, _fakeQnam.data());
+ }
+
+ return reply;
+}
+
+QNetworkReply *ShareTestHelper::handleShareGetOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath)
+{
+ QNetworkReply *reply = nullptr;
+
+ // Properly formatted request to fetch shares goes something like:
+ // GET https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?path=file.md&reshares=true&format=json
+ // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 8ba8960d-ca0d-45ba-abf4-03ab95ba6064, }
+ // Data: []
+ const auto urlQuery = QUrlQuery(req.url());
+ const auto pathParam = urlQuery.queryItemValue(QStringLiteral("path"));
+ const auto resharesParam = urlQuery.queryItemValue(QStringLiteral("reshares"));
+ const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
+
+ if (formatParam != QStringLiteral("json") || (!pathParam.isEmpty() && !pathParam.endsWith(QString(testFileName)))) {
+ reply = new FakeErrorReply(op, req, this, 400, _fake400Response);
+ } else if (reqPath.contains(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares"))) {
+ reply = new FakePayloadReply(op, req, jsonValueToOccReply(_sharesReplyData), searchResultsReplyDelay, _fakeQnam.data());
+ }
+
+ return reply;
+}
+
+const QByteArray ShareTestHelper::createNewShare(const Share::ShareType shareType, const QString &shareWith, const QString &password)
+{
+ const auto displayString = shareType == Share::TypeLink ? QString() : shareWith;
+ const FakeShareDefinition newShareDefinition(this,
+ shareType,
+ shareWith,
+ displayString,
+ password);
+
+ _sharesReplyData.append(newShareDefinition.toShareJsonObject());
+ return newShareDefinition.toRequestReply();
+}
+
+int ShareTestHelper::shareCount() const
+{
+ return _sharesReplyData.count();
+}
+
+void ShareTestHelper::appendShareReplyData(const FakeShareDefinition &definition)
+{
+ _sharesReplyData.append(definition.toShareJsonObject());
+}
+
+void ShareTestHelper::resetTestShares()
+{
+ _sharesReplyData = QJsonArray();
+}
+
+void ShareTestHelper::resetTestData()
+{
+ resetTestShares();
+ account->setCapabilities(_fakeCapabilities);
+}
diff --git a/test/sharetestutils.h b/test/sharetestutils.h
new file mode 100644
index 000000000..c539547fe
--- /dev/null
+++ b/test/sharetestutils.h
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+
+#include "gui/accountmanager.h"
+#include "gui/folderman.h"
+#include "gui/sharemanager.h"
+
+#include "syncenginetestutils.h"
+
+using namespace OCC;
+
+struct FakeFileReplyDefinition
+{
+ QString fileOwnerUid;
+ QString fileOwnerDisplayName;
+ QString fileTarget;
+ bool fileHasPreview;
+ QString fileFileParent;
+ QString fileSource;
+ QString fileItemSource;
+ QString fileItemType;
+ int fileMailSend;
+ QString fileMimeType;
+ QString fileParent;
+ QString filePath;
+ int fileStorage;
+ QString fileStorageId;
+};
+
+struct FakeShareDefinition
+{
+ FakeShareDefinition() = default;
+ FakeShareDefinition(ShareTestHelper *helper,
+ const Share::ShareType type,
+ const QString &shareWith,
+ const QString &displayString,
+ const QString &password = QString(),
+ const QString &note = QString(),
+ const QString &expiration = QString());
+
+ FakeFileReplyDefinition fileDefinition;
+ QString shareId;
+ bool shareCanDelete;
+ bool shareCanEdit;
+ QString shareUidOwner;
+ QString shareDisplayNameOwner;
+ QString sharePassword;
+ int sharePermissions;
+ QString shareNote;
+ int shareHideDownload;
+ QString shareExpiration;
+ bool shareSendPasswordByTalk;
+ int shareType;
+ QString shareShareWith;
+ QString shareShareWithDisplayName;
+ QString shareToken;
+ QString linkShareName;
+ QString linkShareLabel;
+ QString linkShareUrl;
+
+ [[nodiscard]] QJsonObject toShareJsonObject() const;
+ [[nodiscard]] QByteArray toRequestReply() const;
+};
+
+class ShareTestHelper : public QObject
+{
+ Q_OBJECT
+
+public:
+ ShareTestHelper(QObject *parent = nullptr);
+ ~ShareTestHelper() override;
+
+ FolderMan fm;
+ FakeFolder fakeFolder{FileInfo{}};
+ FakeFileReplyDefinition fakeFileDefinition;
+
+ AccountPtr account;
+ AccountStatePtr accountState;
+
+ int latestShareId = 0;
+
+ static constexpr auto testFileName = "file.md";
+ static constexpr auto searchResultsReplyDelay = 100;
+ static constexpr auto expectedDtFormat = "yyyy-MM-dd 00:00:00";
+
+ const QByteArray createNewShare(const Share::ShareType shareType, const QString &shareWith, const QString &password);
+ [[nodiscard]] int shareCount() const;
+
+signals:
+ void setupSucceeded();
+
+public slots:
+ void setup();
+ void appendShareReplyData(const FakeShareDefinition &definition);
+ void resetTestShares();
+ void resetTestData();
+
+private slots:
+ [[nodiscard]] QNetworkReply *qnamOverride(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device);
+ [[nodiscard]] QNetworkReply *handleSharePostOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device);
+ [[nodiscard]] QNetworkReply *handleSharePutOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath, QIODevice *device);
+ [[nodiscard]] QNetworkReply *handleShareDeleteOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath);
+ [[nodiscard]] QNetworkReply *handleShareGetOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath);
+
+private:
+ QScopedPointer<FakeQNAM> _fakeQnam;
+
+ QByteArray _fake404Response = R"({"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Invalid query, please check the syntax. API specifications are here: http:\/\/www.freedesktop.org\/wiki\/Specifications\/open-collaboration-services.\n"},"data":[]}})";
+ QByteArray _fake403Response = R"({"ocs":{"meta":{"status":"failure","statuscode":403,"message":"Operation not allowed."},"data":[]}})";
+ QByteArray _fake400Response = R"({"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}})";
+ QByteArray _fake200JsonResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
+
+ QJsonArray _sharesReplyData;
+ QVariantMap _fakeCapabilities;
+ QSet<int> _liveShareIds;
+};
diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp
index f7e1faf5a..72a08a95b 100644
--- a/test/syncenginetestutils.cpp
+++ b/test/syncenginetestutils.cpp
@@ -8,6 +8,7 @@
#include "syncenginetestutils.h"
#include "httplogger.h"
#include "accessmanager.h"
+#include "gui/sharepermissions.h"
#include <QJsonDocument>
#include <QJsonArray>
@@ -349,8 +350,15 @@ FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAcces
xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(QString::fromLatin1(fileInfo.etag)));
xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
+ xml.writeTextElement(ocUri, QStringLiteral("share-permissions"), QString::number(static_cast<int>(OCC::SharePermissions(OCC::SharePermissionRead |
+ OCC::SharePermissionUpdate |
+ OCC::SharePermissionCreate |
+ OCC::SharePermissionDelete |
+ OCC::SharePermissionShare))));
xml.writeTextElement(ocUri, QStringLiteral("id"), QString::fromUtf8(fileInfo.fileId));
+ xml.writeTextElement(ocUri, QStringLiteral("fileid"), QString::fromUtf8(fileInfo.fileId));
xml.writeTextElement(ocUri, QStringLiteral("checksums"), QString::fromUtf8(fileInfo.checksums));
+ xml.writeTextElement(ocUri, QStringLiteral("privatelink"), href);
xml.writeTextElement(ncUri, QStringLiteral("lock-owner"), fileInfo.lockOwnerId);
xml.writeTextElement(ncUri, QStringLiteral("lock"), fileInfo.lockState == FileInfo::LockState::FileLocked ? QStringLiteral("1") : QStringLiteral("0"));
xml.writeTextElement(ncUri, QStringLiteral("lock-owner-type"), fileInfo.lockOwnerId);
diff --git a/test/testhelper.cpp b/test/testhelper.cpp
index 8ba0f151b..2a540965f 100644
--- a/test/testhelper.cpp
+++ b/test/testhelper.cpp
@@ -1,4 +1,6 @@
#include "testhelper.h"
+#include <QJsonObject>
+#include <QJsonDocument>
OCC::FolderDefinition folderDefinition(const QString &path)
{
@@ -8,3 +10,19 @@ OCC::FolderDefinition folderDefinition(const QString &path)
d.alias = path;
return d;
}
+
+
+const QByteArray jsonValueToOccReply(const QJsonValue &jsonValue)
+{
+ QJsonObject root;
+ QJsonObject ocs;
+ QJsonObject meta;
+
+ meta.insert("statuscode", 200);
+
+ ocs.insert(QStringLiteral("data"), jsonValue);
+ ocs.insert(QStringLiteral("meta"), meta);
+ root.insert(QStringLiteral("ocs"), ocs);
+
+ return QJsonDocument(root).toJson();
+}
diff --git a/test/testhelper.h b/test/testhelper.h
index 40379577c..2ddb859e5 100644
--- a/test/testhelper.h
+++ b/test/testhelper.h
@@ -18,4 +18,6 @@ public:
OCC::FolderDefinition folderDefinition(const QString &path);
+const QByteArray jsonValueToOccReply(const QJsonValue &jsonValue);
+
#endif // TESTHELPER_H
diff --git a/test/testshareemodel.cpp b/test/testshareemodel.cpp
new file mode 100644
index 000000000..c9560a75f
--- /dev/null
+++ b/test/testshareemodel.cpp
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2022 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/filedetails/shareemodel.h"
+
+#include <QTest>
+#include <QSignalSpy>
+
+#include "accountmanager.h"
+#include "syncenginetestutils.h"
+#include "testhelper.h"
+
+using namespace OCC;
+
+static QByteArray fake400Response = R"(
+{"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}}
+)";
+
+constexpr auto searchResultsReplyDelay = 100;
+
+class TestShareeModel : public QObject
+{
+ Q_OBJECT
+
+public:
+ ~TestShareeModel() override
+ {
+ AccountManager::instance()->deleteAccount(_accountState.data());
+ };
+
+ struct FakeShareeDefinition
+ {
+ QString label;
+
+ QString shareWith;
+ Sharee::Type type;
+ QString shareWithAdditionalInfo;
+ };
+
+ void appendShareeToReply(const FakeShareeDefinition &definition)
+ {
+ QJsonObject newShareeJson;
+ newShareeJson.insert("label", definition.label);
+
+ QJsonObject newShareeValueJson;
+ newShareeValueJson.insert("shareWith", definition.shareWith);
+ newShareeValueJson.insert("shareType", definition.type);
+ newShareeValueJson.insert("shareWithAdditionalInfo", definition.shareWithAdditionalInfo);
+
+ newShareeJson.insert("value", newShareeValueJson);
+
+ QString category;
+ switch(definition.type) {
+ case Sharee::Circle:
+ category = QStringLiteral("circles");
+ break;
+ case Sharee::Email:
+ category = QStringLiteral("emails");
+ break;
+ case Sharee::Federated:
+ category = QStringLiteral("remotes");
+ break;
+ case Sharee::Group:
+ category = QStringLiteral("groups");
+ break;
+ case Sharee::Room:
+ category = QStringLiteral("rooms");
+ break;
+ case Sharee::User:
+ category = QStringLiteral("users");
+ break;
+ }
+
+ auto shareesInCategory = _shareesMap.value(category).toJsonArray();
+ shareesInCategory.append(newShareeJson);
+ _shareesMap.insert(category, shareesInCategory);
+ }
+
+ void standardReplyPopulate()
+ {
+ appendShareeToReply(_michaelUserDefinition);
+ appendShareeToReply(_liamUserDefinition);
+ appendShareeToReply(_iqbalUserDefinition);
+ appendShareeToReply(_universityGroupDefinition);
+ appendShareeToReply(_testEmailDefinition);
+ }
+
+ QVariantMap filteredSharees(const QString &searchString)
+ {
+ if (searchString.isEmpty()) {
+ return _shareesMap;
+ }
+
+ QVariantMap returnSharees;
+ QJsonArray exactMatches;
+
+ for (auto it = _shareesMap.constKeyValueBegin(); it != _shareesMap.constKeyValueEnd(); ++it) {
+ const auto shareesCategory = it->first;
+ const auto shareesArray = it->second.toJsonArray();
+ QJsonArray filteredShareesArray;
+
+ std::copy_if(shareesArray.cbegin(), shareesArray.cend(), std::back_inserter(filteredShareesArray), [&searchString](const QJsonValue &shareeValue) {
+ const auto shareeObject = shareeValue.toObject().value("value").toObject();
+ const auto shareeShareWith = shareeObject.value("shareWith").toString();
+ return shareeShareWith.contains(searchString, Qt::CaseInsensitive);
+ });
+
+ std::copy_if(filteredShareesArray.cbegin(), filteredShareesArray.cend(), std::back_inserter(exactMatches), [&searchString](const QJsonValue &shareeValue) {
+ const auto shareeObject = shareeValue.toObject().value("value").toObject();
+ const auto shareeShareWith = shareeObject.value("shareWith").toString();
+ return shareeShareWith == searchString;
+ });
+
+ returnSharees.insert(shareesCategory, filteredShareesArray);
+ }
+
+ returnSharees.insert(QStringLiteral("exact"), exactMatches);
+
+ return returnSharees;
+ }
+
+ QByteArray testShareesReply(const QString &searchString)
+ {
+ QJsonObject root;
+ QJsonObject ocs;
+ QJsonObject meta;
+
+ meta.insert("statuscode", 200);
+
+ const auto resultSharees = filteredSharees(searchString);
+ const auto shareesJsonObject = QJsonObject::fromVariantMap(resultSharees);
+
+ ocs.insert(QStringLiteral("data"), shareesJsonObject);
+ ocs.insert(QStringLiteral("meta"), meta);
+ root.insert(QStringLiteral("ocs"), ocs);
+
+ return QJsonDocument(root).toJson();
+ }
+
+ int shareesCount(const QString &searchString)
+ {
+ const auto sharees = filteredSharees(searchString);
+
+ auto count = 0;
+ const auto shareesCategories = sharees.values();
+ for (const auto &shareesArrayValue : shareesCategories) {
+ const auto shareesArray = shareesArrayValue.toJsonArray();
+ count += shareesArray.count();
+ }
+
+ return count;
+ }
+
+ void resetTestData()
+ {
+ _alwaysReturnErrors = false;
+ _shareesMap.clear();
+ }
+
+
+private:
+ AccountPtr _account;
+ AccountStatePtr _accountState;
+ QScopedPointer<FakeQNAM> _fakeQnam;
+
+ QVariantMap _shareesMap;
+
+ // Some fake sharees of different categories
+ // ALL OF THEM CONTAIN AN 'I' !! Important for testing
+ FakeShareeDefinition _michaelUserDefinition {
+ QStringLiteral("Michael"),
+ QStringLiteral("michael"),
+ Sharee::User,
+ {},
+ };
+ FakeShareeDefinition _liamUserDefinition {
+ QStringLiteral("Liam"),
+ QStringLiteral("liam"),
+ Sharee::User,
+ {},
+ };
+ FakeShareeDefinition _iqbalUserDefinition {
+ QStringLiteral("Iqbal"),
+ QStringLiteral("iqbal"),
+ Sharee::User,
+ {},
+ };
+
+ FakeShareeDefinition _universityGroupDefinition {
+ QStringLiteral("University"),
+ QStringLiteral("university"),
+ Sharee::Group,
+ {},
+ };
+
+ FakeShareeDefinition _testEmailDefinition {
+ QStringLiteral("test.email@nextcloud.com"),
+ QStringLiteral("test.email@nextcloud.com"),
+ Sharee::Email,
+ {},
+ };
+
+ bool _alwaysReturnErrors = false;
+
+private slots:
+ void initTestCase()
+ {
+ _fakeQnam.reset(new FakeQNAM({}));
+ _fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
+ Q_UNUSED(device);
+
+ QNetworkReply *reply = nullptr;
+
+ if (_alwaysReturnErrors) {
+ reply = new FakeErrorReply(op, req, this, 400, fake400Response);
+ return reply;
+ }
+
+ const auto reqUrl = req.url();
+ const auto reqRawPath = reqUrl.path();
+ const auto reqPath = reqRawPath.startsWith("/owncloud/") ? reqRawPath.mid(10) : reqRawPath;
+ qDebug() << req.url() << reqPath << op;
+
+ if(req.url().toString().startsWith(_accountState->account()->url().toString()) &&
+ reqPath == QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/sharees") &&
+ req.attribute(QNetworkRequest::CustomVerbAttribute) == "GET") {
+
+ const auto urlQuery = QUrlQuery(req.url());
+ const auto searchParam = urlQuery.queryItemValue(QStringLiteral("search"));
+ const auto itemTypeParam = urlQuery.queryItemValue(QStringLiteral("itemType"));
+ const auto pageParam = urlQuery.queryItemValue(QStringLiteral("page"));
+ const auto perPageParam = urlQuery.queryItemValue(QStringLiteral("perPage"));
+ const auto lookupParam = urlQuery.queryItemValue(QStringLiteral("lookup"));
+ const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
+
+ if (formatParam != QStringLiteral("json")) {
+ reply = new FakeErrorReply(op, req, this, 400, fake400Response);
+ } else {
+ reply = new FakePayloadReply(op, req, testShareesReply(searchParam), searchResultsReplyDelay, _fakeQnam.data());
+ }
+ }
+
+ return reply;
+ });
+
+ _account = Account::create();
+ _account->setCredentials(new FakeCredentials{_fakeQnam.data()});
+ _account->setUrl(QUrl(("owncloud://somehost/owncloud")));
+ _accountState = new AccountState(_account);
+ AccountManager::instance()->addAccount(_account);
+
+ // Let's verify our test is working -- all sharees have an I in their "shareWith"
+ standardReplyPopulate();
+ const auto searchString = QStringLiteral("i");
+ QCOMPARE(shareesCount(searchString), 5);
+
+ const auto emailSearchString = QStringLiteral("email");
+ QCOMPARE(shareesCount(emailSearchString), 1);
+ }
+
+ void testSetAccountAndPath()
+ {
+ resetTestData();
+
+ ShareeModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy accountStateChanged(&model, &ShareeModel::accountStateChanged);
+ QSignalSpy shareItemIsFolderChanged(&model, &ShareeModel::shareItemIsFolderChanged);
+ QSignalSpy searchStringChanged(&model, &ShareeModel::searchStringChanged);
+ QSignalSpy lookupModeChanged(&model, &ShareeModel::lookupModeChanged);
+ QSignalSpy shareeBlocklistChanged(&model, &ShareeModel::shareeBlocklistChanged);
+
+ model.setAccountState(_accountState.data());
+ QCOMPARE(accountStateChanged.count(), 1);
+ QCOMPARE(model.accountState(), _accountState.data());
+
+ const auto shareItemIsFolder = !model.shareItemIsFolder();
+ model.setShareItemIsFolder(shareItemIsFolder);
+ QCOMPARE(shareItemIsFolderChanged.count(), 1);
+ QCOMPARE(model.shareItemIsFolder(), shareItemIsFolder);
+
+ const auto searchString = QStringLiteral("search string");
+ model.setSearchString(searchString);
+ QCOMPARE(searchStringChanged.count(), 1);
+ QCOMPARE(model.searchString(), searchString);
+
+ const auto lookupMode = ShareeModel::LookupMode::GlobalSearch;
+ model.setLookupMode(lookupMode);
+ QCOMPARE(lookupModeChanged.count(), 1);
+ QCOMPARE(model.lookupMode(), lookupMode);
+
+ const ShareePtr sharee(new Sharee(_testEmailDefinition.shareWith, _testEmailDefinition.label, _testEmailDefinition.type));
+ const QVariantList shareeBlocklist {QVariant::fromValue(sharee)};
+ model.setShareeBlocklist(shareeBlocklist);
+ QCOMPARE(shareeBlocklistChanged.count(), 1);
+ QCOMPARE(model.shareeBlocklist(), shareeBlocklist);
+ }
+
+ void testShareesFetch()
+ {
+ resetTestData();
+ standardReplyPopulate();
+
+ ShareeModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ model.setAccountState(_accountState.data());
+
+ QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
+ const auto searchString = QStringLiteral("i");
+ model.setSearchString(searchString);
+ QVERIFY(shareesReady.wait(3000));
+ QCOMPARE(model.rowCount(), shareesCount(searchString));
+
+ const auto emailSearchString = QStringLiteral("email");
+ model.setSearchString(emailSearchString);
+ QVERIFY(shareesReady.wait(3000));
+ QCOMPARE(model.rowCount(), shareesCount(emailSearchString));
+ }
+
+ void testFetchSignalling()
+ {
+ resetTestData();
+ standardReplyPopulate();
+
+ ShareeModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ model.setAccountState(_accountState.data());
+ QSignalSpy fetchOngoingChanged(&model, &ShareeModel::fetchOngoingChanged);
+ const auto searchString = QStringLiteral("i");
+ model.setSearchString(searchString);
+
+ QVERIFY(fetchOngoingChanged.wait(1000));
+ QCOMPARE(model.fetchOngoing(), true);
+ QVERIFY(fetchOngoingChanged.wait(3000));
+ QCOMPARE(model.fetchOngoing(), false);
+ }
+
+ void testData()
+ {
+ resetTestData();
+ appendShareeToReply(_testEmailDefinition);
+
+ ShareeModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ model.setAccountState(_accountState.data());
+ const auto searchString = QStringLiteral("i");
+ model.setSearchString(searchString);
+
+ QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
+ QVERIFY(shareesReady.wait(3000));
+ QCOMPARE(model.rowCount(), shareesCount(searchString));
+
+ const auto shareeIndex = model.index(0, 0, {});
+
+ const ShareePtr expectedSharee(new Sharee(_testEmailDefinition.shareWith, _testEmailDefinition.label, _testEmailDefinition.type));
+ const auto sharee = shareeIndex.data(ShareeModel::ShareeRole).value<ShareePtr>();
+ QCOMPARE(sharee->format(), expectedSharee->format());
+ QCOMPARE(sharee->shareWith(), expectedSharee->shareWith());
+ QCOMPARE(sharee->displayName(), expectedSharee->displayName());
+ QCOMPARE(sharee->type(), expectedSharee->type());
+
+ const auto expectedShareeDisplay = QString(_testEmailDefinition.label + QStringLiteral(" (email)"));
+ const auto shareeDisplay = shareeIndex.data(Qt::DisplayRole).toString();
+ QCOMPARE(shareeDisplay, expectedShareeDisplay);
+
+ const auto expectedAutoCompleterStringMatch = QString(_testEmailDefinition.label +
+ QStringLiteral(" (") +
+ _testEmailDefinition.shareWith +
+ QStringLiteral(")"));
+ const auto autoCompleterStringMatch = shareeIndex.data(ShareeModel::AutoCompleterStringMatchRole).toString();
+ QCOMPARE(autoCompleterStringMatch, expectedAutoCompleterStringMatch);
+ }
+
+ void testBlocklist()
+ {
+ resetTestData();
+ standardReplyPopulate();
+
+ ShareeModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ model.setAccountState(_accountState.data());
+
+ const ShareePtr sharee(new Sharee(_testEmailDefinition.shareWith, _testEmailDefinition.label, _testEmailDefinition.type));
+ const QVariantList shareeBlocklist {QVariant::fromValue(sharee)};
+ model.setShareeBlocklist(shareeBlocklist);
+
+ QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
+ const auto searchString = QStringLiteral("i");
+ model.setSearchString(searchString);
+ QVERIFY(shareesReady.wait(3000));
+ QCOMPARE(model.rowCount(), shareesCount(searchString) - 1);
+
+ const ShareePtr shareeTwo(new Sharee(_michaelUserDefinition.shareWith, _michaelUserDefinition.label, _michaelUserDefinition.type));
+ const QVariantList largerShareeBlocklist {QVariant::fromValue(sharee), QVariant::fromValue(shareeTwo)};
+ model.setShareeBlocklist(largerShareeBlocklist);
+ QCOMPARE(model.rowCount(), shareesCount(searchString) - 2);
+ }
+
+ void testServerError()
+ {
+ resetTestData();
+ _alwaysReturnErrors = true;
+
+ ShareeModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ model.setAccountState(_accountState.data());
+
+ QSignalSpy displayErrorMessage(&model, &ShareeModel::displayErrorMessage);
+ QSignalSpy fetchOngoingChanged(&model, &ShareeModel::fetchOngoingChanged);
+ model.setSearchString(QStringLiteral("i"));
+ QVERIFY(displayErrorMessage.wait(3000));
+
+ QCOMPARE(fetchOngoingChanged.count(), 2);
+ QCOMPARE(model.fetchOngoing(), false);
+ }
+};
+
+QTEST_MAIN(TestShareeModel)
+#include "testshareemodel.moc"
diff --git a/test/testsharemodel.cpp b/test/testsharemodel.cpp
new file mode 100644
index 000000000..9a3b10853
--- /dev/null
+++ b/test/testsharemodel.cpp
@@ -0,0 +1,962 @@
+/*
+ * 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/filedetails/sharemodel.h"
+
+#include <QTest>
+#include <QAbstractItemModelTester>
+#include <QSignalSpy>
+#include <QFileInfo>
+#include <QFlags>
+#include <QDateTime>
+#include <QTimeZone>
+
+#include "sharetestutils.h"
+#include "libsync/theme.h"
+
+using namespace OCC;
+
+class TestShareModel : public QObject
+{
+ Q_OBJECT
+
+private:
+ ShareTestHelper helper;
+
+ FakeShareDefinition _testLinkShareDefinition;
+ FakeShareDefinition _testEmailShareDefinition;
+ FakeShareDefinition _testUserShareDefinition;
+ FakeShareDefinition _testRemoteShareDefinition;
+
+private slots:
+ void initTestCase()
+ {
+ QSignalSpy helperSetupSucceeded(&helper, &ShareTestHelper::setupSucceeded);
+ helper.setup();
+ QCOMPARE(helperSetupSucceeded.count(), 1);
+
+ const auto testSharePassword = "3|$argon2id$v=19$m=65536,"
+ "t=4,"
+ "p=1$M2FoLnliWkhIZkwzWjFBQg$BPraP+JUqP1sV89rkymXpCGxHBlCct6bZ39xUGaYQ5w";
+ const auto testShareNote = QStringLiteral("This is a note!");
+ const auto testShareExpiration = QDate::currentDate().addDays(1).toString(helper.expectedDtFormat);
+
+ const auto linkShareLabel = QStringLiteral("Link share label");
+ _testLinkShareDefinition = FakeShareDefinition(&helper,
+ Share::TypeLink,
+ {},
+ linkShareLabel,
+ testSharePassword,
+ testShareNote,
+ testShareExpiration);
+
+ const auto emailShareShareWith = QStringLiteral("test-email@nextcloud.com");
+ const auto emailShareShareWithDisplayName = QStringLiteral("Test email");
+ _testEmailShareDefinition = FakeShareDefinition(&helper,
+ Share::TypeEmail,
+ emailShareShareWith,
+ emailShareShareWithDisplayName,
+ testSharePassword,
+ testShareNote,
+ testShareExpiration);
+
+
+ const auto userShareShareWith = QStringLiteral("user");
+ const auto userShareShareWithDisplayName("A Nextcloud user");
+ _testUserShareDefinition = FakeShareDefinition(&helper,
+ Share::TypeUser,
+ userShareShareWith,
+ userShareShareWithDisplayName);
+
+
+
+ const auto remoteShareShareWith = QStringLiteral("remote_share");
+ const auto remoteShareShareWithDisplayName("A remote share");
+ _testRemoteShareDefinition = FakeShareDefinition(&helper,
+ Share::TypeRemote,
+ remoteShareShareWith,
+ remoteShareShareWithDisplayName);
+
+ qRegisterMetaType<ShareePtr>("ShareePtr");
+ }
+
+ void testSetAccountAndPath()
+ {
+ helper.resetTestData();
+ // Test with a link share
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy accountStateChanged(&model, &ShareModel::accountStateChanged);
+ QSignalSpy localPathChanged(&model, &ShareModel::localPathChanged);
+
+ QSignalSpy accountConnectedChanged(&model, &ShareModel::accountConnectedChanged);
+ QSignalSpy sharingEnabledChanged(&model, &ShareModel::sharingEnabledChanged);
+ QSignalSpy publicLinkSharesEnabledChanged(&model, &ShareModel::publicLinkSharesEnabledChanged);
+
+ model.setAccountState(helper.accountState.data());
+ QCOMPARE(accountStateChanged.count(), 1);
+
+ // Check all the account-related properties of the model
+ QCOMPARE(model.accountConnected(), helper.accountState->isConnected());
+ QCOMPARE(model.sharingEnabled(), helper.account->capabilities().shareAPI());
+ QCOMPARE(model.publicLinkSharesEnabled() && Theme::instance()->linkSharing(), helper.account->capabilities().sharePublicLink());
+ QCOMPARE(Theme::instance()->userGroupSharing(), model.userGroupSharingEnabled());
+
+ const QString localPath(helper.fakeFolder.localPath() + helper.testFileName);
+ model.setLocalPath(localPath);
+ QCOMPARE(localPathChanged.count(), 1);
+ QCOMPARE(model.localPath(), localPath);
+ }
+
+ void testSuccessfulFetchShares()
+ {
+ helper.resetTestData();
+ // Test with a link share and a user/group email share "from the server"
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ helper.appendShareReplyData(_testEmailShareDefinition);
+ helper.appendShareReplyData(_testUserShareDefinition);
+ QCOMPARE(helper.shareCount(), 3);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+ }
+
+ void testFetchSharesFailedError()
+ {
+ helper.resetTestData();
+ // Test with a link share "from the server"
+ helper.appendShareReplyData(_testLinkShareDefinition);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy serverError(&model, &ShareModel::serverError);
+
+ // Test fetching the shares of a file that does not exist
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + "wrong-filename-oops.md");
+ QVERIFY(serverError.wait(3000));
+ QCOMPARE(model.hasInitialShareFetchCompleted(), true);
+ QCOMPARE(model.rowCount(), 0); // Make sure no placeholder
+ }
+
+ void testCorrectFetchOngoingSignalling()
+ {
+ helper.resetTestData();
+
+ // Test with a link share "from the server"
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy fetchOngoingChanged(&model, &ShareModel::fetchOngoingChanged);
+
+ // Make sure we are correctly signalling the loading state of the fetch
+ // Model resets twice when we set account and local path, resetting all model state.
+
+ model.setAccountState(helper.accountState.data());
+ QCOMPARE(fetchOngoingChanged.count(), 1);
+ QCOMPARE(model.fetchOngoing(), false);
+
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+ // If we can grab shares it then indicates fetch ongoing...
+ QCOMPARE(fetchOngoingChanged.count(), 3);
+ QCOMPARE(model.fetchOngoing(), true);
+
+ // Then indicates fetch finished when done.
+ QVERIFY(fetchOngoingChanged.wait(3000));
+ QCOMPARE(model.fetchOngoing(), false);
+ }
+
+ void testCorrectInitialFetchCompleteSignalling()
+ {
+ helper.resetTestData();
+
+ // Test with a link share "from the server"
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy accountStateChanged(&model, &ShareModel::accountStateChanged);
+ QSignalSpy localPathChanged(&model, &ShareModel::localPathChanged);
+ QSignalSpy hasInitialShareFetchCompletedChanged(&model, &ShareModel::hasInitialShareFetchCompletedChanged);
+
+ // Make sure we are correctly signalling the loading state of the fetch
+ // Model resets twice when we set account and local path, resetting all model state.
+
+ model.setAccountState(helper.accountState.data());
+ QCOMPARE(accountStateChanged.count(), 1);
+ QCOMPARE(hasInitialShareFetchCompletedChanged.count(), 1);
+ QCOMPARE(model.hasInitialShareFetchCompleted(), false);
+
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+ QCOMPARE(localPathChanged.count(), 1);
+ QCOMPARE(hasInitialShareFetchCompletedChanged.count(), 2);
+ QCOMPARE(model.hasInitialShareFetchCompleted(), false);
+
+ // Once we have acquired shares from the server the initial share fetch is completed
+ QVERIFY(hasInitialShareFetchCompletedChanged.wait(3000));
+ QCOMPARE(hasInitialShareFetchCompletedChanged.count(), 3);
+ QCOMPARE(model.hasInitialShareFetchCompleted(), true);
+ }
+
+ // Link shares and user group shares have slightly different behaviour in model.data()
+ void testModelLinkShareData()
+ {
+ helper.resetTestData();
+ // Test with a link share "from the server"
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ QVERIFY(!shareIndex.data(Qt::DisplayRole).toString().isEmpty());
+ QCOMPARE(shareIndex.data(ShareModel::ShareTypeRole).toInt(), _testLinkShareDefinition.shareType);
+ QCOMPARE(shareIndex.data(ShareModel::ShareIdRole).toString(), _testLinkShareDefinition.shareId);
+ QCOMPARE(shareIndex.data(ShareModel::LinkRole).toString(), _testLinkShareDefinition.linkShareUrl);
+ QCOMPARE(shareIndex.data(ShareModel::LinkShareNameRole).toString(), _testLinkShareDefinition.linkShareName);
+ QCOMPARE(shareIndex.data(ShareModel::LinkShareLabelRole).toString(), _testLinkShareDefinition.linkShareLabel);
+ QCOMPARE(shareIndex.data(ShareModel::NoteEnabledRole).toBool(), !_testLinkShareDefinition.shareNote.isEmpty());
+ QCOMPARE(shareIndex.data(ShareModel::NoteRole).toString(), _testLinkShareDefinition.shareNote);
+ QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), !_testLinkShareDefinition.sharePassword.isEmpty());
+ // We don't expose the fetched password to the user as it's useless to them
+ QCOMPARE(shareIndex.data(ShareModel::PasswordRole).toString(), QString());
+ QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testLinkShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
+
+ const auto expectedLinkShareExpireDate = QDate::fromString(_testLinkShareDefinition.shareExpiration, helper.expectedDtFormat);
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), expectedLinkShareExpireDate.isValid());
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), expectedLinkShareExpireDate.startOfDay(Qt::UTC).toMSecsSinceEpoch());
+
+ const auto iconUrl = shareIndex.data(ShareModel::IconUrlRole).toString();
+ QVERIFY(iconUrl.contains("public.svg"));
+ }
+
+ void testModelEmailShareData()
+ {
+ helper.resetTestData();
+ // Test with a user/group email share "from the server"
+ helper.appendShareReplyData(_testEmailShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), 2); // Remember about placeholder link share
+
+ const auto shareIndex = model.index(0, 0, {}); // Placeholder link share gets added after we are done parsing fetched shares
+ QVERIFY(!shareIndex.data(Qt::DisplayRole).toString().isEmpty());
+ QCOMPARE(shareIndex.data(ShareModel::ShareTypeRole).toInt(), _testEmailShareDefinition.shareType);
+ QCOMPARE(shareIndex.data(ShareModel::ShareIdRole).toString(), _testEmailShareDefinition.shareId);
+ QCOMPARE(shareIndex.data(ShareModel::NoteEnabledRole).toBool(), !_testEmailShareDefinition.shareNote.isEmpty());
+ QCOMPARE(shareIndex.data(ShareModel::NoteRole).toString(), _testEmailShareDefinition.shareNote);
+ QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), !_testEmailShareDefinition.sharePassword.isEmpty());
+ // We don't expose the fetched password to the user as it's useless to them
+ QCOMPARE(shareIndex.data(ShareModel::PasswordRole).toString(), QString());
+ QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testEmailShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
+
+ const auto expectedShareExpireDate = QDate::fromString(_testEmailShareDefinition.shareExpiration, helper.expectedDtFormat);
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), expectedShareExpireDate.isValid());
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), expectedShareExpireDate.startOfDay(Qt::UTC).toMSecsSinceEpoch());
+
+ const auto iconUrl = shareIndex.data(ShareModel::IconUrlRole).toString();
+ QVERIFY(iconUrl.contains("email.svg"));
+ }
+
+ void testModelUserShareData()
+ {
+ helper.resetTestData();
+ // Test with a user/group user share "from the server"
+ helper.appendShareReplyData(_testUserShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), 2); // Remember about placeholder link share
+
+ const auto shareIndex = model.index(0, 0, {}); // Placeholder link share gets added after we are done parsing fetched shares
+ QVERIFY(!shareIndex.data(Qt::DisplayRole).toString().isEmpty());
+ QCOMPARE(shareIndex.data(ShareModel::ShareTypeRole).toInt(), _testUserShareDefinition.shareType);
+ QCOMPARE(shareIndex.data(ShareModel::ShareIdRole).toString(), _testUserShareDefinition.shareId);
+ QCOMPARE(shareIndex.data(ShareModel::NoteEnabledRole).toBool(), !_testUserShareDefinition.shareNote.isEmpty());
+ QCOMPARE(shareIndex.data(ShareModel::NoteRole).toString(), _testUserShareDefinition.shareNote);
+ QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), !_testUserShareDefinition.sharePassword.isEmpty());
+ // We don't expose the fetched password to the user as it's useless to them
+ QCOMPARE(shareIndex.data(ShareModel::PasswordRole).toString(), QString());
+ QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testUserShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
+
+ const auto expectedShareExpireDate = QDate::fromString(_testUserShareDefinition.shareExpiration, helper.expectedDtFormat);
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), expectedShareExpireDate.isValid());
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), expectedShareExpireDate.startOfDay(Qt::UTC).toMSecsSinceEpoch());
+
+ const auto iconUrl = shareIndex.data(ShareModel::IconUrlRole).toString();
+ QVERIFY(iconUrl.contains("user.svg"));
+
+ // Check correct user avatar
+ const auto avatarUrl = shareIndex.data(ShareModel::AvatarUrlRole).toString();
+ const auto relativeAvatarPath = QString("remote.php/dav/avatars/%1/%2.png").arg(_testUserShareDefinition.shareShareWith, QString::number(64));
+ const auto expectedAvatarPath = Utility::concatUrlPath(helper.account->url(), relativeAvatarPath).toString();
+ const QString expectedUrl(QStringLiteral("image://tray-image-provider/") + expectedAvatarPath);
+ QCOMPARE(avatarUrl, expectedUrl);
+ }
+
+ void testSuccessfulCreateShares()
+ {
+ helper.resetTestData();
+
+ // Test with an existing link share
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Test if it gets added
+ model.createNewLinkShare();
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 2); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Test if it's the type we wanted
+ const auto newLinkShareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(newLinkShareIndex.data(ShareModel::ShareTypeRole).toInt(), Share::TypeLink);
+
+ // Do it again with a different type
+ const ShareePtr sharee(new Sharee("testsharee@nextcloud.com", "Test sharee", Sharee::Type::Email));
+ model.createNewUserGroupShare(sharee);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 3); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Test if it's the type we wanted
+ const auto newUserGroupShareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(newUserGroupShareIndex.data(ShareModel::ShareTypeRole).toInt(), Share::TypeEmail);
+
+ // Confirm correct addition of share with password
+ const auto password = QStringLiteral("a pretty bad password but good thing it doesn't matter!");
+ model.createNewLinkShareWithPassword(password);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 4); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ model.createNewUserGroupShareWithPassword(sharee, password);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 5); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ helper.resetTestData();
+ }
+
+ void testEnforcePasswordShares()
+ {
+ helper.resetTestData();
+
+ // Enforce passwords for shares in capabilities
+ const QVariantMap enforcePasswordsCapabilities {
+ {QStringLiteral("files_sharing"), QVariantMap {
+ {QStringLiteral("api_enabled"), true},
+ {QStringLiteral("default_permissions"), 19},
+ {QStringLiteral("public"), QVariantMap {
+ {QStringLiteral("enabled"), true},
+ {QStringLiteral("expire_date"), QVariantMap {
+ {QStringLiteral("days"), 30},
+ {QStringLiteral("enforced"), false},
+ }},
+ {QStringLiteral("expire_date_internal"), QVariantMap {
+ {QStringLiteral("days"), 30},
+ {QStringLiteral("enforced"), false},
+ }},
+ {QStringLiteral("expire_date_remote"), QVariantMap {
+ {QStringLiteral("days"), 30},
+ {QStringLiteral("enforced"), false},
+ }},
+ {QStringLiteral("password"), QVariantMap {
+ {QStringLiteral("enforced"), true},
+ }},
+ }},
+ {QStringLiteral("sharebymail"), QVariantMap {
+ {QStringLiteral("enabled"), true},
+ {QStringLiteral("password"), QVariantMap {
+ {QStringLiteral("enforced"), true},
+ }},
+ }},
+ }},
+ };
+
+ helper.account->setCapabilities(enforcePasswordsCapabilities);
+ QVERIFY(helper.account->capabilities().sharePublicLinkEnforcePassword());
+ QVERIFY(helper.account->capabilities().shareEmailPasswordEnforced());
+
+ // Test with a link share "from the server"
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Confirm that the model requests a password
+ QSignalSpy requestPasswordForLinkShare(&model, &ShareModel::requestPasswordForLinkShare);
+ model.createNewLinkShare();
+ QVERIFY(requestPasswordForLinkShare.wait(3000));
+
+ QSignalSpy requestPasswordForEmailShare(&model, &ShareModel::requestPasswordForEmailSharee);
+ const ShareePtr sharee(new Sharee("testsharee@nextcloud.com", "Test sharee", Sharee::Type::Email));
+ model.createNewUserGroupShare(sharee);
+ QCOMPARE(requestPasswordForEmailShare.count(), 1);
+
+ // Test that the model data is correctly reporting that passwords are enforced
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(shareIndex.data(ShareModel::PasswordEnforcedRole).toBool(), true);
+ QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), true);
+ }
+
+ void testEnforceExpireDate()
+ {
+ helper.resetTestData();
+
+ const auto internalExpireDays = 45;
+ const auto publicExpireDays = 30;
+ const auto remoteExpireDays = 25;
+
+ // Enforce expire dates for shares in capabilities
+ const QVariantMap enforcePasswordsCapabilities {
+ {QStringLiteral("files_sharing"), QVariantMap {
+ {QStringLiteral("api_enabled"), true},
+ {QStringLiteral("default_permissions"), 19},
+ {QStringLiteral("public"), QVariantMap {
+ {QStringLiteral("enabled"), true},
+ {QStringLiteral("expire_date"), QVariantMap {
+ {QStringLiteral("days"), publicExpireDays},
+ {QStringLiteral("enforced"), true},
+ }},
+ {QStringLiteral("expire_date_internal"), QVariantMap {
+ {QStringLiteral("days"), internalExpireDays},
+ {QStringLiteral("enforced"), true},
+ }},
+ {QStringLiteral("expire_date_remote"), QVariantMap {
+ {QStringLiteral("days"), remoteExpireDays},
+ {QStringLiteral("enforced"), true},
+ }},
+ {QStringLiteral("password"), QVariantMap {
+ {QStringLiteral("enforced"), false},
+ }},
+ }},
+ {QStringLiteral("sharebymail"), QVariantMap {
+ {QStringLiteral("enabled"), true},
+ {QStringLiteral("password"), QVariantMap {
+ {QStringLiteral("enforced"), true},
+ }},
+ }},
+ }},
+ };
+
+ helper.account->setCapabilities(enforcePasswordsCapabilities);
+ QVERIFY(helper.account->capabilities().sharePublicLinkEnforceExpireDate());
+ QVERIFY(helper.account->capabilities().shareInternalEnforceExpireDate());
+ QVERIFY(helper.account->capabilities().shareRemoteEnforceExpireDate());
+
+ // Test with shares "from the server"
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ helper.appendShareReplyData(_testEmailShareDefinition);
+ helper.appendShareReplyData(_testRemoteShareDefinition);
+ QCOMPARE(helper.shareCount(), 3);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Test that the model data is correctly reporting that expire dates are enforced for all share types
+ for(auto i = 0; i < model.rowCount(); ++i) {
+ const auto shareIndex = model.index(i, 0, {});
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnforcedRole).toBool(), true);
+
+ QDateTime expectedExpireDateTime;
+ switch(shareIndex.data(ShareModel::ShareTypeRole).toInt()) {
+ case Share::TypePlaceholderLink:
+ break;
+ case Share::TypeUser:
+ case Share::TypeGroup:
+ case Share::TypeCircle:
+ case Share::TypeRoom:
+ expectedExpireDateTime = QDate::currentDate().addDays(internalExpireDays).startOfDay(QTimeZone::utc());
+ break;
+ case Share::TypeLink:
+ case Share::TypeEmail:
+ expectedExpireDateTime = QDate::currentDate().addDays(publicExpireDays).startOfDay(QTimeZone::utc());
+ break;
+ case Share::TypeRemote:
+ expectedExpireDateTime = QDate::currentDate().addDays(remoteExpireDays).startOfDay(QTimeZone::utc());
+ break;
+ }
+
+ QCOMPARE(shareIndex.data(ShareModel::EnforcedMaximumExpireDateRole).toLongLong(), expectedExpireDateTime.toMSecsSinceEpoch());
+ }
+ }
+
+ void testSuccessfulDeleteShares()
+ {
+ helper.resetTestData();
+
+ // Test with an existing link share
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Create share
+ model.createNewLinkShare();
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 2); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Test if it gets deleted properly
+ const auto latestLinkShare = model.index(model.rowCount() - 1, 0, {}).data(ShareModel::ShareRole).value<SharePtr>();
+ QSignalSpy shareDeleted(latestLinkShare.data(), &LinkShare::shareDeleted);
+ model.deleteShare(latestLinkShare);
+ QVERIFY(shareDeleted.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ helper.resetTestData();
+ }
+
+ void testPlaceholderLinkShare()
+ {
+ helper.resetTestData();
+
+ // Start with no shares; should show the placeholder link share
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0); // There should be no placeholder yet
+
+ QSignalSpy hasInitialShareFetchCompletedChanged(&model, &ShareModel::hasInitialShareFetchCompletedChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+ QVERIFY(hasInitialShareFetchCompletedChanged.wait(5000));
+ QVERIFY(model.hasInitialShareFetchCompleted());
+ QCOMPARE(model.rowCount(), 1); // There should be a placeholder now
+
+ const QPersistentModelIndex placeholderLinkShareIndex(model.index(model.rowCount() - 1, 0, {}));
+ QCOMPARE(placeholderLinkShareIndex.data(ShareModel::ShareTypeRole).toInt(), Share::TypePlaceholderLink);
+
+ // Test adding a user group share -- we should still be showing a placeholder link share
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+ const ShareePtr sharee(new Sharee("testsharee@nextcloud.com", "Test sharee", Sharee::Type::Email));
+ model.createNewUserGroupShare(sharee);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount() + 1);
+
+ QVERIFY(placeholderLinkShareIndex.isValid());
+ QCOMPARE(placeholderLinkShareIndex.data(ShareModel::ShareTypeRole).toInt(), Share::TypePlaceholderLink);
+
+ // Now try adding a link share, which should remove the placeholder
+ model.createNewLinkShare();
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 2); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ QVERIFY(!placeholderLinkShareIndex.isValid());
+
+ // Now delete the only link share, which should bring back the placeholder link share
+ const auto latestLinkShare = model.index(model.rowCount() - 1, 0, {}).data(ShareModel::ShareRole).value<SharePtr>();
+ QSignalSpy shareDeleted(latestLinkShare.data(), &LinkShare::shareDeleted);
+ model.deleteShare(latestLinkShare);
+ QVERIFY(shareDeleted.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount() + 1);
+
+ const auto newPlaceholderLinkShareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(newPlaceholderLinkShareIndex.data(ShareModel::ShareTypeRole).toInt(), Share::TypePlaceholderLink);
+
+ helper.resetTestData();
+ }
+
+ void testSuccessfulToggleAllowEditing()
+ {
+ helper.resetTestData();
+
+ // Test with an existing link share
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testLinkShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
+
+ const auto share = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ QSignalSpy permissionsSet(share.data(), &Share::permissionsSet);
+
+ model.toggleShareAllowEditing(share, false);
+ QVERIFY(permissionsSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), false);
+ }
+
+ void testSuccessfulPasswordSet()
+ {
+ helper.resetTestData();
+
+ // Test with an existing link share.
+ // This one has a pre-existing password
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), true);
+
+ const auto share = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ QSignalSpy passwordSet(share.data(), &Share::passwordSet);
+
+ model.toggleSharePasswordProtect(share, false);
+ QVERIFY(passwordSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), false);
+
+ const auto password = QStringLiteral("a pretty bad password but good thing it doesn't matter!");
+ model.setSharePassword(share, password);
+ QVERIFY(passwordSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), true);
+ // The model stores the recently set password.
+ // We want to present the user with it in the UI while the model is alive
+ QCOMPARE(shareIndex.data(ShareModel::PasswordRole).toString(), password);
+ }
+
+ void testSuccessfulExpireDateSet()
+ {
+ helper.resetTestData();
+
+ // Test with an existing link share.
+ // This one has a pre-existing expire date
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Check what we know
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), true);
+
+ // Disable expire date
+ const auto sharePtr = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ const auto linkSharePtr = sharePtr.dynamicCast<LinkShare>(); // Need to connect to signal
+ QSignalSpy expireDateSet(linkSharePtr.data(), &LinkShare::expireDateSet);
+ model.toggleShareExpirationDate(sharePtr, false);
+
+ QVERIFY(expireDateSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), false);
+
+ // Set a new expire date
+ const auto expireDateMsecs = QDate::currentDate().addDays(10).startOfDay(Qt::UTC).toMSecsSinceEpoch();
+ model.setShareExpireDate(linkSharePtr, expireDateMsecs);
+ QVERIFY(expireDateSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), expireDateMsecs);
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), true);
+
+ // Test the QML-specific slot
+ const QVariant newExpireDateMsecs = QDate::currentDate().addDays(20).startOfDay(Qt::UTC).toMSecsSinceEpoch();
+ model.setShareExpireDateFromQml(QVariant::fromValue(sharePtr), newExpireDateMsecs);
+ QVERIFY(expireDateSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), newExpireDateMsecs);
+ QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), true);
+ }
+
+ void testSuccessfulNoteSet()
+ {
+ helper.resetTestData();
+
+ // Test with an existing link share.
+ // This one has a pre-existing password
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(shareIndex.data(ShareModel::NoteEnabledRole).toBool(), true);
+
+ const auto sharePtr = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ const auto linkSharePtr = sharePtr.dynamicCast<LinkShare>(); // Need to connect to signal
+ QSignalSpy noteSet(linkSharePtr.data(), &LinkShare::noteSet);
+
+ model.toggleShareNoteToRecipient(sharePtr, false);
+ QVERIFY(noteSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::NoteEnabledRole).toBool(), false);
+
+ const auto note = QStringLiteral("Don't forget to test everything!");
+ model.setShareNote(sharePtr, note);
+ QVERIFY(noteSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::NoteEnabledRole).toBool(), true);
+ // The model stores the recently set password.
+ // We want to present the user with it in the UI while the model is alive
+ QCOMPARE(shareIndex.data(ShareModel::NoteRole).toString(), note);
+ }
+
+ void testSuccessfulLinkShareLabelSet()
+ {
+ helper.resetTestData();
+
+ // Test with an existing link share.
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ QCOMPARE(shareIndex.data(ShareModel::LinkShareLabelRole).toBool(), true);
+
+ const auto sharePtr = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ const auto linkSharePtr = sharePtr.dynamicCast<LinkShare>(); // Need to connect to signal
+ QSignalSpy labelSet(linkSharePtr.data(), &LinkShare::labelSet);
+ const auto label = QStringLiteral("New link share label!");
+ model.setLinkShareLabel(linkSharePtr, label);
+ QVERIFY(labelSet.wait(3000));
+ QCOMPARE(shareIndex.data(ShareModel::LinkShareLabelRole).toString(), label);
+ }
+
+ void testSharees()
+ {
+ helper.resetTestData();
+
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ helper.appendShareReplyData(_testEmailShareDefinition);
+ helper.appendShareReplyData(_testUserShareDefinition);
+ QCOMPARE(helper.shareCount(), 3);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ QCOMPARE(model.sharees().count(), 2); // Link shares don't have sharees
+
+ // Test adding a user group share -- we should still be showing a placeholder link share
+ const ShareePtr sharee(new Sharee("testsharee@nextcloud.com", "Test sharee", Sharee::Type::Email));
+ model.createNewUserGroupShare(sharee);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 4); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ const auto sharees = model.sharees();
+ QCOMPARE(sharees.count(), 3); // Link shares don't have sharees
+ const auto lastSharee = sharees.last().value<ShareePtr>();
+ QVERIFY(lastSharee);
+
+ // Remove the user group share we just added
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ const auto sharePtr = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ model.deleteShare(sharePtr);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Now check the sharee is gone
+ QCOMPARE(model.sharees().count(), 2);
+ }
+
+ void testSharePropertySetError()
+ {
+ helper.resetTestData();
+
+ // Serve a broken share definition from the server to force an error
+ auto brokenLinkShareDefinition = _testLinkShareDefinition;
+ brokenLinkShareDefinition.shareId = QString();
+
+ helper.appendShareReplyData(brokenLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ ShareModel model;
+ QAbstractItemModelTester modelTester(&model);
+ QCOMPARE(model.rowCount(), 0);
+
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ // Reset the fake server to pretend like nothing is wrong there
+ helper.resetTestShares();
+ helper.appendShareReplyData(_testLinkShareDefinition);
+ QCOMPARE(helper.shareCount(), 1);
+
+ // Now try changing a property of the share
+ const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
+ const auto share = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
+ QSignalSpy serverError(&model, &ShareModel::serverError);
+
+ model.toggleShareAllowEditing(share, false);
+ QVERIFY(serverError.wait(3000));
+
+ // Specific signal for password set error
+ QSignalSpy passwordSetError(&model, &ShareModel::passwordSetError);
+ const auto password = QStringLiteral("a pretty bad password but good thing it doesn't matter!");
+ model.setSharePassword(share, password);
+ QVERIFY(passwordSetError.wait(3000));
+ }
+
+};
+
+QTEST_MAIN(TestShareModel)
+#include "testsharemodel.moc"
diff --git a/test/testsortedsharemodel.cpp b/test/testsortedsharemodel.cpp
new file mode 100644
index 000000000..5619d7bb5
--- /dev/null
+++ b/test/testsortedsharemodel.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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/filedetails/sortedsharemodel.h"
+
+#include <QTest>
+#include <QAbstractItemModelTester>
+#include <QSignalSpy>
+
+#include "sharetestutils.h"
+
+using namespace OCC;
+
+class TestSortedShareModel : public QObject
+{
+ Q_OBJECT
+
+public slots:
+ void addAllTestShares()
+ {
+ // Let's insert them in the opposite order we want from the model
+ for (auto it = _expectedOrder.crbegin(); it != _expectedOrder.crend(); ++it) {
+ helper.appendShareReplyData(*it);
+ }
+ }
+
+private:
+ ShareTestHelper helper;
+
+ FakeShareDefinition _userADefinition;
+ FakeShareDefinition _userBDefinition;
+ FakeShareDefinition _groupADefinition;
+ FakeShareDefinition _groupBDefinition;
+ FakeShareDefinition _linkADefinition;
+ FakeShareDefinition _linkBDefinition;
+ FakeShareDefinition _emailADefinition;
+ FakeShareDefinition _emailBDefinition;
+ FakeShareDefinition _remoteADefinition;
+ FakeShareDefinition _remoteBDefinition;
+ FakeShareDefinition _roomADefinition;
+ FakeShareDefinition _roomBDefinition;
+
+ QVector<FakeShareDefinition> _expectedOrder;
+
+ static constexpr auto _expectedShareCount = 12;
+
+private slots:
+ void initTestCase()
+ {
+ QSignalSpy helperSetupSucceeded(&helper, &ShareTestHelper::setupSucceeded);
+ helper.setup();
+ QCOMPARE(helperSetupSucceeded.count(), 1);
+
+ const auto userAShareWith = QStringLiteral("user_a");
+ const auto userAShareWithDisplayName = QStringLiteral("User A");
+ _userADefinition = FakeShareDefinition(&helper, Share::TypeUser, userAShareWith, userAShareWithDisplayName);
+
+ const auto userBShareWith = QStringLiteral("user_b");
+ const auto userBShareWithDisplayName = QStringLiteral("User B");
+ _userBDefinition = FakeShareDefinition(&helper, Share::TypeUser, userBShareWith, userBShareWithDisplayName);
+
+ const auto groupAShareWith = QStringLiteral("group_a");
+ const auto groupAShareWithDisplayName = QStringLiteral("Group A");
+ _groupADefinition = FakeShareDefinition(&helper, Share::TypeGroup, groupAShareWith, groupAShareWithDisplayName);
+
+ const auto groupBShareWith = QStringLiteral("group_b");
+ const auto groupBShareWithDisplayName = QStringLiteral("Group B");
+ _groupBDefinition = FakeShareDefinition(&helper, Share::TypeGroup, groupBShareWith, groupBShareWithDisplayName);
+
+ const auto linkALabel = QStringLiteral("Link share label A");
+ _linkADefinition = FakeShareDefinition(&helper, Share::TypeLink, {}, linkALabel);
+
+ const auto linkBLabel = QStringLiteral("Link share label B");
+ _linkBDefinition = FakeShareDefinition(&helper, Share::TypeLink, {}, linkBLabel);
+
+ const auto emailAShareWith = QStringLiteral("email_a@nextcloud.com");
+ const auto emailAShareWithDisplayName = QStringLiteral("email_a@nextcloud.com");
+ _emailADefinition = FakeShareDefinition(&helper, Share::TypeEmail, emailAShareWith, emailAShareWithDisplayName);
+
+ const auto emailBShareWith = QStringLiteral("email_b@nextcloud.com");
+ const auto emailBShareWithDisplayName = QStringLiteral("email_b@nextcloud.com");
+ _emailBDefinition = FakeShareDefinition(&helper, Share::TypeEmail, emailBShareWith, emailBShareWithDisplayName);
+
+ const auto remoteAShareWith = QStringLiteral("remote_a");
+ const auto remoteAShareWithDisplayName = QStringLiteral("Remote share A");
+ _remoteADefinition = FakeShareDefinition(&helper, Share::TypeRemote, remoteAShareWith, remoteAShareWithDisplayName);
+
+ const auto remoteBShareWith = QStringLiteral("remote_b");
+ const auto remoteBShareWithDisplayName = QStringLiteral("Remote share B");
+ _remoteBDefinition = FakeShareDefinition(&helper, Share::TypeRemote, remoteBShareWith, remoteBShareWithDisplayName);
+
+ const auto roomAShareWith = QStringLiteral("room_a");
+ const auto roomAShareWithDisplayName = QStringLiteral("Room A");
+ _roomADefinition = FakeShareDefinition(&helper, Share::TypeRoom, roomAShareWith, roomAShareWithDisplayName);
+
+ const auto roomBShareWith = QStringLiteral("room_b");
+ const auto roomBShareWithDisplayName = QStringLiteral("Room B");
+ _roomBDefinition = FakeShareDefinition(&helper, Share::TypeRoom, roomBShareWith, roomBShareWithDisplayName);
+
+ _expectedOrder = {// Placeholder link shares always go first, followed by normal link shares.
+ _linkADefinition,
+ _linkBDefinition,
+ // For all other share types, we follow the Share::ShareType enum.
+ _userADefinition,
+ _userBDefinition,
+ _groupADefinition,
+ _groupBDefinition,
+ _emailADefinition,
+ _emailBDefinition,
+ _remoteADefinition,
+ _remoteBDefinition,
+ _roomADefinition,
+ _roomBDefinition};
+ }
+
+ void testSetModel()
+ {
+ helper.resetTestData();
+ addAllTestShares();
+ QCOMPARE(helper.shareCount(), _expectedShareCount);
+
+ ShareModel model;
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ SortedShareModel sortedModel;
+ QAbstractItemModelTester sortedModelTester(&sortedModel);
+ QSignalSpy sortedModelReset(&sortedModel, &SortedShareModel::modelReset);
+ QSignalSpy shareModelChanged(&sortedModel, &SortedShareModel::shareModelChanged);
+
+ sortedModel.setShareModel(&model);
+ QCOMPARE(shareModelChanged.count(), 1);
+ QCOMPARE(sortedModelReset.count(), 1);
+ QCOMPARE(sortedModel.rowCount(), model.rowCount());
+ QCOMPARE(sortedModel.shareModel(), &model);
+ }
+
+ void testCorrectSort()
+ {
+ helper.resetTestData();
+ addAllTestShares();
+ QCOMPARE(helper.shareCount(), _expectedShareCount);
+
+ ShareModel model;
+ QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
+ model.setAccountState(helper.accountState.data());
+ model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
+ QVERIFY(sharesChanged.wait(5000));
+ QCOMPARE(model.rowCount(), helper.shareCount());
+
+ SortedShareModel sortedModel;
+ QAbstractItemModelTester sortedModelTester(&sortedModel);
+ QSignalSpy sortedModelReset(&sortedModel, &SortedShareModel::modelReset);
+
+ sortedModel.setShareModel(&model);
+ QCOMPARE(sortedModelReset.count(), 1);
+ QCOMPARE(sortedModel.rowCount(), model.rowCount());
+
+ for(auto i = 0; i < sortedModel.rowCount(); ++i) {
+ const auto shareIndex = sortedModel.index(i, 0);
+ const auto expectedShareDefinition = _expectedOrder.at(i);
+
+ QCOMPARE(shareIndex.data(ShareModel::ShareTypeRole).toInt(), expectedShareDefinition.shareType);
+ QCOMPARE(shareIndex.data(ShareModel::ShareIdRole).toString(), expectedShareDefinition.shareId);
+ }
+ }
+
+};
+
+QTEST_MAIN(TestSortedShareModel)
+#include "testsortedsharemodel.moc"
diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml
index 9214c17ea..cba2ee9f3 100644
--- a/theme/Style/Style.qml
+++ b/theme/Style/Style.qml
@@ -51,6 +51,8 @@ QtObject {
property int standardSpacing: 10
property int smallSpacing: 5
+ property int iconButtonWidth: 36
+
property int minActivityHeight: variableSize(40)
property int currentAccountButtonWidth: 220