Explorar o código

Display 'Search globally' as the last sharees list element

Signed-off-by: alex-z <blackslayer4@gmail.com>
alex-z %!s(int64=3) %!d(string=hai) anos
pai
achega
31de652d9b

+ 1 - 0
src/gui/filedetails/ShareView.qml

@@ -152,6 +152,7 @@ ColumnLayout {
     }
 
     ShareeSearchField {
+        id: shareeSearchField
         Layout.fillWidth: true
         Layout.leftMargin: root.horizontalPadding
         Layout.rightMargin: root.horizontalPadding

+ 46 - 0
src/gui/filedetails/ShareeDelegate.qml

@@ -20,8 +20,54 @@ import QtQuick.Controls 2.15
 import com.nextcloud.desktopclient 1.0
 import Style 1.0
 
+import "../tray"
+
 ItemDelegate {
     id: root
 
     text: model.display
+
+    contentItem: RowLayout {
+        height: visible ? implicitHeight : 0
+
+        Loader {
+              id: shareeIconLoader
+
+              Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+
+              active: model.icon !== ""
+
+              sourceComponent: Image {
+                  id: shareeIcon
+
+                  horizontalAlignment: Qt.AlignLeft
+                  verticalAlignment: Qt.AlignVCenter
+
+                  width: height
+                  height: shareeLabel.height
+
+                  smooth: true
+                  antialiasing: true
+                  mipmap: true
+                  fillMode: Image.PreserveAspectFit
+
+                  source: model.icon
+
+                  sourceSize: Qt.size(shareeIcon.height * 1.0, shareeIcon.height * 1.0)
+              }
+        }
+
+        EnforcedPlainTextLabel {
+            id: shareeLabel
+            Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth
+            Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+
+            Layout.fillWidth: true
+
+            horizontalAlignment: Text.AlignLeft
+            verticalAlignment: Text.AlignVCenter
+            text: model.display
+            color: Style.ncTextColor
+        }
+    }
 }

+ 15 - 3
src/gui/filedetails/ShareeSearchField.qml

@@ -41,7 +41,7 @@ TextField {
     readonly property double iconsScaleFactor: 0.6
 
     function triggerSuggestionsVisibility() {
-        shareeListView.count > 0 && text !== "" ? suggestionsPopup.open() : suggestionsPopup.close();
+        shareeListView.count > 0 ? suggestionsPopup.open() : suggestionsPopup.close();
     }
 
     placeholderText: qsTr("Search for users or groups…")
@@ -73,7 +73,7 @@ TextField {
             case Qt.Key_Enter:
             case Qt.Key_Return:
                 if(shareeListView.currentIndex > -1) {
-                    shareeListView.itemAtIndex(shareeListView.currentIndex).selectSharee();
+                    shareeListView.itemAtIndex(shareeListView.currentIndex).selectItem();
                     event.accepted = true;
                     break;
                 }
@@ -219,6 +219,9 @@ TextField {
                     anchors.left: parent.left
                     anchors.right: parent.right
 
+                    enabled: model.type !== Sharee.LookupServerSearchResults
+                    hoverEnabled: model.type !== Sharee.LookupServerSearchResults
+
                     function selectSharee() {
                         root.shareeSelected(model.sharee);
                         suggestionsPopup.close();
@@ -226,6 +229,15 @@ TextField {
                         root.clear();
                     }
 
+                    function selectItem() {
+                        if (model.type === Sharee.LookupServerSearch) {
+                            shareeListView.currentIndex = -1
+                            root.shareeModel.searchGlobally()
+                        } else {
+                            selectSharee()
+                        }
+                    }
+
                     onHoveredChanged: if (hovered) {
                         // When we set the currentIndex the list view will scroll...
                         // unless we tamper with the preferred highlight points to stop this.
@@ -241,7 +253,7 @@ TextField {
                         shareeListView.preferredHighlightBegin = savedPreferredHighlightBegin;
                         shareeListView.preferredHighlightEnd = savedPreferredHighlightEnd;
                     }
-                    onClicked: selectSharee()
+                    onClicked: selectItem()
                 }
             }
         }

+ 64 - 1
src/gui/filedetails/shareemodel.cpp

@@ -19,6 +19,7 @@
 #include <QJsonArray>
 
 #include "ocsshareejob.h"
+#include "theme.h"
 
 namespace OCC {
 
@@ -29,7 +30,10 @@ ShareeModel::ShareeModel(QObject *parent)
 {
     _searchRateLimitingTimer.setSingleShot(true);
     _searchRateLimitingTimer.setInterval(500);
+    _searchGloballyPlaceholder.reset(new Sharee({}, tr("Search globally"), Sharee::LookupServerSearch, QStringLiteral("magnifying-glass.svg")));
+    _searchGloballyPlaceholder->setIsIconColourful(true);
     connect(&_searchRateLimitingTimer, &QTimer::timeout, this, &ShareeModel::fetch);
+    connect(Theme::instance(), &Theme::darkModeChanged, this, &ShareeModel::slotDarkModeChanged);
 }
 
 // ---------------------- QAbstractListModel methods ---------------------- //
@@ -48,6 +52,8 @@ QHash<int, QByteArray> ShareeModel::roleNames() const
     auto roles = QAbstractListModel::roleNames();
     roles[ShareeRole] = "sharee";
     roles[AutoCompleterStringMatchRole] = "autoCompleterStringMatch";
+    roles[TypeRole] = "type";
+    roles[IconRole] = "icon";
 
     return roles;
 }
@@ -68,6 +74,10 @@ QVariant ShareeModel::data(const QModelIndex &index, const int role) const
     case AutoCompleterStringMatchRole:
         // Don't show this to the user
         return QString(sharee->displayName() + " (" + sharee->shareWith() + ")");
+    case IconRole:
+        return sharee->iconUrlColoured();
+    case TypeRole:
+        return sharee->type();
     case ShareeRole:
         return QVariant::fromValue(sharee);
     }
@@ -119,6 +129,12 @@ void ShareeModel::setSearchString(const QString &searchString)
         return;
     }
 
+    beginResetModel();
+    _sharees.clear();
+    endResetModel();
+
+    Q_EMIT shareesReady();
+
     _searchString = searchString;
     Q_EMIT searchStringChanged();
 
@@ -165,16 +181,28 @@ void ShareeModel::setShareeBlocklist(const QVariantList shareeBlocklist)
     filterSharees();
 }
 
