1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-13 19:25:40 +02:00

chatview: image scaling algorithm

Revise the image scaling algorithm to deal with images
whose aspect ratio was too large or small. Deals with
oversized images as well as images which are too small.

GitLab: #1437
Change-Id: I454e64972ccde1415d80182a2aa89db9656fec1b
This commit is contained in:
Andreas Hatziiliou 2024-12-24 14:08:34 -05:00 committed by François-Simon Fauteux-Chapleau
parent d1a8ec3af0
commit 32a44c9820
2 changed files with 99 additions and 126 deletions

View file

@ -22,7 +22,6 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import net.jami.Models 1.1 import net.jami.Models 1.1
import net.jami.Constants 1.1 import net.jami.Constants 1.1
import net.jami.Adapters 1.1 import net.jami.Adapters 1.1
@ -44,12 +43,12 @@ Loader {
property int transferStatus: TransferStatus property int transferStatus: TransferStatus
onTidChanged: { onTidChanged: {
if (tid === "") { if (tid === "") {
sourceComponent = deletedMsgComp sourceComponent = deletedMsgComp;
} }
} }
onTransferStatusChanged: { onTransferStatusChanged: {
if (tid === "") { if (tid === "") {
sourceComponent = deletedMsgComp sourceComponent = deletedMsgComp;
return; return;
} else if (transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED) { } else if (transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED) {
mediaInfo = MessagesAdapter.getMediaInfo(root.body); mediaInfo = MessagesAdapter.getMediaInfo(root.body);
@ -64,7 +63,11 @@ Loader {
width: ListView.view ? ListView.view.width : 0 width: ListView.view ? ListView.view.width : 0
opacity: 0 opacity: 0
Behavior on opacity { NumberAnimation { duration: 100 } } Behavior on opacity {
NumberAnimation {
duration: 100
}
}
onLoaded: opacity = 1 onLoaded: opacity = 1
Component { Component {
@ -93,7 +96,7 @@ Loader {
bottomPadding: 6 bottomPadding: 6
topPadding: 6 topPadding: 6
leftPadding: 10 leftPadding: 10
text: UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author) + " " + JamiStrings.deletedMedia ; text: UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author) + " " + JamiStrings.deletedMedia
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
width: Math.min((2 / 3) * parent.width, implicitWidth + 18, innerContent.width - senderMargin + 18) width: Math.min((2 / 3) * parent.width, implicitWidth + 18, innerContent.width - senderMargin + 18)
@ -107,8 +110,8 @@ Loader {
opacity: 0.5 opacity: 0.5
function getBaseColor() { function getBaseColor() {
bubble.isDeleted = true bubble.isDeleted = true;
return UtilsAdapter.luma(bubble.color) ? "white" : "dark" return UtilsAdapter.luma(bubble.color) ? "white" : "dark";
} }
} }
] ]
@ -124,9 +127,7 @@ Loader {
transferId: Id transferId: Id
property var transferStats: MessagesAdapter.getTransferStats(transferId, root.transferStatus) property var transferStats: MessagesAdapter.getTransferStats(transferId, root.transferStatus)
property bool canOpen: root.transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED || isOutgoing property bool canOpen: root.transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED || isOutgoing
property real maxMsgWidth: root.width - senderMargin - property real maxMsgWidth: root.width - senderMargin - 2 * hPadding - avatarBlockWidth - buttonsLoader.width - 24 - 6 - 24
2 * hPadding - avatarBlockWidth
- buttonsLoader.width - 24 - 6 - 24
isOutgoing: Author === CurrentAccount.uri isOutgoing: Author === CurrentAccount.uri
showTime: root.showTime showTime: root.showTime
@ -150,14 +151,12 @@ Loader {
enabled: canOpen enabled: canOpen
onHoveredChanged: { onHoveredChanged: {
if (enabled && hovered) { if (enabled && hovered) {
dataTransferItem.hoveredLink = UtilsAdapter.urlFromLocalPath(location) dataTransferItem.hoveredLink = UtilsAdapter.urlFromLocalPath(location);
} else { } else {
dataTransferItem.hoveredLink = "" dataTransferItem.hoveredLink = "";
} }
} }
cursorShape: enabled ? cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
Qt.PointingHandCursor :
Qt.ArrowCursor
} }
Loader { Loader {
id: buttonsLoader id: buttonsLoader
@ -171,21 +170,21 @@ Loader {
switch (root.transferStatus) { switch (root.transferStatus) {
case Interaction.TransferStatus.TRANSFER_CREATED: case Interaction.TransferStatus.TRANSFER_CREATED:
case Interaction.TransferStatus.TRANSFER_FINISHED: case Interaction.TransferStatus.TRANSFER_FINISHED:
iconSource = JamiResources.link_black_24dp_svg iconSource = JamiResources.link_black_24dp_svg;
return terminatedComp return terminatedComp;
case Interaction.TransferStatus.TRANSFER_CANCELED: case Interaction.TransferStatus.TRANSFER_CANCELED:
case Interaction.TransferStatus.TRANSFER_ERROR: case Interaction.TransferStatus.TRANSFER_ERROR:
case Interaction.TransferStatus.TRANSFER_UNJOINABLE_PEER: case Interaction.TransferStatus.TRANSFER_UNJOINABLE_PEER:
case Interaction.TransferStatus.TRANSFER_TIMEOUT_EXPIRED: case Interaction.TransferStatus.TRANSFER_TIMEOUT_EXPIRED:
case Interaction.TransferStatus.TRANSFER_AWAITING_HOST: case Interaction.TransferStatus.TRANSFER_AWAITING_HOST:
iconSource = JamiResources.download_black_24dp_svg iconSource = JamiResources.download_black_24dp_svg;
return optionsComp return optionsComp;
case Interaction.TransferStatus.TRANSFER_ONGOING: case Interaction.TransferStatus.TRANSFER_ONGOING:
iconSource = JamiResources.close_black_24dp_svg iconSource = JamiResources.close_black_24dp_svg;
return optionsComp return optionsComp;
default: default:
iconSource = JamiResources.error_outline_black_24dp_svg iconSource = JamiResources.error_outline_black_24dp_svg;
return terminatedComp return terminatedComp;
} }
} }
Component { Component {
@ -216,9 +215,9 @@ Loader {
imageColor: JamiTheme.chatviewButtonColor imageColor: JamiTheme.chatviewButtonColor
onClicked: { onClicked: {
if (root.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING) { if (root.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING) {
return MessagesAdapter.cancelFile(transferId) return MessagesAdapter.cancelFile(transferId);
} else { } else {
return MessagesAdapter.acceptFile(transferId) return MessagesAdapter.acceptFile(transferId);
} }
} }
} }
@ -230,27 +229,21 @@ Loader {
TextEdit { TextEdit {
width: Math.min(implicitWidth, maxMsgWidth) width: Math.min(implicitWidth, maxMsgWidth)
topPadding: 10 topPadding: 10
text: CurrentConversation.isSwarm ? text: CurrentConversation.isSwarm ? transferName : location
transferName :
location
wrapMode: Label.WrapAtWordBoundaryOrAnywhere wrapMode: Label.WrapAtWordBoundaryOrAnywhere
font.pointSize: 11 font.pointSize: 11
renderType: Text.NativeRendering renderType: Text.NativeRendering
readOnly: true readOnly: true
color: UtilsAdapter.luma(bubble.color) color: UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
? JamiTheme.chatviewTextColorLight
: JamiTheme.chatviewTextColorDark
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: canOpen ? cursorShape: canOpen ? Qt.PointingHandCursor : Qt.ArrowCursor
Qt.PointingHandCursor :
Qt.ArrowCursor
onClicked: function (mouse) { onClicked: function (mouse) {
if (canOpen) { if (canOpen) {
dataTransferItem.hoveredLink = UtilsAdapter.urlFromLocalPath(location) dataTransferItem.hoveredLink = UtilsAdapter.urlFromLocalPath(location);
Qt.openUrlExternally(new URL(dataTransferItem.hoveredLink)) Qt.openUrlExternally(new URL(dataTransferItem.hoveredLink));
} else { } else {
dataTransferItem.hoveredLink = "" dataTransferItem.hoveredLink = "";
} }
} }
} }
@ -261,23 +254,20 @@ Loader {
width: Math.min(implicitWidth, maxMsgWidth) width: Math.min(implicitWidth, maxMsgWidth)
bottomPadding: 10 bottomPadding: 10
text: { text: {
var res = "" var res = "";
if (transferStats.totalSize !== undefined) { if (transferStats.totalSize !== undefined) {
if (transferStats.progress !== 0 && if (transferStats.progress !== 0 && transferStats.progress !== transferStats.totalSize) {
transferStats.progress !== transferStats.totalSize) { res += UtilsAdapter.humanFileSize(transferStats.progress) + " / ";
res += UtilsAdapter.humanFileSize(transferStats.progress) + " / "
} }
var totalSize = transferStats.totalSize !== 0 ? transferStats.totalSize : TotalSize var totalSize = transferStats.totalSize !== 0 ? transferStats.totalSize : TotalSize;
res += UtilsAdapter.humanFileSize(totalSize) res += UtilsAdapter.humanFileSize(totalSize);
} }
return res return res;
} }
wrapMode: Label.WrapAtWordBoundaryOrAnywhere wrapMode: Label.WrapAtWordBoundaryOrAnywhere
font.pointSize: 10 font.pointSize: 10
renderType: Text.NativeRendering renderType: Text.NativeRendering
color: UtilsAdapter.luma(bubble.color) color: UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
? JamiTheme.chatviewTextColorLight
: JamiTheme.chatviewTextColorDark
} }
} }
}, },
@ -316,23 +306,23 @@ Loader {
Component.onCompleted: { Component.onCompleted: {
if (transferStats.totalSize !== undefined) { if (transferStats.totalSize !== undefined) {
var totalSize = transferStats.totalSize !== 0 ? transferStats.totalSize : TotalSize var totalSize = transferStats.totalSize !== 0 ? transferStats.totalSize : TotalSize;
var txt = UtilsAdapter.humanFileSize(totalSize) var txt = UtilsAdapter.humanFileSize(totalSize);
} }
bubble.timestampItem.timeLabel.text += " - " + txt bubble.timestampItem.timeLabel.text += " - " + txt;
bubble.color = "transparent" bubble.color = "transparent";
if (mediaInfo.isImage) if (mediaInfo.isImage)
bubble.z = 1 bubble.z = 1;
else else
timeUnderBubble = true timeUnderBubble = true;
} }
onContentWidthChanged: { onContentWidthChanged: {
if (bubble.timestampItem.timeLabel.width > contentWidth) if (bubble.timestampItem.timeLabel.width > contentWidth)
timeUnderBubble = true timeUnderBubble = true;
else { else {
bubble.timestampItem.timeColor = JamiTheme.whiteColor bubble.timestampItem.timeColor = JamiTheme.whiteColor;
bubble.timestampItem.timeLabel.opacity = 1 bubble.timestampItem.timeLabel.opacity = 1;
} }
} }
@ -346,10 +336,10 @@ Loader {
height: sourceComponent.height height: sourceComponent.height
sourceComponent: { sourceComponent: {
if (mediaInfo.isImage) if (mediaInfo.isImage)
return imageComp return imageComp;
if (mediaInfo.isAnimatedImage) if (mediaInfo.isAnimatedImage)
return animatedImageComp return animatedImageComp;
return avComp return avComp;
} }
Component { Component {
@ -357,10 +347,11 @@ Loader {
Loader { Loader {
Component.onCompleted: { Component.onCompleted: {
var qml = WITH_WEBENGINE ? var qml = WITH_WEBENGINE ? "qrc:/webengine/MediaPreviewBase.qml" : "qrc:/nowebengine/MediaPreviewBase.qml";
"qrc:/webengine/MediaPreviewBase.qml" : setSource(qml, {
"qrc:/nowebengine/MediaPreviewBase.qml" isVideo: mediaInfo.isVideo,
setSource( qml, { isVideo: mediaInfo.isVideo, html: mediaInfo.html } ) html: mediaInfo.html
});
} }
} }
} }
@ -381,9 +372,7 @@ Loader {
asynchronous: true asynchronous: true
source: UtilsAdapter.urlFromLocalPath(Body) source: UtilsAdapter.urlFromLocalPath(Body)
property real aspectRatio: implicitWidth / implicitHeight property real aspectRatio: implicitWidth / implicitHeight
property real adjustedWidth: Math.min(maxSize, property real adjustedWidth: Math.min(maxSize, Math.max(minSize, innerContent.width - senderMargin))
Math.max(minSize,
innerContent.width - senderMargin))
width: adjustedWidth width: adjustedWidth
height: Math.ceil(adjustedWidth / aspectRatio) height: Math.ceil(adjustedWidth / aspectRatio)
Rectangle { Rectangle {
@ -403,7 +392,7 @@ Loader {
} }
onWidthChanged: { onWidthChanged: {
localMediaMsgItem.contentWidth = width localMediaMsgItem.contentWidth = width;
} }
Component.onCompleted: localMediaMsgItem.bubble.imgSource = source Component.onCompleted: localMediaMsgItem.bubble.imgSource = source
@ -429,10 +418,23 @@ Loader {
Component { Component {
id: imageComp id: imageComp
Rectangle {
border.color: img.useBox ? (JamiTheme.darkTheme ? "white" : JamiTheme.blackColor) : JamiTheme.transparentColor
color: JamiTheme.transparentColor
anchors.right: isOutgoing ? parent.right : undefined
border.width: 1
radius: msgRadius
implicitWidth: img.width + (img.useBox ? 20 : 0)
implicitHeight: img.height + (img.useBox ? 20 : 0)
onWidthChanged: {
localMediaMsgItem.contentWidth = width;
}
Image { Image {
id: img id: img
anchors.right: isOutgoing ? parent.right : undefined anchors.centerIn: parent
cache: true cache: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
mipmap: true mipmap: true
@ -445,33 +447,18 @@ Loader {
localMediaMsgItem.bubble.imgSource = source; localMediaMsgItem.bubble.imgSource = source;
} }
// The sourceSize represents the maximum source dimensions. // Scale down the image if it's too wide or too tall.
// This should not be a dynamic binding, as property changes property real maxWidth: localMediaMsgItem.width - 170
// (resizing the chat view) here will trigger a reload of the image. property bool xOverflow: sourceSize.width > maxWidth
sourceSize: Qt.size(256, 256) property bool yOverflow: sourceSize.height > JamiTheme.maxImageHeight
property real scaleFactor: (xOverflow || yOverflow) ? Math.min(maxWidth / sourceSize.width, JamiTheme.maxImageHeight / sourceSize.height) : 1
width: sourceSize.width * scaleFactor
height: sourceSize.height * scaleFactor
// Now we setup bindings for the destination image component size. // Add a bounding box around the image if it's small (along at least one
// This based on the width available (width of the chat view), and // dimension) to ensure that it's easy for users to see it and click on it.
// a restriction on the height. property bool useBox: (paintedWidth < 40) || (paintedHeight < 40)
readonly property real aspectRatio: paintedWidth / paintedHeight layer.enabled: !useBox
readonly property real idealWidth: innerContent.width - senderMargin
onStatusChanged: {
if (img.status == Image.Ready && aspectRatio) {
height = Qt.binding(() => JamiQmlUtils.clamp(idealWidth / aspectRatio, 64, 256))
width = Qt.binding(() => height * aspectRatio)
}
}
onWidthChanged: {
localMediaMsgItem.contentWidth = width
}
Rectangle {
color: JamiTheme.previewImageBackgroundColor
z: -1
anchors.fill: parent
}
layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {
maskSource: MessageBubble { maskSource: MessageBubble {
out: isOutgoing out: isOutgoing
@ -481,21 +468,6 @@ Loader {
radius: msgRadius radius: msgRadius
} }
} }
LinearGradient {
id: gradient
anchors.fill: parent
start: Qt.point(0, height / 3)
gradient: Gradient {
GradientStop {
position: 0.0
color: JamiTheme.transparentColor
}
GradientStop {
position: 1.0
color: JamiTheme.darkGreyColorOpacityFade
}
}
} }
} }
} }

View file

@ -255,6 +255,7 @@ Item {
property color messageWebViewFooterButtonImageColor: darkTheme ? "#838383" : "#656565" property color messageWebViewFooterButtonImageColor: darkTheme ? "#838383" : "#656565"
property color chatviewSecondaryInformationColor: "#A7A7A7" property color chatviewSecondaryInformationColor: "#A7A7A7"
property color draftIconColor: "#707070" property color draftIconColor: "#707070"
property real maxImageHeight: 375
// ChatView Footer // ChatView Footer
property color chatViewFooterListColor: darkTheme ? blackColor : "#E5E5E5" property color chatViewFooterListColor: darkTheme ? blackColor : "#E5E5E5"