diff --git a/src/gui/tray/UnifiedSearchResultItemSkeleton.qml b/src/gui/tray/UnifiedSearchResultItemSkeleton.qml index a7a1a6f95369a..32e19cca3aada 100644 --- a/src/gui/tray/UnifiedSearchResultItemSkeleton.qml +++ b/src/gui/tray/UnifiedSearchResultItemSkeleton.qml @@ -1,6 +1,7 @@ import QtQml 2.15 import QtQuick 2.15 import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.15 import Style 1.0 RowLayout { @@ -14,47 +15,181 @@ RowLayout { property int titleFontSize: Style.unifiedSearchResultTitleFontSize property int sublineFontSize: Style.unifiedSearchResultSublineFontSize - property color titleColor: Style.ncTextColor - property color sublineColor: Style.ncSecondaryTextColor + Accessible.role: Accessible.ListItem + Accessible.name: qsTr("Search result skeleton.").arg(model.index) - property string iconColor: "#afafaf" + height: Style.trayWindowHeaderHeight - property int index: 0 + /* + * An overview of what goes on here: + * + * We want to provide the user with a loading animation of a unified search result list item "skeleton". + * This skeleton has a square and two rectangles that are meant to represent the icon and the text of a + * real search result. + * + * We also want to provide a nice animation that has a dark gradient moving over these shapes. To do so, + * we very simply create rectangles that have a gradient going transparent->dark->dark->transparent, and + * overlay them over the shapes beneath. + * + * We then use NumberAnimations to move these gradients left-to-right over the base shapes below. Since + * the gradient rectangles are child elements of the base-color rectangles which they move over, as long + * as we ensure that the parent rectangle has the "clip" property enabled this will look nice and won't + * spill over onto other elements. + * + * We also want to make sure that, even though these gradient rectangles are separate, it looks as it is + * one single gradient sweeping over the base color components + */ - Accessible.role: Accessible.ListItem - Accessible.name: qsTr("Search result skeleton.").arg(index) + property color baseGradientColor: Style.lightHover + property color progressGradientColor: Style.darkMode ? Qt.lighter(baseGradientColor, 1.2) : Qt.darker(baseGradientColor, 1.1) - height: Style.trayWindowHeaderHeight + property int animationRectangleWidth: Style.trayWindowWidth + property int animationStartX: -animationRectangleWidth + property int animationEndX: animationRectangleWidth - Rectangle { - id: unifiedSearchResultSkeletonThumbnail - color: unifiedSearchResultSkeletonItemDetails.iconColor + Component { + id: gradientAnimationRectangle + Rectangle { + width: unifiedSearchResultSkeletonItemDetails.animationRectangleWidth + height: parent.height + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { + position: 0 + color: "transparent" + } + GradientStop { + position: 0.4 + color: unifiedSearchResultSkeletonItemDetails.progressGradientColor + } + GradientStop { + position: 0.6 + color: unifiedSearchResultSkeletonItemDetails.progressGradientColor + } + GradientStop { + position: 1.0 + color: "transparent" + } + } + + NumberAnimation on x { + from: unifiedSearchResultSkeletonItemDetails.animationStartX + to: unifiedSearchResultSkeletonItemDetails.animationEndX + duration: 1000 + loops: Animation.Infinite + running: true + } + } + } + + Item { Layout.preferredWidth: unifiedSearchResultSkeletonItemDetails.iconWidth Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.iconLeftMargin Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + + Rectangle { + id: unifiedSearchResultSkeletonThumbnail + anchors.fill: parent + color: unifiedSearchResultSkeletonItemDetails.baseGradientColor + clip: true + visible: false + + Loader { + x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x + height: parent.height + sourceComponent: gradientAnimationRectangle + } + } + + Rectangle { + id: unifiedSearchResultSkeletonThumbnailMask + anchors.fill: unifiedSearchResultSkeletonThumbnail + color: "white" + radius: 100 + visible: false + } + + OpacityMask { + anchors.fill: unifiedSearchResultSkeletonThumbnail + source: unifiedSearchResultSkeletonThumbnail + maskSource: unifiedSearchResultSkeletonThumbnailMask + } } - ColumnLayout { + Column { id: unifiedSearchResultSkeletonTextContainer + Layout.fillWidth: true + Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin + Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin - Rectangle { - id: unifiedSearchResultSkeletonTitleText - color: unifiedSearchResultSkeletonItemDetails.titleColor - Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.titleFontSize - Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin - Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin - Layout.fillWidth: true + spacing: Style.standardSpacing + + Item { + height: unifiedSearchResultSkeletonItemDetails.titleFontSize + width: parent.width + + Rectangle { + id: unifiedSearchResultSkeletonTitleText + anchors.fill: parent + color: unifiedSearchResultSkeletonItemDetails.baseGradientColor + clip: true + visible: false + + Loader { + x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x + height: parent.height + sourceComponent: gradientAnimationRectangle + } + } + + Rectangle { + id: unifiedSearchResultSkeletonTitleTextMask + anchors.fill: unifiedSearchResultSkeletonTitleText + color: "white" + radius: Style.veryRoundedButtonRadius + visible: false + } + + OpacityMask { + anchors.fill: unifiedSearchResultSkeletonTitleText + source: unifiedSearchResultSkeletonTitleText + maskSource: unifiedSearchResultSkeletonTitleTextMask + } } - Rectangle { - id: unifiedSearchResultSkeletonTextSubline - color: unifiedSearchResultSkeletonItemDetails.sublineColor - Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.sublineFontSize - Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin - Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin - Layout.fillWidth: true + Item { + height: unifiedSearchResultSkeletonItemDetails.sublineFontSize + width: parent.width + + Rectangle { + id: unifiedSearchResultSkeletonTextSubline + anchors.fill: parent + color: unifiedSearchResultSkeletonItemDetails.baseGradientColor + clip: true + visible: false + + Loader { + x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x + height: parent.height + sourceComponent: gradientAnimationRectangle + } + } + + Rectangle { + id: unifiedSearchResultSkeletonTextSublineMask + anchors.fill: unifiedSearchResultSkeletonTextSubline + color: "white" + radius: Style.veryRoundedButtonRadius + visible: false + } + + OpacityMask { + anchors.fill:unifiedSearchResultSkeletonTextSubline + source: unifiedSearchResultSkeletonTextSubline + maskSource: unifiedSearchResultSkeletonTextSublineMask + } } } } diff --git a/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml b/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml index 0ed23482cc316..354835d51e8b7 100644 --- a/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml +++ b/src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml @@ -5,22 +5,14 @@ import Style 1.0 Column { id: unifiedSearchResultsListViewSkeletonColumn + property int animationRectangleWidth: Style.trayWindowWidth + Repeater { - model: 10 + model: Math.ceil(unifiedSearchResultsListViewSkeletonColumn.height / Style.trayWindowHeaderHeight) UnifiedSearchResultItemSkeleton { width: unifiedSearchResultsListViewSkeletonColumn.width - } - } - - OpacityAnimator { - target: unifiedSearchResultsListViewSkeletonColumn - from: 0.5 - to: 1 - duration: 800 - running: unifiedSearchResultsListViewSkeletonColumn.visible - loops: Animation.Infinite - easing { - type: Easing.InOutBounce + height: Style.trayWindowHeaderHeight + animationRectangleWidth: unifiedSearchResultsListViewSkeletonColumn.animationRectangleWidth } } } diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml index 34873a4d0813c..22c92f64887b2 100644 --- a/src/gui/tray/Window.qml +++ b/src/gui/tray/Window.qml @@ -111,7 +111,7 @@ Window { Rectangle { id: trayWindowBackground - property bool isUnifiedSearchActive: unifiedSearchResultsListViewSkeleton.visible + property bool isUnifiedSearchActive: unifiedSearchResultsListViewSkeletonLoader.active || unifiedSearchResultNothingFound.visible || unifiedSearchResultsErrorLabel.visible || unifiedSearchResultsListView.visible @@ -721,13 +721,22 @@ Window { } } - UnifiedSearchResultItemSkeletonContainer { - id: unifiedSearchResultsListViewSkeleton - visible: !unifiedSearchResultNothingFound.visible && !unifiedSearchResultsListView.visible && ! UserModel.currentUser.unifiedSearchResultsListModel.errorString && UserModel.currentUser.unifiedSearchResultsListModel.searchTerm + Loader { + id: unifiedSearchResultsListViewSkeletonLoader anchors.top: trayWindowUnifiedSearchInputContainer.bottom anchors.left: trayWindowBackground.left anchors.right: trayWindowBackground.right anchors.bottom: trayWindowBackground.bottom + + active: !unifiedSearchResultNothingFound.visible && + !unifiedSearchResultsListView.visible && + !UserModel.currentUser.unifiedSearchResultsListModel.errorString && + UserModel.currentUser.unifiedSearchResultsListModel.searchTerm + + sourceComponent: UnifiedSearchResultItemSkeletonContainer { + anchors.fill: parent + animationRectangleWidth: trayWindow.width + } } ScrollView { diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 1d8b200e3852f..06970bae098de 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -6,6 +6,7 @@ import com.nextcloud.desktopclient 1.0 QtObject { readonly property int pixelSize: fontMetrics.font.pixelSize + readonly property bool darkMode: Theme.darkMode // Colors readonly property color ncBlue: Theme.wizardHeaderBackgroundColor