+void ShareeModel::searchGlobally()
+{
+    setLookupMode(ShareeModel::LookupMode::GlobalSearch);
+    beginResetModel();
+    _sharees.clear();
+    endResetModel();
+
+    Q_EMIT shareesReady();
+    fetch();
+}
+
 // ------------------------- Internal data methods ------------------------- //
 
 void ShareeModel::fetch()
 {
-    if(!_accountState || !_accountState->account() || _searchString.isEmpty()) {
+    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");
@@ -233,9 +261,35 @@ void ShareeModel::shareesFetched(const QJsonDocument &reply)
 
     beginResetModel();
     _sharees = newSharees;
+    insertSearchGloballyItem(newSharees);
     endResetModel();
 
     Q_EMIT shareesReady();
+
+    setLookupMode(LookupMode::LocalSearch);
+}
+
+void ShareeModel::insertSearchGloballyItem(const QVector<ShareePtr> &newShareesFetched)
+{
+    const auto foundIt = std::find_if(std::begin(_sharees), std::end(_sharees), [](const ShareePtr &sharee) {
+        return sharee->type() == Sharee::LookupServerSearch || sharee->type() == Sharee::LookupServerSearchResults;
+    });
+
+    // remove it if it somehow appeared not at the end, to avoid writing complex proxy models for sorting
+    if (foundIt != std::end(_sharees) && (foundIt + 1) != std::end(_sharees)) {
+        _sharees.erase(foundIt);
+    }
+
+    _sharees.push_back(_searchGloballyPlaceholder);
+
+    if (lookupMode() == LookupMode::GlobalSearch) {
+        const auto displayName = newShareesFetched.isEmpty() ? tr("No results found") : tr("Global search results");
+        _searchGloballyPlaceholder->setDisplayName(displayName);
+        _searchGloballyPlaceholder->setType(Sharee::LookupServerSearchResults);
+    } else {
+        _searchGloballyPlaceholder->setDisplayName(tr("Search globally"));
+        _searchGloballyPlaceholder->setType(Sharee::LookupServerSearch);
+    }
 }
 
 ShareePtr ShareeModel::parseSharee(const QJsonObject &data) const
@@ -275,4 +329,13 @@ void ShareeModel::filterSharees()
     Q_EMIT shareesReady();
 }
 
