feat(@wallet): Show lightbox when clicking collectible (#14168)

This commit is contained in:
Cuteivist 2024-05-15 11:36:56 +02:00 committed by GitHub
parent 2565c8a135
commit d70f2dcf23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 341 additions and 68 deletions

View File

@ -0,0 +1,66 @@
import QtQuick 2.13
import QtQuick.Controls 2.12
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
/*!
\qmltype LoadingErrorComponent
\inherits Control
\inqmlmodule StatusQ.Components
\since StatusQ.Components 0.1
\brief A component that can be used to adding a load error state to a widget
Example:
\qml
AnimatedImage {
id: root
LoadingErrorComponent {
visible: root.status === AnimatedImage.Error
anchors.fill: parent
radius: 8
}
}
\endqml
For a list of components available see StatusQ.
*/
Control {
id: root
/*!
\qmlproperty bool LoadingComponent::radius
This property lets user set custom radius
*/
property int radius: 4
/*!
\qmlproperty string LoadingComponent::text
This property lets user set error message
*/
property string text: qsTr("Failed\nto load")
background: Rectangle {
color: Theme.palette.baseColor5
radius: root.radius
}
contentItem: Item {
Column {
anchors.centerIn: parent
spacing: 10
StatusIcon {
anchors.horizontalCenter: parent.horizontalCenter
icon: "frowny"
opacity: 0.1
color: Theme.palette.directColor1
}
StatusBaseText {
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Qt.AlignHCenter
color: Theme.palette.directColor6
text: root.text
}
}
}
}

View File

@ -85,6 +85,20 @@ StatusRoundedComponent {
*/
property int manualMaxDimension: 0
/*!
\qmlproperty bool StatusRoundedMedia::interactive
Enable mouse interaction with the media.
*/
property bool interactive: false
/*!
\qmlproperty bool StatusRoundedMedia::isEmpty
Media source is empty.
*/
property bool isEmpty: false
readonly property int componentMediaType: {
if (root.mediaType.startsWith("image")) {
return StatusRoundedMedia.MediaType.Image
@ -94,6 +108,11 @@ StatusRoundedComponent {
return StatusRoundedMedia.MediaType.Unknown
}
signal imageClicked(var image, bool plain)
signal videoClicked(var mediaUrl)
signal openImageContextMenu(var url, bool isGif)
signal openVideoContextMenu(var url)
isLoading: {
if (mediaLoader.status === Loader.Ready) {
return mediaLoader.item.isLoading
@ -115,14 +134,23 @@ StatusRoundedComponent {
}
}
Binding on isEmpty {
when: mediaLoader.status === Loader.Ready
value: !!mediaLoader.item && mediaLoader.item.source.toString() === ""
delayed: true
restoreMode: Binding.RestoreBindingOrValue
}
QtObject {
id: d
property bool isFallback: false
property int errorCounter: 0
property bool plainImage: false
function reset() {
isFallback = false
errorCounter = 0
plainImage = false
}
}
@ -141,6 +169,33 @@ StatusRoundedComponent {
else return root.manualMaxDimension
}
MouseArea {
anchors.fill: parent
enabled: root.enabled && root.interactive && mediaLoader.visible && mediaLoader.item
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (root.isError || root.isEmpty) {
return
}
if (mouse.button == Qt.RightButton) {
if (d.isFallback || componentMediaType === StatusRoundedMedia.MediaType.Image) {
root.openImageContextMenu(mediaLoader.item.source, !!mediaLoader.item.playing)
} else if (componentMediaType === StatusRoundedMedia.MediaType.Video) {
root.openVideoContextMenu(mediaUrl)
}
return
}
if (!d.isFallback && componentMediaType === StatusRoundedMedia.MediaType.Video) {
root.videoClicked(root.mediaUrl)
} else {
root.imageClicked(mediaLoader.item, d.plainImage)
}
}
}
Component.onCompleted: updateMediaLoader()
onMediaUrlChanged: updateMediaLoader()
onComponentMediaTypeChanged: updateMediaLoader()
@ -182,6 +237,7 @@ StatusRoundedComponent {
if (!d.isFallback) {
// AnimatedImage sometimes cannot load stuff that plan Image can, try that first
if (componentMediaType === StatusRoundedMedia.MediaType.Image && d.errorCounter <= 1) {
d.plainImage = true
mediaLoader.setSource("StatusImage.qml",
{
"source": root.mediaUrl,
@ -197,6 +253,7 @@ StatusRoundedComponent {
}
function setFallbackImage() {
d.plainImage = true
d.isFallback = true
mediaLoader.setSource("StatusImage.qml",
{
@ -206,6 +263,7 @@ StatusRoundedComponent {
}
function setEmptyComponent() {
d.plainImage = true
mediaLoader.setSource("StatusImage.qml",
{
"source": "",

View File

@ -31,6 +31,7 @@ Item {
readonly property bool isLoading: player.playbackState !== MediaPlayer.PlayingState
readonly property bool isError: player.status === MediaPlayer.InvalidMedia
property alias source: player.source
property alias player: player
property alias output: output
property alias fillMode: output.fillMode

View File

@ -1,6 +1,7 @@
module StatusQ.Components
LoadingComponent 0.1 LoadingComponent.qml
LoadingErrorComponent 0.1 LoadingErrorComponent.qml
StatusAddress 0.1 StatusAddress.qml
StatusAddressPanel 0.1 StatusAddressPanel.qml
StatusAnimatedImage 0.1 StatusAnimatedImage.qml

View File

@ -1,6 +1,7 @@
<RCC>
<qresource prefix="/">
<file>StatusQ/Components/LoadingComponent.qml</file>
<file>StatusQ/Components/LoadingErrorComponent.qml</file>
<file>StatusQ/Components/StatusAddress.qml</file>
<file>StatusQ/Components/StatusAddressPanel.qml</file>
<file>StatusQ/Components/StatusAnimatedImage.qml</file>

View File

@ -14,6 +14,7 @@ import AppLayouts.Communities.panels 1.0
import utils 1.0
import shared.controls 1.0
import shared.views 1.0
import shared.popups 1.0
import "../../stores"
import "../../controls"
@ -149,11 +150,11 @@ Item {
property int modelIndex: index
anchors.top: parent.top
anchors.left: parent.left
sourceComponent: isCollectibleLoading ?
collectibleimage:
sourceComponent: root.isCollectibleLoading ?
collectibleimageComponent:
root.isCommunityCollectible && (root.isOwnerTokenType || root.isTMasterTokenType) ?
privilegedCollectibleImage:
collectibleimage
privilegedCollectibleImageComponent:
collectibleimageComponent
active: root.visible
}
Loader {
@ -345,7 +346,7 @@ Item {
}
}
Component {
id: privilegedCollectibleImage
id: privilegedCollectibleImageComponent
// Special artwork representation for community `Owner and Master Token` token types:
PrivilegedTokenArtworkPanel {
size: PrivilegedTokenArtworkPanel.Size.Large
@ -356,43 +357,52 @@ Item {
}
Component {
id: collectibleimage
StatusRoundedMedia {
id: collectibleImage
readonly property bool isEmpty: !mediaUrl.toString() && !fallbackImageUrl.toString()
radius: Style.current.radius
color: isError || isEmpty ? Theme.palette.baseColor5 : collectible.backgroundColor
mediaUrl: collectible.mediaUrl ?? ""
mediaType: !!collectible ? (modelIndex > 0 && collectible.mediaType.startsWith("video")) ? "" : collectible.mediaType: ""
fallbackImageUrl: collectible.imageUrl
manualMaxDimension: 240
id: collectibleimageComponent
StatusRoundedMedia {
id: collectibleImage
readonly property bool isEmpty: !mediaUrl.toString() && !fallbackImageUrl.toString()
radius: Style.current.radius
color: isError || isEmpty ? Theme.palette.baseColor5 : collectible.backgroundColor
mediaUrl: collectible.mediaUrl ?? ""
mediaType: !!collectible ? (modelIndex > 0 && collectible.mediaType.startsWith("video")) ? "" : collectible.mediaType: ""
fallbackImageUrl: collectible.imageUrl
manualMaxDimension: 240
interactive: !isError && !isEmpty
enabled: interactive
onImageClicked: (image, plain) => Global.openImagePopup(image, "", plain)
onVideoClicked: (url) => Global.openVideoPopup(url)
onOpenImageContextMenu: (url, isGif) => Global.openMenu(imageContextMenu, collectibleImage, { imageSource: url, isGif: isGif, isVideo: false })
onOpenVideoContextMenu: (url) => Global.openMenu(imageContextMenu, collectibleImage, { imageSource: url, url: url, isVideo: true, isGif: false })
Column {
anchors.centerIn: parent
visible: collectibleImage.isError || collectibleImage.isEmpty
spacing: 10
Loader {
anchors.fill: parent
active: collectibleImage.isLoading
sourceComponent: LoadingComponent {radius: collectibleImage.radius}
}
StatusIcon {
anchors.horizontalCenter: parent.horizontalCenter
icon: "frowny"
opacity: 0.1
color: Theme.palette.directColor1
}
StatusBaseText {
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Qt.AlignHCenter
color: Theme.palette.directColor6
text: {
if (collectibleImage.isError && collectibleImage.componentMediaType === StatusRoundedMedia.MediaType.Unkown) {
return qsTr("Unsupported\nfile format")
}
if (!collectible.description && !collectible.name) {
return qsTr("Info can't\nbe fetched")
}
return qsTr("Failed\nto load")
Loader {
anchors.fill: parent
active: collectibleImage.isError || collectibleImage.isEmpty
sourceComponent: LoadingErrorComponent {
radius: collectibleImage.radius
text: {
if (collectibleImage.isError && collectibleImage.componentMediaType === StatusRoundedMedia.MediaType.Unkown) {
return qsTr("Unsupported\nfile format")
}
if (!collectible.description && !collectible.name) {
return qsTr("Info can't\nbe fetched")
}
return qsTr("Failed\nto load")
}
}
}
Component {
id: imageContextMenu
ImageContextMenu {
onClosed: destroy()
}
}
}
}
}

View File

@ -118,7 +118,7 @@ Control {
visible: root.isCommunityCollectible && root.isPrivilegedToken
size: PrivilegedTokenArtworkPanel.Size.Medium
artwork: root.fallbackImageUrl
artwork: visible ? root.fallbackImageUrl : ""
color: root.ornamentColor
fillMode: Image.PreserveAspectCrop
isOwner: root.privilegesLevel === Constants.TokenPrivilegesLevel.Owner

View File

@ -58,6 +58,7 @@ QtObject {
Global.openChooseBrowserPopup.connect(openChooseBrowserPopup)
Global.openDownloadModalRequested.connect(openDownloadModal)
Global.openImagePopup.connect(openImagePopup)
Global.openVideoPopup.connect(openVideoPopup)
Global.openProfilePopupRequested.connect(openProfilePopup)
Global.openNicknamePopupRequested.connect(openNicknamePopup)
Global.markAsUntrustedRequested.connect(openMarkAsUntrustedPopup)
@ -130,8 +131,12 @@ QtObject {
openPopup(downloadPageComponent, popupProperties)
}
function openImagePopup(image, url) {
openPopup(imagePopupComponent, {image: image, url: url})
function openImagePopup(image, url, plain) {
openPopup(imagePopupComponent, {image: image, url: url, plain: plain})
}
function openVideoPopup(url) {
openPopup(videoPopupComponent, { url: url })
}
function openProfilePopup(publicKey: string, parentPopup, cb) {
@ -537,6 +542,14 @@ QtObject {
}
},
Component {
id: videoPopupComponent
StatusVideoModal {
id: videoPopup
onClosed: destroy()
}
},
Component {
id: profilePopupComponent
ProfileDialog {

View File

@ -9,24 +9,25 @@ StatusMenu {
property string imageSource
property string domain
property bool requireConfirmationOnOpen: false
property bool isGif: root.imageSource.toLowerCase().endsWith(".gif")
property bool isVideo: root.imageSource.toLowerCase().endsWith(".mp4")
QtObject {
id: d
readonly property bool isUnfurled: (!!url&&url!=="")
readonly property bool isGif: root.imageSource.toLowerCase().endsWith(".gif")
}
StatusAction {
text: d.isGif ? qsTr("Copy GIF") : qsTr("Copy image")
text: root.isGif ? qsTr("Copy GIF") : qsTr("Copy image")
icon.name: "copy"
enabled: !!root.imageSource
enabled: !!root.imageSource && !root.isVideo
onTriggered: {
Utils.copyImageToClipboardByUrl(root.imageSource)
}
}
StatusAction {
text: d.isGif ? qsTr("Download GIF") : qsTr("Download image")
text: root.isGif ? qsTr("Download GIF") : root.isVideo ? qsTr("Download video") : qsTr("Download image")
icon.name: "download"
enabled: !!root.imageSource
onTriggered: {

View File

@ -32,3 +32,4 @@ SettingsDirtyToastMessage 1.0 SettingsDirtyToastMessage.qml
UnblockContactConfirmationDialog 1.0 UnblockContactConfirmationDialog.qml
UserAgreementPopup 1.0 UserAgreementPopup.qml
UserStatusContextMenu 1.0 UserStatusContextMenu.qml
ImageContextMenu 1.0 ImageContextMenu.qml

View File

@ -1195,7 +1195,7 @@ Rectangle {
}
control.fileUrlsAndSources = urls
}
onImageClicked: (chatImage) => Global.openImagePopup(chatImage, "")
onImageClicked: (chatImage) => Global.openImagePopup(chatImage, "", false)
onLinkReload: (link) => control.linkPreviewReloaded(link)
onLinkClicked: (link) => Global.openLink(link)
onEnableLinkPreview: () => control.enableLinkPreview()

View File

@ -4,10 +4,12 @@ import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import StatusQ.Popups.Dialog 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
import shared 1.0
import shared.views.chat 1.0
import shared.popups 1.0
StatusDialog {
id: root
@ -15,11 +17,10 @@ StatusDialog {
property var store
property var image
property string url: ""
property bool plain: false
width: (root.image.sourceSize.width > d.maxWidth) ?
d.maxWidth : root.image.sourceSize.width
height: (root.image.sourceSize.height > d.maxHeight) ?
d.maxHeight : root.image.sourceSize.height
width: Math.min(root.image.sourceSize.width, d.maxWidth)
height: Math.min(root.image.sourceSize.height, d.maxHeight)
padding: 0
background: null
@ -31,39 +32,81 @@ StatusDialog {
property int maxHeight: Global.applicationWindow.height - 80
property int maxWidth: Global.applicationWindow.width - 80
readonly property int radius: Style.current.radius
}
onOpened: {
messageImage.source = root.image.source;
}
onOpened: imageLoader.source = root.image.source;
onClosed: imageLoader.source = ""
contentItem: Loader {
id: imageLoader
readonly property bool isError: status === Loader.Error || (imageLoader.item && imageLoader.item.status === Image.Error)
readonly property bool isLoading: status === Loader.Loading || (imageLoader.item && imageLoader.item.status === Image.Loading)
property string source
contentItem: AnimatedImage {
id: messageImage
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectFit
mipmap: true
smooth: false
active: true
sourceComponent: root.plain ? plainImage : animatedImage
onStatusChanged: playing = (status == AnimatedImage.Ready)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton)
root.close()
if (mouse.button === Qt.RightButton)
Global.openMenu(imageContextMenu,
messageImage,
{ imageSource: messageImage.source, url: root.url})
if (imageLoader.isError || imageLoader.isLoading || mouse.button !== Qt.RightButton)
return
const isGif = (!root.plain && imageLoader.item && imageLoader.item.playing)
Global.openMenu(imageContextMenu,
imageLoader.item,
{ imageSource: imageLoader.source, url: root.url, isGif: isGif})
}
}
Loader {
anchors.centerIn: parent
width: Math.min(root.width, 300)
height: Math.min(root.height, 300)
active: imageLoader.isError
sourceComponent: LoadingErrorComponent { radius: d.radius }
}
Loader {
anchors.fill: parent
active: imageLoader.isLoading
sourceComponent: LoadingComponent {radius: d.radius}
}
}
Component {
id: animatedImage
AnimatedImage {
asynchronous: true
fillMode: Image.PreserveAspectFit
mipmap: true
smooth: false
onStatusChanged: playing = (status == AnimatedImage.Ready)
source: imageLoader.source
}
}
Component {
id: plainImage
Image {
asynchronous: true
fillMode: Image.PreserveAspectFit
mipmap: true
smooth: false
source: imageLoader.source
}
}
Component {
id: imageContextMenu
ImageContextMenu {
isVideo: false
onClosed: {
destroy()
}

View File

@ -0,0 +1,77 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtMultimedia 5.15
import StatusQ.Popups.Dialog 0.1
import StatusQ.Components 0.1
import utils 1.0
import shared.popups 1.0
StatusDialog {
id: root
property string url: ""
width: Math.min(d.maxWidth, videoItem.output.sourceRect.width)
height: Math.min(d.maxHeight, videoItem.output.sourceRect.height)
padding: 0
background: null
standardButtons: Dialog.NoButton
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
QtObject {
id: d
readonly property int maxHeight: Global.applicationWindow.height - 80
readonly property int maxWidth: Global.applicationWindow.width - 80
}
onOpened: {
videoItem.source = root.url
}
contentItem: StatusVideo {
id: videoItem
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton)
root.close()
if (mouse.button === Qt.RightButton)
Global.openMenu(imageContextMenu, videoItem)
}
}
Loader {
anchors.centerIn: parent
width: Math.min(root.width, 300)
height: Math.min(root.height, 300)
active: videoItem.isError
sourceComponent: LoadingErrorComponent { }
}
Loader {
anchors.fill: parent
active: videoItem.isLoading
sourceComponent: LoadingComponent { }
}
}
Component {
id: imageContextMenu
ImageContextMenu {
isGif: false
isVideo: true
url: root.url
imageSource: root.url
onClosed: {
destroy()
}
}
}
}

View File

@ -15,6 +15,7 @@ StatusEmojiSuggestionPopup 1.0 StatusEmojiSuggestionPopup.qml
StatusExpandableAddress 1.0 StatusExpandableAddress.qml
StatusGifPopup 1.0 StatusGifPopup.qml
StatusImageModal 1.0 StatusImageModal.qml
StatusVideoModal 1.0 StatusVideoModal.qml
StatusImageRadioButton 1.0 StatusImageRadioButton.qml
StatusInputListPopup 1.0 StatusInputListPopup.qml
StatusNotification 1.0 StatusNotification.qml

View File

@ -340,7 +340,7 @@ Loader {
function onImageClicked(image, mouse, imageSource, url = "") {
switch (mouse.button) {
case Qt.LeftButton:
Global.openImagePopup(image, url)
Global.openImagePopup(image, url, false)
break;
case Qt.RightButton:
Global.openMenu(imageContextMenuComponent, image, { imageSource, url })

View File

@ -11,6 +11,5 @@ ProfileHeaderContextMenuView 1.0 ProfileHeaderContextMenuView.qml
TransactionBubbleView 1.0 TransactionBubbleView.qml
NewMessagesMarker 1.0 NewMessagesMarker.qml
SimplifiedMessageView 1.0 SimplifiedMessageView.qml
ImageContextMenu 1.0 ImageContextMenu.qml
ProfileContextMenu 1.0 ProfileContextMenu.qml
MessageAddReactionContextMenu 1.0 MessageAddReactionContextMenu.qml

View File

@ -9,7 +9,7 @@ import StatusQ.Popups.Dialog 0.1
import utils 1.0
import shared.controls 1.0
import shared.views.chat 1.0
import shared.popups 1.0
import shared.controls.chat 1.0
StatusDialog {

View File

@ -38,7 +38,8 @@ QtObject {
signal openDownloadModalRequested(bool available, string version, string url)
signal openChangeProfilePicPopup(var cb)
signal openBackUpSeedPopup()
signal openImagePopup(var image, string url)
signal openImagePopup(var image, string url, bool plain)
signal openVideoPopup(string url)
signal openProfilePopupRequested(string publicKey, var parentPopup, var cb)
signal openActivityCenterPopupRequested()
signal openSendIDRequestPopup(string publicKey, var contactDetails, var cb)