diff --git a/resources.qrc b/resources.qrc
index 134c11cc91..43b277f77a 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -15,5 +15,13 @@
src/gui/tray/AutoSizingMenu.qml
src/gui/tray/ActivityList.qml
src/gui/tray/FileActivityDialog.qml
+ src/gui/tray/UnifiedSearchInputContainer.qml
+ src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml
+ src/gui/tray/UnifiedSearchResultItem.qml
+ src/gui/tray/UnifiedSearchResultItemSkeleton.qml
+ src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml
+ src/gui/tray/UnifiedSearchResultListItem.qml
+ src/gui/tray/UnifiedSearchResultNothingFound.qml
+ src/gui/tray/UnifiedSearchResultSectionItem.qml
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index bbc6663749..3c2d790eaf 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -43,6 +43,14 @@ set(client_UI_SRCS
tray/ActivityList.qml
tray/Window.qml
tray/UserLine.qml
+ tray/UnifiedSearchInputContainer.qml
+ tray/UnifiedSearchResultFetchMoreTrigger.qml
+ tray/UnifiedSearchResultItem.qml
+ tray/UnifiedSearchResultItemSkeleton.qml
+ tray/UnifiedSearchResultItemSkeletonContainer.qml
+ tray/UnifiedSearchResultListItem.qml
+ tray/UnifiedSearchResultNothingFound.qml
+ tray/UnifiedSearchResultSectionItem.qml
wizard/flow2authwidget.ui
wizard/owncloudadvancedsetuppage.ui
wizard/owncloudconnectionmethoddialog.ui
@@ -116,6 +124,9 @@ set(client_SRCS
tray/syncstatussummary.cpp
tray/ActivityData.cpp
tray/ActivityListModel.cpp
+ tray/unifiedsearchresult.cpp
+ tray/unifiedsearchresultimageprovider.cpp
+ tray/unifiedsearchresultslistmodel.cpp
tray/UserModel.cpp
tray/NotificationHandler.cpp
tray/NotificationCache.cpp
diff --git a/src/gui/ErrorBox.qml b/src/gui/ErrorBox.qml
index 5b0d102ad7..8ef5d053c8 100644
--- a/src/gui/ErrorBox.qml
+++ b/src/gui/ErrorBox.qml
@@ -1,16 +1,22 @@
import QtQuick 2.15
+import Style 1.0
+
Item {
id: errorBox
property var text: ""
+
+ property color color: Style.errorBoxTextColor
+ property color backgroundColor: Style.errorBoxBackgroundColor
+ property color borderColor: Style.errorBoxBorderColor
implicitHeight: errorMessage.implicitHeight + 2 * 8
Rectangle {
anchors.fill: parent
- color: "red"
- border.color: "black"
+ color: errorBox.backgroundColor
+ border.color: errorBox.borderColor
}
Text {
@@ -19,7 +25,7 @@ Item {
anchors.fill: parent
anchors.margins: 8
width: parent.width
- color: "white"
+ color: errorBox.color
wrapMode: Text.WordWrap
text: errorBox.text
}
diff --git a/src/gui/main.cpp b/src/gui/main.cpp
index df7279308c..bf78b4c076 100644
--- a/src/gui/main.cpp
+++ b/src/gui/main.cpp
@@ -31,6 +31,7 @@
#include "userstatusselectormodel.h"
#include "emojimodel.h"
#include "tray/syncstatussummary.h"
+#include "tray/unifiedsearchresultslistmodel.h"
#if defined(BUILD_UPDATER)
#include "updater/updater.h"
@@ -68,6 +69,9 @@ int main(int argc, char **argv)
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "UserStatusSelectorModel");
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ActivityListModel");
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "FileActivityListModel");
+ qmlRegisterUncreatableType(
+ "com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
+ qRegisterMetaType("UnifiedSearchResultsListModel*");
qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp
index 1f4760b355..dafb0dfc1a 100644
--- a/src/gui/systray.cpp
+++ b/src/gui/systray.cpp
@@ -18,6 +18,7 @@
#include "config.h"
#include "common/utility.h"
#include "tray/UserModel.h"
+#include "tray/unifiedsearchresultimageprovider.h"
#include "configfile.h"
#include
@@ -58,6 +59,7 @@ void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine)
_trayEngine->addImportPath("qrc:/qml/theme");
_trayEngine->addImageProvider("avatars", new ImageProvider);
+ _trayEngine->addImageProvider(QLatin1String("unified-search-result-icon"), new UnifiedSearchResultImageProvider);
}
Systray::Systray()
diff --git a/src/gui/tray/UnifiedSearchInputContainer.qml b/src/gui/tray/UnifiedSearchInputContainer.qml
new file mode 100644
index 0000000000..eda93a11d8
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchInputContainer.qml
@@ -0,0 +1,110 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import QtGraphicalEffects 1.0
+import Style 1.0
+
+TextField {
+ id: trayWindowUnifiedSearchTextField
+
+ property bool isSearchInProgress: false
+
+ readonly property color textFieldIconsColor: Style.menuBorder
+
+ readonly property int textFieldIconsOffset: 10
+
+ readonly property double textFieldIconsScaleFactor: 0.6
+
+ readonly property int textFieldHorizontalPaddingOffset: 14
+
+ leftPadding: trayWindowUnifiedSearchTextFieldSearchIcon.width + trayWindowUnifiedSearchTextFieldSearchIcon.anchors.leftMargin + textFieldHorizontalPaddingOffset
+ rightPadding: trayWindowUnifiedSearchTextFieldClearTextButton.width + trayWindowUnifiedSearchTextFieldClearTextButton.anchors.rightMargin + textFieldHorizontalPaddingOffset
+
+ placeholderText: qsTr("Search files, messages, events...")
+
+ selectByMouse: true
+
+ background: Rectangle {
+ radius: 5
+ border.color: parent.activeFocus ? Style.ncBlue : Style.menuBorder
+ border.width: 1
+ }
+
+ Image {
+ id: trayWindowUnifiedSearchTextFieldSearchIcon
+
+ anchors {
+ left: parent.left
+ leftMargin: parent.textFieldIconsOffset
+ verticalCenter: parent.verticalCenter
+ }
+
+ visible: !trayWindowUnifiedSearchTextField.isSearchInProgress
+
+ smooth: true;
+ antialiasing: true
+ mipmap: true
+
+ source: "qrc:///client/theme/black/search.svg"
+ sourceSize: Qt.size(parent.height * parent.textFieldIconsScaleFactor, parent.height * parent.textFieldIconsScaleFactor)
+
+ ColorOverlay {
+ anchors.fill: parent
+ source: parent
+ cached: true
+ color: parent.parent.textFieldIconsColor
+ }
+ }
+
+ BusyIndicator {
+ id: trayWindowUnifiedSearchTextFieldIconInProgress
+ running: visible
+ visible: trayWindowUnifiedSearchTextField.isSearchInProgress
+ anchors {
+ left: trayWindowUnifiedSearchTextField.left
+ bottom: trayWindowUnifiedSearchTextField.bottom
+ leftMargin: trayWindowUnifiedSearchTextField.textFieldIconsOffset - 4
+ topMargin: 4
+ bottomMargin: 4
+ verticalCenter: trayWindowUnifiedSearchTextField.verticalCenter
+ }
+ width: height
+ }
+
+ Image {
+ id: trayWindowUnifiedSearchTextFieldClearTextButton
+
+ anchors {
+ right: parent.right
+ rightMargin: parent.textFieldIconsOffset
+ verticalCenter: parent.verticalCenter
+ }
+
+ smooth: true;
+ antialiasing: true
+ mipmap: true
+
+ visible: parent.text
+
+ source: "qrc:///client/theme/black/clear.svg"
+ sourceSize: Qt.size(parent.height * parent.textFieldIconsScaleFactor, parent.height * parent.textFieldIconsScaleFactor)
+
+ ColorOverlay {
+ anchors.fill: parent
+ cached: true
+ source: parent
+ color: parent.parent.textFieldIconsColor
+ }
+
+ MouseArea {
+ id: trayWindowUnifiedSearchTextFieldClearTextButtonMouseArea
+
+ anchors.fill: parent
+
+ onClicked: {
+ trayWindowUnifiedSearchTextField.text = ""
+ trayWindowUnifiedSearchTextField.onTextEdited()
+ }
+ }
+ }
+}
diff --git a/src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml b/src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml
new file mode 100644
index 0000000000..1af2d42ca7
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml
@@ -0,0 +1,42 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import Style 1.0
+
+ColumnLayout {
+ id: unifiedSearchResultItemFetchMore
+
+ property bool isFetchMoreInProgress: false
+
+ property bool isWihinViewPort: false
+
+ property int fontSize: Style.topLinePixelSize
+
+ property string textColor: "grey"
+
+ Accessible.role: Accessible.ListItem
+ Accessible.name: unifiedSearchResultItemFetchMoreText.text
+ Accessible.onPressAction: unifiedSearchResultMouseArea.clicked()
+
+ Label {
+ id: unifiedSearchResultItemFetchMoreText
+ text: qsTr("Load more results")
+ visible: !unifiedSearchResultItemFetchMore.isFetchMoreInProgress
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ font.pixelSize: unifiedSearchResultItemFetchMore.fontSize
+ color: unifiedSearchResultItemFetchMore.textColor
+ }
+
+ BusyIndicator {
+ id: unifiedSearchResultItemFetchMoreIconInProgress
+ running: visible
+ visible: unifiedSearchResultItemFetchMore.isFetchMoreInProgress && unifiedSearchResultItemFetchMore.isWihinViewPort
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ Layout.preferredWidth: parent.height * 0.70
+ Layout.preferredHeight: parent.height * 0.70
+ }
+}
diff --git a/src/gui/tray/UnifiedSearchResultItem.qml b/src/gui/tray/UnifiedSearchResultItem.qml
new file mode 100644
index 0000000000..0241ed28e7
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchResultItem.qml
@@ -0,0 +1,107 @@
+import QtQml 2.15
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import Style 1.0
+import QtGraphicalEffects 1.0
+
+RowLayout {
+ id: unifiedSearchResultItemDetails
+
+ property string title: ""
+ property string subline: ""
+ property string icons: ""
+ property string iconPlaceholder: ""
+ property bool isRounded: false
+
+
+ property int textLeftMargin: 18
+ property int textRightMargin: 16
+ property int iconWidth: 24
+ property int iconLeftMargin: 12
+
+ property int titleFontSize: Style.topLinePixelSize
+ property int sublineFontSize: Style.subLinePixelSize
+
+ property string titleColor: "black"
+ property string sublineColor: "grey"
+
+ Accessible.role: Accessible.ListItem
+ Accessible.name: resultTitle
+ Accessible.onPressAction: unifiedSearchResultMouseArea.clicked()
+
+ ColumnLayout {
+ id: unifiedSearchResultImageContainer
+ visible: true
+ Layout.preferredWidth: unifiedSearchResultItemDetails.iconWidth + 10
+ Layout.preferredHeight: unifiedSearchResultItemDetails.height
+ Image {
+ id: unifiedSearchResultThumbnail
+ visible: false
+ asynchronous: true
+ source: "image://unified-search-result-icon/" + icons
+ cache: true
+ sourceSize.width: imageData.width
+ sourceSize.height: imageData.height
+ width: imageData.width
+ height: imageData.height
+ }
+ Rectangle {
+ id: mask
+ visible: false
+ radius: isRounded ? width / 2 : 0
+ width: imageData.width
+ height: imageData.height
+ }
+ OpacityMask {
+ id: imageData
+ visible: !unifiedSearchResultThumbnailPlaceholder.visible && icons
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ Layout.leftMargin: iconLeftMargin
+ Layout.preferredWidth: unifiedSearchResultItemDetails.iconWidth
+ Layout.preferredHeight: unifiedSearchResultItemDetails.iconWidth
+ source: unifiedSearchResultThumbnail
+ maskSource: mask
+ }
+ Image {
+ id: unifiedSearchResultThumbnailPlaceholder
+ visible: icons && iconPlaceholder && unifiedSearchResultThumbnail.status !== Image.Ready
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ Layout.leftMargin: iconLeftMargin
+ verticalAlignment: Qt.AlignCenter
+ cache: true
+ source: iconPlaceholder
+ sourceSize.height: unifiedSearchResultItemDetails.iconWidth
+ sourceSize.width: unifiedSearchResultItemDetails.iconWidth
+ Layout.preferredWidth: unifiedSearchResultItemDetails.iconWidth
+ Layout.preferredHeight: unifiedSearchResultItemDetails.iconWidth
+ }
+ }
+
+ ColumnLayout {
+ id: unifiedSearchResultTextContainer
+ Layout.fillWidth: true
+
+ Label {
+ id: unifiedSearchResultTitleText
+ text: title.replace(/[\r\n]+/g, " ")
+ Layout.leftMargin: textLeftMargin
+ Layout.rightMargin: textRightMargin
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ font.pixelSize: unifiedSearchResultItemDetails.titleFontSize
+ color: unifiedSearchResultItemDetails.titleColor
+ }
+ Label {
+ id: unifiedSearchResultTextSubline
+ text: subline.replace(/[\r\n]+/g, " ")
+ elide: Text.ElideRight
+ font.pixelSize: unifiedSearchResultItemDetails.sublineFontSize
+ Layout.leftMargin: textLeftMargin
+ Layout.rightMargin: textRightMargin
+ Layout.fillWidth: true
+ color: unifiedSearchResultItemDetails.sublineColor
+ }
+ }
+
+}
diff --git a/src/gui/tray/UnifiedSearchResultItemSkeleton.qml b/src/gui/tray/UnifiedSearchResultItemSkeleton.qml
new file mode 100644
index 0000000000..3f86c4b9f8
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchResultItemSkeleton.qml
@@ -0,0 +1,58 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Layouts 1.2
+import Style 1.0
+
+RowLayout {
+ id: unifiedSearchResultSkeletonItemDetails
+
+ property int textLeftMargin: 18
+ property int textRightMargin: 16
+ property int iconWidth: 24
+ property int iconLeftMargin: 12
+
+ property int titleFontSize: Style.topLinePixelSize
+ property int sublineFontSize: Style.subLinePixelSize
+
+ property string titleColor: "black"
+ property string sublineColor: "grey"
+
+ property string iconColor: "#afafaf"
+
+ property int index: 0
+
+ Accessible.role: Accessible.ListItem
+ Accessible.name: qsTr("Search result skeleton.").arg(index)
+
+ Rectangle {
+ id: unifiedSearchResultSkeletonThumbnail
+ color: unifiedSearchResultSkeletonItemDetails.iconColor
+ Layout.preferredWidth: unifiedSearchResultSkeletonItemDetails.iconWidth
+ Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth
+ Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.iconLeftMargin
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ }
+
+ ColumnLayout {
+ id: unifiedSearchResultSkeletonTextContainer
+ Layout.fillWidth: true
+
+ Rectangle {
+ id: unifiedSearchResultSkeletonTitleText
+ color: unifiedSearchResultSkeletonItemDetails.titleColor
+ Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.titleFontSize
+ Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin
+ Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin
+ Layout.fillWidth: true
+ }
+
+ Rectangle {
+ id: unifiedSearchResultSkeletonTextSubline
+ color: unifiedSearchResultSkeletonItemDetails.sublineColor
+ Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.sublineFontSize
+ Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin
+ Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin
+ Layout.fillWidth: true
+ }
+ }
+}
diff --git a/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml b/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml
new file mode 100644
index 0000000000..1b9f8c262a
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml
@@ -0,0 +1,49 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import Style 1.0
+
+Column {
+ id: unifiedSearchResultsListViewSkeletonColumn
+
+ property int textLeftMargin: 18
+ property int textRightMargin: 16
+ property int iconWidth: 24
+ property int iconLeftMargin: 12
+ property int itemHeight: Style.trayWindowHeaderHeight
+ property int titleFontSize: Style.topLinePixelSize
+ property int sublineFontSize: Style.subLinePixelSize
+ property string titleColor: "black"
+ property string sublineColor: "grey"
+ property string iconColor: "#afafaf"
+
+ Repeater {
+ model: 10
+ UnifiedSearchResultItemSkeleton {
+ textLeftMargin: unifiedSearchResultsListViewSkeletonColumn.textLeftMargin
+ textRightMargin: unifiedSearchResultsListViewSkeletonColumn.textRightMargin
+ iconWidth: unifiedSearchResultsListViewSkeletonColumn.iconWidth
+ iconLeftMargin: unifiedSearchResultsListViewSkeletonColumn.iconLeftMargin
+ width: unifiedSearchResultsListViewSkeletonColumn.width
+ height: unifiedSearchResultsListViewSkeletonColumn.itemHeight
+ index: model.index
+ titleFontSize: unifiedSearchResultsListViewSkeletonColumn.titleFontSize
+ sublineFontSize: unifiedSearchResultsListViewSkeletonColumn.sublineFontSize
+ titleColor: unifiedSearchResultsListViewSkeletonColumn.titleColor
+ sublineColor: unifiedSearchResultsListViewSkeletonColumn.sublineColor
+ iconColor: unifiedSearchResultsListViewSkeletonColumn.iconColor
+ }
+ }
+
+ OpacityAnimator {
+ target: unifiedSearchResultsListViewSkeletonColumn;
+ from: 0.5;
+ to: 1;
+ duration: 800
+ running: unifiedSearchResultsListViewSkeletonColumn.visible
+ loops: Animation.Infinite;
+ easing {
+ type: Easing.InOutBounce;
+ }
+ }
+}
diff --git a/src/gui/tray/UnifiedSearchResultListItem.qml b/src/gui/tray/UnifiedSearchResultListItem.qml
new file mode 100644
index 0000000000..33a05f2d56
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchResultListItem.qml
@@ -0,0 +1,87 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import Style 1.0
+
+MouseArea {
+ id: unifiedSearchResultMouseArea
+
+ property int textLeftMargin: 18
+ property int textRightMargin: 16
+ property int iconWidth: 24
+ property int iconLeftMargin: 12
+
+ property int titleFontSize: Style.topLinePixelSize
+ property int sublineFontSize: Style.subLinePixelSize
+
+ property string titleColor: "black"
+ property string sublineColor: "grey"
+
+ property string currentFetchMoreInProgressProviderId: ""
+
+ readonly property bool isFetchMoreTrigger: model.typeAsString === "FetchMoreTrigger"
+
+ property bool isFetchMoreInProgress: currentFetchMoreInProgressProviderId === model.providerId
+ property bool isSearchInProgress: false
+
+ property bool isPooled: false
+
+ property var fetchMoreTriggerClicked: function(){}
+ property var resultClicked: function(){}
+
+ enabled: !isFetchMoreTrigger || !isSearchInProgress
+ hoverEnabled: enabled
+
+ ToolTip {
+ visible: unifiedSearchResultMouseArea.containsMouse
+ text: isFetchMoreTrigger ? qsTr("Load more results") : model.resultTitle + "\n\n" + model.subline
+ delay: Qt.styleHints.mousePressAndHoldInterval
+ }
+
+ Rectangle {
+ id: unifiedSearchResultHoverBackground
+ anchors.fill: parent
+ color: (parent.containsMouse ? Style.lightHover : "transparent")
+ }
+
+ Loader {
+ active: !isFetchMoreTrigger
+ sourceComponent: UnifiedSearchResultItem {
+ width: unifiedSearchResultMouseArea.width
+ height: unifiedSearchResultMouseArea.height
+ title: model.resultTitle
+ subline: model.subline
+ icons: model.icons
+ iconPlaceholder: model.imagePlaceholder
+ isRounded: model.isRounded
+ textLeftMargin: unifiedSearchResultMouseArea.textLeftMargin
+ textRightMargin: unifiedSearchResultMouseArea.textRightMargin
+ iconWidth: unifiedSearchResultMouseArea.iconWidth
+ iconLeftMargin: unifiedSearchResultMouseArea.iconLeftMargin
+ titleFontSize: unifiedSearchResultMouseArea.titleFontSize
+ sublineFontSize: unifiedSearchResultMouseArea.sublineFontSize
+ titleColor: unifiedSearchResultMouseArea.titleColor
+ sublineColor: unifiedSearchResultMouseArea.sublineColor
+ }
+ }
+
+ Loader {
+ active: isFetchMoreTrigger
+ sourceComponent: UnifiedSearchResultFetchMoreTrigger {
+ isFetchMoreInProgress: unifiedSearchResultMouseArea.isFetchMoreInProgress
+ width: unifiedSearchResultMouseArea.width
+ height: unifiedSearchResultMouseArea.height
+ isWihinViewPort: !unifiedSearchResultMouseArea.isPooled
+ fontSize: unifiedSearchResultMouseArea.titleFontSize
+ textColor: unifiedSearchResultMouseArea.sublineColor
+ }
+ }
+
+ onClicked: {
+ if (isFetchMoreTrigger) {
+ unifiedSearchResultMouseArea.fetchMoreTriggerClicked(model.providerId)
+ } else {
+ unifiedSearchResultMouseArea.resultClicked(model.providerId, model.resourceUrlRole)
+ }
+ }
+}
diff --git a/src/gui/tray/UnifiedSearchResultNothingFound.qml b/src/gui/tray/UnifiedSearchResultNothingFound.qml
new file mode 100644
index 0000000000..36cea93a63
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchResultNothingFound.qml
@@ -0,0 +1,47 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import Style 1.0
+
+ColumnLayout {
+ id: unifiedSearchResultNothingFoundContainer
+
+ required property string text
+
+ spacing: 8
+ anchors.leftMargin: 10
+ anchors.rightMargin: 10
+
+ Image {
+ id: unifiedSearchResultsNoResultsLabelIcon
+ source: "qrc:///client/theme/magnifying-glass.svg"
+ sourceSize.width: Style.trayWindowHeaderHeight / 2
+ sourceSize.height: Style.trayWindowHeaderHeight / 2
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ Label {
+ id: unifiedSearchResultsNoResultsLabel
+ text: qsTr("No results for")
+ color: Style.menuBorder
+ font.pixelSize: Style.subLinePixelSize * 1.25
+ wrapMode: Text.Wrap
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.trayWindowHeaderHeight / 2
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Label {
+ id: unifiedSearchResultsNoResultsLabelDetails
+ text: unifiedSearchResultNothingFoundContainer.text
+ color: "black"
+ font.pixelSize: Style.topLinePixelSize * 1.25
+ wrapMode: Text.Wrap
+ maximumLineCount: 2
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.trayWindowHeaderHeight / 2
+ horizontalAlignment: Text.AlignHCenter
+ }
+}
diff --git a/src/gui/tray/UnifiedSearchResultSectionItem.qml b/src/gui/tray/UnifiedSearchResultSectionItem.qml
new file mode 100644
index 0000000000..a6f9fd591b
--- /dev/null
+++ b/src/gui/tray/UnifiedSearchResultSectionItem.qml
@@ -0,0 +1,20 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.2
+import Style 1.0
+
+Label {
+ required property string section
+
+ topPadding: 8
+ bottomPadding: 8
+ leftPadding: 16
+
+ text: section
+ font.pixelSize: Style.topLinePixelSize
+ color: Style.ncBlue
+
+ Accessible.role: Accessible.Separator
+ Accessible.name: qsTr("Search results section %1").arg(section)
+}
diff --git a/src/gui/tray/UserModel.cpp b/src/gui/tray/UserModel.cpp
index c9e3d92b57..3624dd6522 100644
--- a/src/gui/tray/UserModel.cpp
+++ b/src/gui/tray/UserModel.cpp
@@ -14,6 +14,7 @@
#include "syncfileitem.h"
#include "tray/ActivityListModel.h"
#include "tray/NotificationCache.h"
+#include "tray/unifiedsearchresultslistmodel.h"
#include "userstatusconnector.h"
#include
@@ -38,7 +39,8 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
: QObject(parent)
, _account(account)
, _isCurrentUser(isCurrent)
- , _activityModel(new ActivityListModel(_account.data()))
+ , _activityModel(new ActivityListModel(_account.data(), this))
+ , _unifiedSearchResultsModel(new UnifiedSearchResultsListModel(_account.data(), this))
, _notificationRequestsRunning(0)
{
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
@@ -589,6 +591,11 @@ ActivityListModel *User::getActivityModel()
return _activityModel;
}
+UnifiedSearchResultsListModel *User::getUnifiedSearchResultsListModel() const
+{
+ return _unifiedSearchResultsModel;
+}
+
void User::openLocalFolder()
{
const auto folder = getFolder();
diff --git a/src/gui/tray/UserModel.h b/src/gui/tray/UserModel.h
index eb3be91365..88dc78a25d 100644
--- a/src/gui/tray/UserModel.h
+++ b/src/gui/tray/UserModel.h
@@ -18,6 +18,7 @@
#include
namespace OCC {
+class UnifiedSearchResultsListModel;
class User : public QObject
{
@@ -33,6 +34,7 @@ class User : public QObject
Q_PROPERTY(bool serverHasTalk READ serverHasTalk NOTIFY serverHasTalkChanged)
Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged)
+ Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT)
public:
User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr);
@@ -44,6 +46,7 @@ public:
void setCurrentUser(const bool &isCurrent);
Folder *getFolder() const;
ActivityListModel *getActivityModel();
+ UnifiedSearchResultsListModel *getUnifiedSearchResultsListModel() const;
void openLocalFolder();
QString name() const;
QString server(bool shortened = true) const;
@@ -113,6 +116,7 @@ private:
AccountStatePtr _account;
bool _isCurrentUser;
ActivityListModel *_activityModel;
+ UnifiedSearchResultsListModel *_unifiedSearchResultsModel;
ActivityList _blacklistedNotifications;
QTimer _expiredActivitiesCheckTimer;
diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml
index 5bda9242da..d85615adb8 100644
--- a/src/gui/tray/Window.qml
+++ b/src/gui/tray/Window.qml
@@ -1,10 +1,11 @@
import QtQml 2.12
import QtQml.Models 2.1
-import QtQuick 2.9
+import QtQuick 2.15
import QtQuick.Window 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtGraphicalEffects 1.0
+import "../"
// Custom qml modules are in /theme (and included by resources.qrc)
import Style 1.0
@@ -101,6 +102,11 @@ Window {
Rectangle {
id: trayWindowBackground
+ property bool isUnifiedSearchActive: unifiedSearchResultsListViewSkeleton.visible
+ || unifiedSearchResultNothingFound.visible
+ || unifiedSearchResultsErrorLabel.visible
+ || unifiedSearchResultsListView.visible
+
anchors.fill: parent
radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius
border.width: Style.trayWindowBorderWidth
@@ -420,7 +426,7 @@ Window {
visible: UserModel.currentUser.statusMessage !== ""
width: Style.currentAccountLabelWidth
text: UserModel.currentUser.statusMessage !== ""
- ? UserModel.currentUser.statusMessage
+ ? UserModel.currentUser.statusMessage
: UserModel.currentUser.server
elide: Text.ElideRight
color: Style.ncTextColor
@@ -452,20 +458,20 @@ Window {
Item {
Layout.fillWidth: true
}
-
+
RowLayout {
id: openLocalFolderRowLayout
spacing: 0
Layout.preferredWidth: Style.trayWindowHeaderHeight
Layout.preferredHeight: Style.trayWindowHeaderHeight
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
-
+
HeaderButton {
id: openLocalFolderButton
visible: UserModel.currentUser.hasLocalFolder
icon.source: "qrc:///client/theme/white/folder.svg"
onClicked: UserModel.openCurrentAccountLocalFolder()
-
+
Rectangle {
id: folderStateIndicatorBackground
width: Style.folderStateIndicatorSize
@@ -476,7 +482,7 @@ Window {
radius: width*0.5
z: 1
}
-
+
Image {
id: folderStateIndicator
visible: UserModel.currentUser.hasLocalFolder
@@ -484,19 +490,17 @@ Window {
? Style.stateOnlineImageSource
: Style.stateOfflineImageSource
cache: false
-
+
anchors.top: openLocalFolderButton.verticalCenter
- anchors.left: openLocalFolderButton.horizontalCenter
+ anchors.left: openLocalFolderButton.horizontalCenter
sourceSize.width: Style.folderStateIndicatorSize
sourceSize.height: Style.folderStateIndicatorSize
-
+
Accessible.role: Accessible.Indicator
Accessible.name: UserModel.currentUser.isConnected ? qsTr("Connected") : qsTr("Disconnected")
z: 2
}
}
-
-
Accessible.role: Accessible.Button
Accessible.name: qsTr("Open local folder of current account")
@@ -504,11 +508,11 @@ Window {
HeaderButton {
id: trayWindowTalkButton
-
+
visible: UserModel.currentUser.serverHasTalk
icon.source: "qrc:///client/theme/white/talk-app.svg"
onClicked: UserModel.openCurrentAccountTalk()
-
+
Accessible.role: Accessible.Button
Accessible.name: qsTr("Open Nextcloud Talk in browser")
Accessible.onPressAction: trayWindowTalkButton.clicked()
@@ -517,7 +521,7 @@ Window {
HeaderButton {
id: trayWindowAppsButton
icon.source: "qrc:///client/theme/white/more-apps.svg"
-
+
onClicked: {
if(appsMenu.count <= 0) {
UserModel.openCurrentAccountServer()
@@ -566,20 +570,158 @@ Window {
}
} // Rectangle trayWindowHeaderBackground
+ UnifiedSearchInputContainer {
+ id: trayWindowUnifiedSearchInputContainer
+ height: Style.trayWindowHeaderHeight * 0.65
+
+ anchors {
+ top: trayWindowHeaderBackground.bottom
+ left: trayWindowBackground.left
+ right: trayWindowBackground.right
+
+ margins: {
+ top: 10
+ }
+ }
+
+ text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm
+ readOnly: !UserModel.currentUser.isConnected || UserModel.currentUser.unifiedSearchResultsListModel.currentFetchMoreInProgressProviderId
+ isSearchInProgress: UserModel.currentUser.unifiedSearchResultsListModel.isSearchInProgress
+ onTextEdited: { UserModel.currentUser.unifiedSearchResultsListModel.searchTerm = trayWindowUnifiedSearchInputContainer.text }
+ }
+
+ ErrorBox {
+ id: unifiedSearchResultsErrorLabel
+ visible: UserModel.currentUser.unifiedSearchResultsListModel.errorString && !unifiedSearchResultsListView.visible && ! UserModel.currentUser.unifiedSearchResultsListModel.isSearchInProgress && ! UserModel.currentUser.unifiedSearchResultsListModel.currentFetchMoreInProgressProviderId
+ text: UserModel.currentUser.unifiedSearchResultsListModel.errorString
+ color: Style.errorBoxBackgroundColor
+ backgroundColor: Style.errorBoxTextColor
+ borderColor: "transparent"
+ anchors.top: trayWindowUnifiedSearchInputContainer.bottom
+ anchors.left: trayWindowBackground.left
+ anchors.right: trayWindowBackground.right
+ anchors.margins: 10
+ }
+
+ UnifiedSearchResultNothingFound {
+ id: unifiedSearchResultNothingFound
+ visible: false
+ anchors.top: trayWindowUnifiedSearchInputContainer.bottom
+ anchors.left: trayWindowBackground.left
+ anchors.right: trayWindowBackground.right
+ anchors.topMargin: 10
+
+ text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm
+
+ property bool isSearchRunning: UserModel.currentUser.unifiedSearchResultsListModel.isSearchInProgress
+ property bool isSearchResultsEmpty: unifiedSearchResultsListView.count === 0
+ property bool nothingFound: text && isSearchResultsEmpty && !UserModel.currentUser.unifiedSearchResultsListModel.errorString
+
+ onIsSearchRunningChanged: {
+ if (unifiedSearchResultNothingFound.isSearchRunning) {
+ visible = false;
+ } else {
+ if (nothingFound) {
+ visible = true;
+ }
+ }
+ }
+
+ onTextChanged: {
+ visible = false;
+ }
+
+ onIsSearchResultsEmptyChanged: {
+ if (!unifiedSearchResultNothingFound.isSearchResultsEmpty) {
+ visible = false;
+ }
+ }
+ }
+
+ UnifiedSearchResultItemSkeletonContainer {
+ id: unifiedSearchResultsListViewSkeleton
+ visible: !unifiedSearchResultNothingFound.visible && !unifiedSearchResultsListView.visible && ! UserModel.currentUser.unifiedSearchResultsListModel.errorString && UserModel.currentUser.unifiedSearchResultsListModel.searchTerm
+ anchors.top: trayWindowUnifiedSearchInputContainer.bottom
+ anchors.left: trayWindowBackground.left
+ anchors.right: trayWindowBackground.right
+ anchors.bottom: trayWindowBackground.bottom
+ textLeftMargin: trayWindowBackground.Style.unifiedSearchResultTextLeftMargin
+ textRightMargin: trayWindowBackground.Style.unifiedSearchResultTextRightMargin
+ iconWidth: trayWindowBackground.Style.unifiedSearchResulIconWidth
+ iconLeftMargin: trayWindowBackground.Style.unifiedSearchResulIconLeftMargin
+ itemHeight: trayWindowBackground.Style.unifiedSearchItemHeight
+ titleFontSize: trayWindowBackground.Style.unifiedSearchResulTitleFontSize
+ sublineFontSize: trayWindowBackground.Style.unifiedSearchResulSublineFontSize
+ titleColor: trayWindowBackground.Style.unifiedSearchResulTitleColor
+ sublineColor: trayWindowBackground.Style.unifiedSearchResulSublineColor
+ iconColor: "#afafaf"
+ }
+
+ ListView {
+ id: unifiedSearchResultsListView
+ anchors.top: trayWindowUnifiedSearchInputContainer.bottom
+ anchors.left: trayWindowBackground.left
+ anchors.right: trayWindowBackground.right
+ anchors.bottom: trayWindowBackground.bottom
+ spacing: 4
+ visible: count > 0
+ clip: true
+ ScrollBar.vertical: ScrollBar {
+ id: unifiedSearchResultsListViewScrollbar
+ }
+
+ keyNavigationEnabled: true
+
+ reuseItems: true
+
+ Accessible.role: Accessible.List
+ Accessible.name: qsTr("Unified search results list")
+
+ model: UserModel.currentUser.unifiedSearchResultsListModel
+
+ delegate: UnifiedSearchResultListItem {
+ width: unifiedSearchResultsListView.width
+ height: trayWindowBackground.Style.unifiedSearchItemHeight
+ isSearchInProgress: unifiedSearchResultsListView.model.isSearchInProgress
+ textLeftMargin: trayWindowBackground.Style.unifiedSearchResultTextLeftMargin
+ textRightMargin: trayWindowBackground.Style.unifiedSearchResultTextRightMargin
+ iconWidth: trayWindowBackground.Style.unifiedSearchResulIconWidth
+ iconLeftMargin: trayWindowBackground.Style.unifiedSearchResulIconLeftMargin
+ titleFontSize: trayWindowBackground.Style.unifiedSearchResulTitleFontSize
+ sublineFontSize: trayWindowBackground.Style.unifiedSearchResulSublineFontSize
+ titleColor: trayWindowBackground.Style.unifiedSearchResulTitleColor
+ sublineColor: trayWindowBackground.Style.unifiedSearchResulSublineColor
+ currentFetchMoreInProgressProviderId: unifiedSearchResultsListView.model.currentFetchMoreInProgressProviderId
+ fetchMoreTriggerClicked: unifiedSearchResultsListView.model.fetchMoreTriggerClicked
+ resultClicked: unifiedSearchResultsListView.model.resultClicked
+ ListView.onPooled: isPooled = true
+ ListView.onReused: isPooled = false
+ }
+
+ section.property: "providerName"
+ section.criteria: ViewSection.FullString
+ section.delegate: UnifiedSearchResultSectionItem {
+ width: unifiedSearchResultsListView.width
+ }
+ }
+
SyncStatus {
id: syncStatus
- anchors.top: trayWindowHeaderBackground.bottom
+ visible: !trayWindowBackground.isUnifiedSearchActive
+
+ anchors.top: trayWindowUnifiedSearchInputContainer.bottom
anchors.left: trayWindowBackground.left
anchors.right: trayWindowBackground.right
}
ActivityList {
+ visible: !trayWindowBackground.isUnifiedSearchActive
anchors.top: syncStatus.bottom
anchors.left: trayWindowBackground.left
anchors.right: trayWindowBackground.right
anchors.bottom: trayWindowBackground.bottom
-
+
model: activityModel
onShowFileActivity: {
openFileActivityDialog(displayPath, absolutePath)
@@ -598,7 +740,7 @@ Window {
function refresh() {
active = true
item.model.load(activityModel.accountState, absolutePath)
- item.show()
+ item.show()
}
active: false
diff --git a/src/gui/tray/unifiedsearchresult.cpp b/src/gui/tray/unifiedsearchresult.cpp
new file mode 100644
index 0000000000..768ff3148c
--- /dev/null
+++ b/src/gui/tray/unifiedsearchresult.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov
+ *
+ * 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
+
+#include "unifiedsearchresult.h"
+
+namespace OCC {
+
+QString UnifiedSearchResult::typeAsString(UnifiedSearchResult::Type type)
+{
+ QString result;
+
+ switch (type) {
+ case Default:
+ result = QStringLiteral("Default");
+ break;
+
+ case FetchMoreTrigger:
+ result = QStringLiteral("FetchMoreTrigger");
+ break;
+ }
+ return result;
+}
+}
diff --git a/src/gui/tray/unifiedsearchresult.h b/src/gui/tray/unifiedsearchresult.h
new file mode 100644
index 0000000000..bae3158d63
--- /dev/null
+++ b/src/gui/tray/unifiedsearchresult.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov
+ *
+ * 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
+
+#include
+
+namespace OCC {
+
+/**
+ * @brief The UnifiedSearchResult class
+ * @ingroup gui
+ * Simple data structure that represents single Unified Search result
+ */
+
+struct UnifiedSearchResult
+{
+ enum Type : quint8 {
+ Default,
+ FetchMoreTrigger,
+ };
+
+ static QString typeAsString(UnifiedSearchResult::Type type);
+
+ QString _title;
+ QString _subline;
+ QString _providerId;
+ QString _providerName;
+ bool _isRounded = false;
+ qint32 _order = std::numeric_limits::max();
+ QUrl _resourceUrl;
+ QString _icons;
+ Type _type = Type::Default;
+};
+}
diff --git a/src/gui/tray/unifiedsearchresultimageprovider.cpp b/src/gui/tray/unifiedsearchresultimageprovider.cpp
new file mode 100644
index 0000000000..f0bba8efef
--- /dev/null
+++ b/src/gui/tray/unifiedsearchresultimageprovider.cpp
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov
+ *
+ * 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 "unifiedsearchresultimageprovider.h"
+
+#include "UserModel.h"
+
+#include
+#include
+#include
+
+namespace {
+class AsyncImageResponse : public QQuickImageResponse
+{
+public:
+ AsyncImageResponse(const QString &id, const QSize &requestedSize)
+ {
+ if (id.isEmpty()) {
+ setImageAndEmitFinished();
+ return;
+ }
+
+ _imagePaths = id.split(QLatin1Char(';'), Qt::SkipEmptyParts);
+ _requestedImageSize = requestedSize;
+
+ if (_imagePaths.isEmpty()) {
+ setImageAndEmitFinished();
+ } else {
+ processNextImage();
+ }
+ }
+
+ void setImageAndEmitFinished(const QImage &image = {})
+ {
+ _image = image;
+ emit finished();
+ }
+
+ QQuickTextureFactory *textureFactory() const override
+ {
+ return QQuickTextureFactory::textureFactoryForImage(_image);
+ }
+
+private:
+ void processNextImage()
+ {
+ if (_index < 0 || _index >= _imagePaths.size()) {
+ setImageAndEmitFinished();
+ return;
+ }
+
+ if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) {
+ setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage());
+ return;
+ }
+
+ const auto currentUser = OCC::UserModel::instance()->currentUser();
+ if (currentUser && currentUser->account()) {
+ 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);
+ connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply);
+ ++_index;
+ return;
+ }
+ }
+
+ setImageAndEmitFinished();
+ }
+
+private slots:
+ void slotProcessNetworkReply()
+ {
+ const auto reply = qobject_cast(sender());
+ if (!reply) {
+ setImageAndEmitFinished();
+ return;
+ }
+
+ const QByteArray imageData = reply->readAll();
+ // server returns "[]" for some some file previews (have no idea why), so, we use another image
+ // from the list if available
+ if (imageData.isEmpty() || imageData == QByteArrayLiteral("[]")) {
+ processNextImage();
+ } else {
+ if (imageData.startsWith(QByteArrayLiteral("