+void ShareeModel::slotDarkModeChanged()
+{
+    for (int i = 0; i < _sharees.size(); ++i) {
+        if (_sharees[i]->updateIconUrl()) {
+            Q_EMIT dataChanged(index(i), index(i), {IconRole});
+        }
+    }
+}
+
 }

+ 7 - 0
src/gui/filedetails/shareemodel.h

@@ -45,6 +45,8 @@ public:
     enum Roles {
         ShareeRole = Qt::UserRole + 1,
         AutoCompleterStringMatchRole,
+        TypeRole,
+        IconRole,
     };
     Q_ENUM(Roles);
 
@@ -80,12 +82,15 @@ public slots:
     void setSearchString(const QString &searchString);
     void setLookupMode(const OCC::ShareeModel::LookupMode lookupMode);
     void setShareeBlocklist(const QVariantList shareeBlocklist);
+    void searchGlobally();
 
     void fetch();
 
 private slots:
     void shareesFetched(const QJsonDocument &reply);
+    void insertSearchGloballyItem(const QVector<ShareePtr> &newShareesFetched);
     void filterSharees();
+    void slotDarkModeChanged();
 
 private:
     [[nodiscard]] ShareePtr parseSharee(const QJsonObject &data) const;
@@ -100,6 +105,8 @@ private:
 
     QVector<ShareePtr> _sharees;
     QVector<ShareePtr> _shareeBlocklist;
+
+    ShareePtr _searchGloballyPlaceholder;
 };
 
 }

+ 2 - 0
src/gui/owncloudgui.cpp

