diff --git a/storybook/pages/ProfileShowcaseAccountsPanelPage.qml b/storybook/pages/ProfileShowcaseAccountsPanelPage.qml index 2f5c785e31..d45549ec15 100644 --- a/storybook/pages/ProfileShowcaseAccountsPanelPage.qml +++ b/storybook/pages/ProfileShowcaseAccountsPanelPage.qml @@ -99,6 +99,7 @@ SplitView { inShowcaseModel: emptyModelChecker.checked ? emptyModel : inShowcaseModelItem hiddenModel: emptyModelChecker.checked ? emptyModel : hiddenModelItem currentWallet: root.currentWallet + showcaseLimit: 5 } LogsAndControlsPanel { diff --git a/storybook/pages/ProfileShowcaseAssetsPanelPage.qml b/storybook/pages/ProfileShowcaseAssetsPanelPage.qml index dcbef58c6e..f807f307e3 100644 --- a/storybook/pages/ProfileShowcaseAssetsPanelPage.qml +++ b/storybook/pages/ProfileShowcaseAssetsPanelPage.qml @@ -66,6 +66,7 @@ SplitView { SplitView.preferredHeight: 500 inShowcaseModel: inShowcaseModelItem hiddenModel: hiddenShowcaseModelItem + showcaseLimit: 8 addAccountsButtonVisible: !hasAllAccountsChecker.checked diff --git a/storybook/pages/ProfileShowcaseCollectiblesPanelPage.qml b/storybook/pages/ProfileShowcaseCollectiblesPanelPage.qml index 7c7540c320..ea9e720be5 100644 --- a/storybook/pages/ProfileShowcaseCollectiblesPanelPage.qml +++ b/storybook/pages/ProfileShowcaseCollectiblesPanelPage.qml @@ -211,6 +211,7 @@ SplitView { SplitView.preferredHeight: 500 inShowcaseModel: emptyModelChecker.checked ? emptyModelItem : joinedInShowcase hiddenModel: emptyModelChecker.checked ? emptyModelItem : joinedHiddenModel + showcaseLimit: 8 addAccountsButtonVisible: !hasAllAccountsChecker.checked diff --git a/storybook/pages/ProfileShowcaseCommunitiesPanelPage.qml b/storybook/pages/ProfileShowcaseCommunitiesPanelPage.qml index c34da7fa6c..900709b249 100644 --- a/storybook/pages/ProfileShowcaseCommunitiesPanelPage.qml +++ b/storybook/pages/ProfileShowcaseCommunitiesPanelPage.qml @@ -129,6 +129,7 @@ SplitView { inShowcaseModel: emptyModelChecker.checked ? emptyModel : inShowcaseModelItem hiddenModel: emptyModelChecker.checked ? emptyModel : hiddenModelItem + showcaseLimit: 5 } LogsAndControlsPanel { diff --git a/storybook/pages/ProfileShowcasePanelPage.qml b/storybook/pages/ProfileShowcasePanelPage.qml index 15efe8db18..11dfd68ad2 100644 --- a/storybook/pages/ProfileShowcasePanelPage.qml +++ b/storybook/pages/ProfileShowcasePanelPage.qml @@ -70,6 +70,7 @@ SplitView { SplitView.fillHeight: true emptyInShowcasePlaceholderText: "No items in showcase" emptyHiddenPlaceholderText: "No hidden items" + showcaseLimit: limitCounter.value onChangePositionRequested: function (from, to) { inShowcaseModelItem.move(from, to, 1) } @@ -100,6 +101,8 @@ SplitView { } delegate: ProfileShowcasePanelDelegate { + id: delegate + title: model ? model.title : "" secondaryTitle: model ? model.secondaryTitle : "" hasImage: model ? model.hasImage : false @@ -137,11 +140,13 @@ SplitView { Slider { id: inShowcaseCounter from: 0 - to: 200 + to: limitCounter.value stepSize: 1 - value: 25 + value: limitCounter.value > 0 ? limitCounter.value - 1 : 0 } + } + ColumnLayout { Label { text: "Hidden: " + hiddenCounter.value } @@ -153,31 +158,43 @@ SplitView { value: 25 } } + ColumnLayout { + Label { + text: "Showcase limit: " + limitCounter.value + } + Slider { + id: limitCounter + from: 0 + to: 200 + stepSize: 1 + value: 5 + } + } } } onInShowcaseModelCountChanged: { let count = inShowcaseModelCount - inShowcaseModelItem.count; let operation = count > 0 ? (i) =>{ - inShowcaseModelItem.append({ - showcaseKey: Math.random() * Math.random() * Math.random() * 1000, - title: "Item " + i, - secondaryTitle: "Description " + i, - hasImage: true, - image: "https://picsum.photos/200/300?random=" + i, - iconName: "https://picsum.photos/40/40?random=" + i, - showcaseVisibility: Math.ceil(Math.random() * 3), - name: "Test community", - joined: true, - isControlNode: true, - color: "yellow", - hasTag: Math.random() > 0.5, - tagText: "New " + 1, - tagAsset: "https://picsum.photos/40/40?random=" + i, - tagLoading: Math.random() > 0.5 - })} : (i) => { - inShowcaseModelItem.remove(inShowcaseModelItem.count - 1); - } + inShowcaseModelItem.append({ + showcaseKey: Math.random() * Math.random() * Math.random() * 1000, + title: "Item " + i, + secondaryTitle: "Description " + i, + hasImage: true, + image: "https://picsum.photos/200/300?random=" + i, + iconName: "https://picsum.photos/40/40?random=" + i, + showcaseVisibility: Math.ceil(Math.random() * 3), + name: "Test community", + joined: true, + isControlNode: true, + color: "yellow", + hasTag: Math.random() > 0.5, + tagText: "New " + 1, + tagAsset: "https://picsum.photos/40/40?random=" + i, + tagLoading: Math.random() > 0.5 + })} : (i) => { + inShowcaseModelItem.remove(inShowcaseModelItem.count - 1); + } for (var i = 0; i < Math.abs(count); i++) { operation(i) @@ -187,26 +204,26 @@ SplitView { onHiddenModelCountChanged: { let count = hiddenModelCount - hiddenModelItem.count; let operation = count > 0 ? (i) =>{ - hiddenModelItem.append({ - showcaseKey: Math.random() * Math.random() * Math.random() * 1000, - title: "Item " + i, - secondaryTitle: "Description " + i, - hasImage: true, - image: "https://picsum.photos/200/300?random=" + i, - iconName: "https://picsum.photos/40/40?random=" + i, - showcaseVisibility: 0, - name: "Test community", - joined: true, - memberRole: Constants.memberRole.owner, - isControlNode: true, - color: "yellow", - hasTag: Math.random() > 0.5, - tagText: "New " + i, - tagAsset: "https://picsum.photos/40/40?random=" + i, - tagLoading: Math.random() > 0.8 - })} : (i) => { - hiddenModelItem.remove(hiddenModelItem.count - 1); - } + hiddenModelItem.append({ + showcaseKey: Math.random() * Math.random() * Math.random() * 1000, + title: "Item " + i, + secondaryTitle: "Description " + i, + hasImage: true, + image: "https://picsum.photos/200/300?random=" + i, + iconName: "https://picsum.photos/40/40?random=" + i, + showcaseVisibility: 0, + name: "Test community", + joined: true, + memberRole: Constants.memberRole.owner, + isControlNode: true, + color: "yellow", + hasTag: Math.random() > 0.5, + tagText: "New " + i, + tagAsset: "https://picsum.photos/40/40?random=" + i, + tagLoading: Math.random() > 0.8 + })} : (i) => { + hiddenModelItem.remove(hiddenModelItem.count - 1); + } for (var i = 0; i < Math.abs(count); i++) { operation(i) diff --git a/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml b/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml index 83b2c567ed..e8b5d33ce6 100644 --- a/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml +++ b/ui/app/AppLayouts/Profile/controls/ShowcaseDelegate.qml @@ -13,6 +13,7 @@ import StatusQ.Core.Theme 0.1 import AppLayouts.Wallet.controls 1.0 import utils 1.0 +import shared.controls 1.0 StatusDraggableListItem { id: root @@ -20,6 +21,8 @@ StatusDraggableListItem { property alias actionComponent: additionalActionsLoader.sourceComponent property int showcaseVisibility: Constants.ShowcaseVisibility.NoOne property bool blurState: false + property bool contextMenuEnabled: true + property string tooltipTextWhenContextMenuDisabled signal showcaseVisibilityRequested(int value) @@ -46,66 +49,71 @@ StatusDraggableListItem { actions: [ Loader { - Layout.maximumWidth: root.width *.4 id: additionalActionsLoader - } - , - StatusRoundButton { - icon.name: ProfileUtils.visibilityIcon(root.showcaseVisibility) - Layout.preferredWidth: 58 - Layout.preferredHeight: 28 - border.width: 1 - border.color: Theme.palette.directColor7 - radius: 14 - highlighted: menuLoader.item && menuLoader.item.opened - onClicked: { - menuLoader.active = true - menuLoader.item.popup(width - menuLoader.item.width, height) - } - ButtonGroup { - id: showcaseVisibilityGroup - exclusive: true - onClicked: function(button) { - const newVisibility = (button as ShowcaseVisibilityAction).showcaseVisibility - if (newVisibility !== root.showcaseVisibility) - root.showcaseVisibilityRequested(newVisibility) + Layout.maximumWidth: root.width *.4 + }, + DisabledTooltipButton { + interactive: root.contextMenuEnabled + tooltipText: root.tooltipTextWhenContextMenuDisabled + buttonComponent: StatusRoundButton { + enabled: root.contextMenuEnabled + icon.name: ProfileUtils.visibilityIcon(root.showcaseVisibility) + width: 58 + height: 30 + border.width: 1 + border.color: Theme.palette.directColor7 + radius: 14 + highlighted: menuLoader.item && menuLoader.item.opened + onClicked: { + menuLoader.active = true + menuLoader.item.popup(width - menuLoader.item.width, height) } - } - Loader { - id: menuLoader - active: false - sourceComponent: StatusMenu { - onClosed: menuLoader.active = false - StatusMenuHeadline { text: qsTr("Show to") } - - ShowcaseVisibilityAction { - ButtonGroup.group: showcaseVisibilityGroup - showcaseVisibility: Constants.ShowcaseVisibility.Everyone - text: qsTr("Everyone") - checked: root.showcaseVisibility === showcaseVisibility - } - ShowcaseVisibilityAction { - ButtonGroup.group: showcaseVisibilityGroup - showcaseVisibility: Constants.ShowcaseVisibility.Contacts - text: qsTr("Contacts") - checked: root.showcaseVisibility === showcaseVisibility - } - ShowcaseVisibilityAction { - ButtonGroup.group: showcaseVisibilityGroup - showcaseVisibility: Constants.ShowcaseVisibility.IdVerifiedContacts - text: qsTr("ID verified contacts") - checked: root.showcaseVisibility === showcaseVisibility + ButtonGroup { + id: showcaseVisibilityGroup + exclusive: true + onClicked: function(button) { + const newVisibility = (button as ShowcaseVisibilityAction).showcaseVisibility + if (newVisibility !== root.showcaseVisibility) + root.showcaseVisibilityRequested(newVisibility) } + } - StatusMenuSeparator {} + Loader { + id: menuLoader + active: false + sourceComponent: StatusMenu { + onClosed: menuLoader.active = false + StatusMenuHeadline { text: qsTr("Show to") } - ShowcaseVisibilityAction { - ButtonGroup.group: showcaseVisibilityGroup - showcaseVisibility: Constants.ShowcaseVisibility.NoOne - text: qsTr("No one") - checked: root.showcaseVisibility === showcaseVisibility + ShowcaseVisibilityAction { + ButtonGroup.group: showcaseVisibilityGroup + showcaseVisibility: Constants.ShowcaseVisibility.Everyone + text: qsTr("Everyone") + checked: root.showcaseVisibility === showcaseVisibility + } + ShowcaseVisibilityAction { + ButtonGroup.group: showcaseVisibilityGroup + showcaseVisibility: Constants.ShowcaseVisibility.Contacts + text: qsTr("Contacts") + checked: root.showcaseVisibility === showcaseVisibility + } + ShowcaseVisibilityAction { + ButtonGroup.group: showcaseVisibilityGroup + showcaseVisibility: Constants.ShowcaseVisibility.IdVerifiedContacts + text: qsTr("ID verified contacts") + checked: root.showcaseVisibility === showcaseVisibility + } + + StatusMenuSeparator {} + + ShowcaseVisibilityAction { + ButtonGroup.group: showcaseVisibilityGroup + showcaseVisibility: Constants.ShowcaseVisibility.NoOne + text: qsTr("No one") + checked: root.showcaseVisibility === showcaseVisibility + } } } } diff --git a/ui/app/AppLayouts/Profile/panels/ProfileShowcasePanel.qml b/ui/app/AppLayouts/Profile/panels/ProfileShowcasePanel.qml index 882c6c069c..985918e130 100644 --- a/ui/app/AppLayouts/Profile/panels/ProfileShowcasePanel.qml +++ b/ui/app/AppLayouts/Profile/panels/ProfileShowcasePanel.qml @@ -19,7 +19,7 @@ DoubleFlickableWithFolding { property Component delegate: ProfileShowcasePanelDelegate {} - // Expected roles: + // Expected roles: // - visibility: int property var inShowcaseModel property var hiddenModel @@ -30,8 +30,11 @@ DoubleFlickableWithFolding { property string emptyInShowcasePlaceholderText property string emptyHiddenPlaceholderText - // Signal to requst position change of the visible items + property int showcaseLimit: ProfileUtils.showcaseLimit + + // Signal to request position change of the visible items signal changePositionRequested(int from, int to) + // Signal to request visibility change of the items signal setVisibilityRequested(var key, int toVisibility) @@ -43,6 +46,8 @@ DoubleFlickableWithFolding { QtObject { id: d + readonly property bool limitReached: root.showcaseLimit === inShowcaseCounterTracker.count + readonly property var dragHiddenItemKey: ["x-status-draggable-showcase-item-hidden"] readonly property var dragShowcaseItemKey: ["x-status-draggable-showcase-item"] @@ -51,6 +56,27 @@ DoubleFlickableWithFolding { property int additionalHeaderComponentWidth: 350 // by design property int additionalHeaderComponentHeight: 40 // by design + + property bool startAnimation: false + + signal setVisibilityInternalRequested(var key, int toVisibility) + onSetVisibilityInternalRequested: { + if(toVisibility !== Constants.ShowcaseVisibility.NoOne) { + startAnimation = !startAnimation + } + root.setVisibilityRequested(key, toVisibility) + } + } + + ModelChangeTracker { + id: inShowcaseCounterTracker + + property int count: { + revision + return model.rowCount() + } + + model: root.inShowcaseModel } clip: true @@ -67,23 +93,68 @@ DoubleFlickableWithFolding { model: root.inShowcaseModel header: FoldableHeader { + readonly property bool isDropAreaVisible: root.flickable1Folded && d.isAnyHiddenDragActive + width: ListView.view.width title: qsTr("In showcase") folded: root.flickable1Folded - rightAdditionalComponent: VisibilityDropAreaButtonsRow { - width: d.additionalHeaderComponentWidth - height: d.additionalHeaderComponentHeight - margins: 0 - visible: root.flickable1Folded && - (d.isAnyHiddenDragActive || - parent.containsDrag || - everyoneContainsDrag || - contactsContainsDrag || - verifiedContainsDrag) + rightAdditionalComponent: isDropAreaVisible && d.limitReached ? limitReachedHeaderButton : + isDropAreaVisible ? dropHeaderAreaComponent : counterComponent + + Component { + id: counterComponent + StatusBaseText { + id: counterText + + width: d.additionalHeaderComponentWidth + height: d.additionalHeaderComponentHeight + horizontalAlignment: Text.AlignRight + text: "%1 / %2".arg(inShowcaseCounterTracker.count).arg(root.showcaseLimit) + font.pixelSize: Style.current.tertiaryTextFontSize + color: Theme.palette.baseColor1 + + ColorAnimation { + id: animateColor + target: counterText + properties: "color" + from: Theme.palette.successColor1 + to: Theme.palette.baseColor1 + duration: 2000 + } + + Connections { + target: d + function onStartAnimationChanged() { + animateColor.start() + } + } + } + } + + Component { + id: dropHeaderAreaComponent + VisibilityDropAreaButtonsRow { + margins: 0 + width: d.additionalHeaderComponentWidth + height: d.additionalHeaderComponentHeight + } + } + + Component { + id: limitReachedHeaderButton + VisibilityDropAreaButton { + width: d.additionalHeaderComponentWidth + height: d.additionalHeaderComponentHeight + rightInset: 1 + text: qsTr("Showcase limit of %1 reached").arg(root.showcaseLimit) + enabled: false + textColor: Theme.palette.baseColor1 + iconVisible: false + } } onToggleFolding: root.flip1Folding() - } + } // Overlaid showcase listview content drop area: DropArea { @@ -99,13 +170,30 @@ DoubleFlickableWithFolding { width: parent.width height: ProfileUtils.defaultDelegateHeight anchors.bottom: parent.bottom - visible: d.isAnyHiddenDragActive || - parent.containsDrag || - everyoneContainsDrag || - contactsContainsDrag || - verifiedContainsDrag + visible: !d.limitReached && + (d.isAnyHiddenDragActive || + parent.containsDrag || + everyoneContainsDrag || + contactsContainsDrag || + verifiedContainsDrag) } } + + // Overlaid showcase listview content when limit reached: + VisibilityDropAreaButton { + id: limitReachedButton + + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.current.halfPadding + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - Style.current.padding + height: ProfileUtils.defaultDelegateHeight - Style.current.padding + visible: d.isAnyHiddenDragActive && d.limitReached + enabled: false + text: qsTr("Showcase limit of %1 reached").arg(root.showcaseLimit) + textColor: Theme.palette.baseColor1 + iconVisible: false + } } flickable2: EmptyShapeRectangleFooterListView { @@ -165,8 +253,10 @@ DoubleFlickableWithFolding { readonly property alias containsDrag: dropArea.containsDrag + property bool iconVisible: true + property string textColor: icon.color property int showcaseVisibility: Constants.ShowcaseVisibility.NoOne - property var dropAreaKeys + property var dropAreaKeys: [] padding: Style.current.halfPadding spacing: padding/2 @@ -175,7 +265,7 @@ DoubleFlickableWithFolding { background: ShapeRectangle { path.strokeColor: dropArea.containsDrag ? Theme.palette.primaryColor2 : Theme.palette.directColor7 - path.fillColor: dropArea.containsDrag ? Theme.palette.primaryColor3 : Theme.palette.baseColor4 + path.fillColor: dropArea.containsDrag ? Theme.palette.primaryColor3 : Theme.palette.getColor(Theme.palette.baseColor4, 0.7) DropArea { id: dropArea @@ -188,7 +278,7 @@ DoubleFlickableWithFolding { } onDropped: function(drop) { - root.setVisibilityRequested(drop.source.key, visibilityDropAreaButton.showcaseVisibility) + d.setVisibilityInternalRequested(drop.source.key, visibilityDropAreaButton.showcaseVisibility) } } } @@ -200,6 +290,7 @@ DoubleFlickableWithFolding { spacing: visibilityDropAreaButton.spacing StatusIcon { + visible: visibilityDropAreaButton.iconVisible width: 20 height: width icon: ProfileUtils.visibilityIcon(visibilityDropAreaButton.showcaseVisibility) @@ -211,7 +302,7 @@ DoubleFlickableWithFolding { font.pixelSize: Style.current.additionalTextSize font.weight: Font.Medium elide: Text.ElideRight - color: visibilityDropAreaButton.icon.color + color: visibilityDropAreaButton.textColor text: visibilityDropAreaButton.text } } @@ -270,7 +361,7 @@ DoubleFlickableWithFolding { Component { id: delegateWrapper - DropArea { + DropArea { id: showcaseDelegateRoot required property var model @@ -290,7 +381,7 @@ DoubleFlickableWithFolding { } function handleDropped(drop) { if (showcaseDelegateRoot.isHiddenShowcaseItem) { - root.setVisibilityRequested(drop.source.key, Constants.ShowcaseVisibility.NoOne) + d.setVisibilityInternalRequested(drop.source.key, Constants.ShowcaseVisibility.NoOne) } } @@ -315,32 +406,49 @@ DoubleFlickableWithFolding { property var dragParentData: root property int visualIndexData: showcaseDelegateRoot.index property var dragKeysData: showcaseDelegateRoot.isHiddenShowcaseItem ? - d.dragHiddenItemKey : d.dragShowcaseItemKey + d.dragHiddenItemKey : d.dragShowcaseItemKey width: parent.width sourceComponent: root.delegate onItemChanged: { if (item) { - item.showcaseVisibilityRequested.connect((toVisibility) => root.setVisibilityRequested(showcaseDelegateRoot.model.showcaseKey, toVisibility)) + item.showcaseVisibilityRequested.connect((toVisibility) => d.setVisibilityInternalRequested(showcaseDelegateRoot.model.showcaseKey, toVisibility)) } } } Binding { - when: showcaseDelegateRoot.isHiddenShowcaseItem ? d.isAnyShowcaseDragActive : d.isAnyHiddenDragActive - target: showcaseDraggableDelegateLoader.item - property: "blurState" - value: true - restoreMode: Binding.RestoreBindingOrValue - } + when: showcaseDelegateRoot.isHiddenShowcaseItem ? d.isAnyShowcaseDragActive : (d.isAnyHiddenDragActive || + (d.isAnyHiddenDragActive && d.limitReached)) + target: showcaseDraggableDelegateLoader.item + property: "blurState" + value: true + restoreMode: Binding.RestoreBindingOrValue + } - Binding { + Binding { when: showcaseShadow.visible target: d property: showcaseDelegateRoot.isHiddenShowcaseItem ? "isAnyHiddenDragActive" : "isAnyShowcaseDragActive" value: true restoreMode: Binding.RestoreBindingOrValue - } + } + + Binding { + when: showcaseDelegateRoot.isHiddenShowcaseItem && d.limitReached + target: showcaseDraggableDelegateLoader.item + property: "contextMenuEnabled" + value: false + restoreMode: Binding.RestoreBindingOrValue + } + + Binding { + when: showcaseDelegateRoot.isHiddenShowcaseItem && d.limitReached + target: showcaseDraggableDelegateLoader.item + property: "tooltipTextWhenContextMenuDisabled" + value: qsTr("Showcase limit of %1 reached.
Remove item from showcase to add more.").arg(root.showcaseLimit) + restoreMode: Binding.RestoreBindingOrValue + } // Delegate shadow background when dragging: ShadowDelegate { diff --git a/ui/imports/shared/controls/DisabledTooltipButton.qml b/ui/imports/shared/controls/DisabledTooltipButton.qml index f9654e8bc8..5d80a8fe34 100644 --- a/ui/imports/shared/controls/DisabledTooltipButton.qml +++ b/ui/imports/shared/controls/DisabledTooltipButton.qml @@ -36,6 +36,7 @@ Item { StatusToolTip { id: tooltip visible: hoverHandler.hovered && !!text + offset: -(tooltip.x + tooltip.width/2 - root.width/2) } Component{ diff --git a/ui/imports/utils/ProfileUtils.qml b/ui/imports/utils/ProfileUtils.qml index 9a3e3bbf73..f6ad6fad07 100644 --- a/ui/imports/utils/ProfileUtils.qml +++ b/ui/imports/utils/ProfileUtils.qml @@ -7,6 +7,7 @@ import StatusQ.Core.Theme 0.1 QtObject { readonly property int defaultDelegateHeight: 76 + readonly property int showcaseLimit: 100 function displayName(nickName, ensName, displayName, aliasName) {