@@ -128,6 +128,7 @@ ownCloudGui::ownCloudGui(Application *parent)
 
     qmlRegisterUncreatableType<UnifiedSearchResultsListModel>("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
     qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
+    qmlRegisterUncreatableType<Sharee>("com.nextcloud.desktopclient", 1, 0, "Sharee", "Access to Type enum");
 
     qRegisterMetaTypeStreamOperators<Emoji>();
 
@@ -136,6 +137,7 @@ ownCloudGui::ownCloudGui(Application *parent)
     qRegisterMetaType<UserStatus>("UserStatus");
     qRegisterMetaType<SharePtr>("SharePtr");
     qRegisterMetaType<ShareePtr>("ShareePtr");
+    qRegisterMetaType<Sharee>("Sharee");
 
     qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserModel", UserModel::instance());
     qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserAppsModel", UserAppsModel::instance());

+ 64 - 5
src/gui/sharee.cpp

@@ -14,22 +14,29 @@
 
 #include "sharee.h"
 #include "ocsshareejob.h"
+#include "theme.h"
 
 #include <QJsonObject>
 #include <QJsonDocument>
 #include <QJsonArray>
 
-namespace OCC {
-
+namespace OCC
+{
 Q_LOGGING_CATEGORY(lcSharing, "nextcloud.gui.sharing", QtInfoMsg)
 
-Sharee::Sharee(const QString shareWith,
-    const QString displayName,
-    const Type type)
+Sharee::Sharee(const QString &shareWith, const QString &displayName, const Type type, const QString &iconUrl)
     : _shareWith(shareWith)
     , _displayName(displayName)
     , _type(type)
+    , _iconUrl(iconUrl)
 {
+    if (!_iconUrl.isEmpty()) {
+        // make sure no color path is contained in the url
+        _iconUrl.replace(QStringLiteral("/black"), "");
+        _iconUrl.replace(QStringLiteral("/white"), "");
+        _iconColor = Theme::instance()->darkMode() ? QStringLiteral("white") : QStringLiteral("black");
+    }
+    updateIconUrl();
 }
 
 QString Sharee::format() const
@@ -61,9 +68,61 @@ QString Sharee::displayName() const
     return _displayName;
 }
 
+void Sharee::setDisplayName(const QString &displayName)
+{
+    if (displayName != _displayName) {
+        _displayName = displayName;
+    }
+}
+
+void Sharee::setIconUrl(const QString &iconUrl)
+{
+    if (iconUrl != _iconUrl) {
+        _iconUrl = iconUrl;
+    }
+}
+
+void Sharee::setType(const Type &type)
+{
+    if (type != _type) {
+        _type = type;
+    }
+}
+
+void Sharee::setIsIconColourful(const bool isColourful)
+{
+    if (_isIconColourful != isColourful) {
+        _isIconColourful = isColourful;
+        updateIconUrl();
+    }
+}
+
+bool Sharee::updateIconUrl()
+{
+    if (_iconUrl.isEmpty() || !_isIconColourful) {
+        return false;
+    }
+
+    const auto iconUrlColoured = _iconUrlColoured;
+    _iconColor = (!_isIconColourful || !Theme::instance()->darkMode()) ? QStringLiteral("black") : QStringLiteral("white");
+    _iconUrlColoured = QStringLiteral("image://svgimage-custom-color/") + _iconUrl + QStringLiteral("/") + _iconColor;
+
+    return iconUrlColoured != _iconUrlColoured;
+}
+
 Sharee::Type Sharee::type() const
 {
     return _type;
 }
 
+QString Sharee::iconUrl() const
+{
+    return _iconUrl;
+}
+
+QString Sharee::iconUrlColoured() const
+{
+    return _iconUrlColoured;
+}
+
 }

+ 25 - 13
src/gui/sharee.h

@@ -35,35 +35,47 @@ Q_DECLARE_LOGGING_CATEGORY(lcSharing)
 
 class Sharee
 {
+    Q_GADGET
+    Q_PROPERTY(QString format READ format)
+    Q_PROPERTY(QString shareWith MEMBER _shareWith)
+    Q_PROPERTY(QString displayName MEMBER _displayName)
+    Q_PROPERTY(QString iconUrlColoured MEMBER _iconUrlColoured)
+    Q_PROPERTY(Type type MEMBER _type)
+
 public:
     // Keep in sync with Share::ShareType
-    enum Type {
-        User = 0,
-        Group = 1,
-        Email = 4,
-        Federated = 6,
-        Circle = 7,
-        Room = 10
-    };
-
-    explicit Sharee(const QString shareWith,
-        const QString displayName,
-        const Type type);
+    enum Type { Invalid = -1, User = 0, Group = 1, Email = 4, Federated = 6, Circle = 7, Room = 10, LookupServerSearch = 999, LookupServerSearchResults = 1000 };
+    Q_ENUM(Type);
+    explicit Sharee() = default;
+    explicit Sharee(const QString &shareWith, const QString &displayName, const Type type, const QString &iconUrl = {});
 
     [[nodiscard]] QString format() const;
     [[nodiscard]] QString shareWith() const;
     [[nodiscard]] QString displayName() const;
+    [[nodiscard]] QString iconUrl() const;
+    [[nodiscard]] QString iconUrlColoured() const;
     [[nodiscard]] Type type() const;
+    bool updateIconUrl();
+
+    void setDisplayName(const QString &displayName);
+    void setType(const Type &type);
+    void setIsIconColourful(const bool isColourful);
+    void setIconUrl(const QString &iconUrl);
 
 private:
     QString _shareWith;
     QString _displayName;
-    Type _type;
+    QString _iconUrlColoured;
+    QString _iconColor;
+    Type _type = Type::Invalid;
+    QString _iconUrl;
+    bool _isIconColourful = false;
 };
 
 using ShareePtr = QSharedPointer<OCC::Sharee>;
 }
 
 Q_DECLARE_METATYPE(OCC::ShareePtr)
+Q_DECLARE_METATYPE(OCC::Sharee)
 
 #endif //SHAREE_H

+ 65 - 5
test/testshareemodel.cpp

@@ -33,6 +33,8 @@ class TestShareeModel : public QObject
 {
     Q_OBJECT
 
+    int _numLookupSearchParamSet = 0;
+
 public:
     ~TestShareeModel() override
     {
@@ -62,6 +64,9 @@ public:
 
         QString category;
         switch(definition.type) {
+        case Sharee::Invalid:
+            category = QStringLiteral("invalid");
+            break;
         case Sharee::Circle:
             category = QStringLiteral("circles");
             break;
@@ -80,6 +85,12 @@ public:
         case Sharee::User:
             category = QStringLiteral("users");
             break;
+        case Sharee::LookupServerSearch:
+            category = QStringLiteral("placeholder_lookupserversearch");
+            break;
+        case Sharee::LookupServerSearchResults:
+            category = QStringLiteral("placeholder_lookupserversearchresults");
+            break;
         }
 
         auto shareesInCategory = _shareesMap.value(category).toJsonArray();
@@ -244,6 +255,10 @@ private slots:
                 const auto lookupParam = urlQuery.queryItemValue(QStringLiteral("lookup"));
                 const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
 
+                if (!lookupParam.isEmpty() && lookupParam == QStringLiteral("true")) {
+                    ++_numLookupSearchParamSet;
+                }
+
                 if (formatParam != QStringLiteral("json")) {
                     reply = new FakeErrorReply(op, req, this, 400, fake400Response);
                 } else {
@@ -324,12 +339,51 @@ private slots:
         const auto searchString = QStringLiteral("i");
         model.setSearchString(searchString);
         QVERIFY(shareesReady.wait(3000));
-        QCOMPARE(model.rowCount(), shareesCount(searchString));
+        QCOMPARE(model.rowCount(), shareesCount(searchString) + 1);
+        QVERIFY(model.rowCount() > 0);
+        auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
+        QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
 
         const auto emailSearchString = QStringLiteral("email");
         model.setSearchString(emailSearchString);
         QVERIFY(shareesReady.wait(3000));
-        QCOMPARE(model.rowCount(), shareesCount(emailSearchString));
+        QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
+        QVERIFY(model.rowCount() > 0);
+        lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
+        QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
+    }
+
+    void testShareesFetchGlobally()
+    {
+        resetTestData();
+        standardReplyPopulate();
+
+        ShareeModel model;
+        QAbstractItemModelTester modelTester(&model);
+        QCOMPARE(model.rowCount(), 0);
+
+        model.setAccountState(_accountState.data());
+
+        QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
+        const auto emailSearchString = QStringLiteral("email");
+        model.setSearchString(emailSearchString);
+        QVERIFY(shareesReady.wait(3000));
+        QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
+        QVERIFY(model.rowCount() > 0);
+        auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
+        QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
+        QCOMPARE(_numLookupSearchParamSet, 0);
+
+        QSignalSpy lookupModeChanged(&model, &ShareeModel::lookupModeChanged);
+        model.searchGlobally();
+        QVERIFY(shareesReady.wait(3000));
+        QCOMPARE(lookupModeChanged.count(), 2);
+        QVERIFY(model.lookupMode() == ShareeModel::LookupMode::LocalSearch);
+        QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
+        QVERIFY(model.rowCount() > 0);
+        lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
+        QVERIFY(lastElementType == Sharee::Type::LookupServerSearchResults);
+        QCOMPARE(_numLookupSearchParamSet, 1);
     }
 
     void testFetchSignalling()
@@ -367,7 +421,9 @@ private slots:
 
         QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
         QVERIFY(shareesReady.wait(3000));
-        QCOMPARE(model.rowCount(), shareesCount(searchString));
+        QCOMPARE(model.rowCount(), shareesCount(searchString) + 1);
+        auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
+        QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
 
         const auto shareeIndex = model.index(0, 0, {});
 
@@ -409,12 +465,16 @@ private slots:
         const auto searchString = QStringLiteral("i");
         model.setSearchString(searchString);
         QVERIFY(shareesReady.wait(3000));
-        QCOMPARE(model.rowCount(), shareesCount(searchString) - 1);
+        QCOMPARE(model.rowCount(), shareesCount(searchString) - 1 + 1);
+        auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
+        QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
 
         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);
+        QCOMPARE(model.rowCount(), shareesCount(searchString) - 2 + 1);
+        lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
+        QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
     }
 
     void testServerError()