feat: drag and drop images
Allow up to 5 images to be dragged and dropped in to one-on-one chats and in the timeline. Can be combined with the existing upload button. The upload file dialog has been changed to allow multiple selections. Drag and dropped images adhere to the following rules, with corresponding validations messages: - Max 5 image - Image size must be 0.5 MB or less - File extension must be one of [".png", ".jpg", ".jpeg", ".heif", "tif", ".tiff"] Drag and drop and uploaded images are now also deduplicated.
This commit is contained in:
parent
de290727c2
commit
f1e83f74bc
|
@ -224,6 +224,24 @@ QtObject:
|
|||
error "Error sending the image", msg = e.msg
|
||||
result = fmt"Error sending the image: {e.msg}"
|
||||
|
||||
proc sendImages*(self: ChatsView, imagePathsArray: string): string {.slot.} =
|
||||
result = ""
|
||||
try:
|
||||
var images = Json.decode(imagePathsArray, seq[string])
|
||||
let channelId = self.activeChannel.id
|
||||
|
||||
for imagePath in images.mitems:
|
||||
var image = image_utils.formatImagePath(imagePath)
|
||||
imagePath = image_resizer(image, 2000, TMPDIR)
|
||||
|
||||
self.status.chat.sendImages(channelId, images)
|
||||
|
||||
for imagePath in images.items:
|
||||
removeFile(imagePath)
|
||||
except Exception as e:
|
||||
error "Error sending images", msg = e.msg
|
||||
result = fmt"Error sending images: {e.msg}"
|
||||
|
||||
proc activeChannelChanged*(self: ChatsView) {.signal.}
|
||||
|
||||
proc contextChannelChanged*(self: ChatsView) {.signal.}
|
||||
|
|
|
@ -10,6 +10,7 @@ import ../../status/libstatus/settings
|
|||
import ../../status/libstatus/wallet as status_wallet
|
||||
import ../../status/libstatus/utils as status_utils
|
||||
import ../../status/ens as status_ens
|
||||
import ../utils/image_utils
|
||||
import web3/[ethtypes, conversions]
|
||||
import stew/byteutils
|
||||
|
||||
|
@ -99,3 +100,13 @@ QtObject:
|
|||
|
||||
proc getNetworkName*(self: UtilsView): string {.slot.} =
|
||||
getCurrentNetworkDetails().name
|
||||
|
||||
proc getFileSize*(self: UtilsView, filename: string): string {.slot.} =
|
||||
var f: File = nil
|
||||
if f.open(filename.formatImagePath):
|
||||
try:
|
||||
result = $(f.getFileSize())
|
||||
finally:
|
||||
close(f)
|
||||
else:
|
||||
raise newException(IOError, "cannot open: " & filename)
|
||||
|
|
|
@ -247,6 +247,10 @@ proc sendImage*(self: ChatModel, chatId: string, image: string) =
|
|||
var response = status_chat.sendImageMessage(chatId, image)
|
||||
discard self.processMessageUpdateAfterSend(response)
|
||||
|
||||
proc sendImages*(self: ChatModel, chatId: string, images: var seq[string]) =
|
||||
var response = status_chat.sendImageMessages(chatId, images)
|
||||
discard self.processMessageUpdateAfterSend(response)
|
||||
|
||||
proc sendSticker*(self: ChatModel, chatId: string, sticker: Sticker) =
|
||||
var response = status_chat.sendStickerMessage(chatId, sticker)
|
||||
self.events.emit("stickerSent", StickerArgs(sticker: sticker, save: true))
|
||||
|
|
|
@ -166,6 +166,22 @@ proc sendImageMessage*(chatId: string, image: string): string =
|
|||
}
|
||||
])
|
||||
|
||||
proc sendImageMessages*(chatId: string, images: var seq[string]): string =
|
||||
let
|
||||
preferredUsername = getSetting[string](Setting.PreferredUsername, "")
|
||||
debugEcho ">>> [status/libstatus/chat.sendImageMessages] about to send images"
|
||||
let imagesJson = %* images.map(image => %*
|
||||
{
|
||||
"chatId": chatId,
|
||||
"contentType": ContentType.Image.int,
|
||||
"imagePath": image,
|
||||
"ensName": preferredUsername,
|
||||
"text": "Update to latest version to see a nice image here!"
|
||||
}
|
||||
)
|
||||
debugEcho ">>> [status/libstatus/chat.sendImageMessages] imagesJson:", $imagesJson
|
||||
callPrivateRPC("sendChatMessages".prefix, %* [imagesJson])
|
||||
|
||||
proc sendStickerMessage*(chatId: string, sticker: Sticker): string =
|
||||
let preferredUsername = getSetting[string](Setting.PreferredUsername, "")
|
||||
callPrivateRPC("sendChatMessage".prefix, %* [
|
||||
|
@ -358,10 +374,9 @@ proc pendingRequestsToJoinForCommunity*(communityId: string): seq[CommunityMembe
|
|||
|
||||
proc myPendingRequestsToJoin*(): seq[CommunityMembershipRequest] =
|
||||
let rpcResult = callPrivateRPC("myPendingRequestsToJoin".prefix).parseJSON()
|
||||
|
||||
var communityRequests: seq[CommunityMembershipRequest] = @[]
|
||||
|
||||
if rpcResult{"result"}.kind != JNull:
|
||||
if rpcResult{"error"}.kind == JNull and rpcResult{"result"}.kind != JNull:
|
||||
for jsonCommunityReqest in rpcResult["result"]:
|
||||
communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest())
|
||||
|
||||
|
|
|
@ -317,7 +317,7 @@ StackLayout {
|
|||
}
|
||||
onSendMessage: {
|
||||
if (chatInput.fileUrls.length > 0){
|
||||
chatsModel.sendImage(chatInput.fileUrls[0], false);
|
||||
chatsModel.sendImages(JSON.stringify(fileUrls));
|
||||
}
|
||||
let msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text))
|
||||
if (msg.length > 0){
|
||||
|
|
|
@ -71,9 +71,13 @@ ScrollView {
|
|||
anchors.top: parent.top
|
||||
anchors.topMargin: 40
|
||||
chatType: Constants.chatTypeStatusUpdate
|
||||
imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Bottom
|
||||
z: 1
|
||||
onSendMessage: {
|
||||
if (statusUpdateInput.fileUrls.length > 0){
|
||||
chatsModel.sendImage(statusUpdateInput.fileUrls[0], true);
|
||||
statusUpdateInput.fileUrls.forEach(url => {
|
||||
chatsModel.sendImage(url, true);
|
||||
})
|
||||
}
|
||||
var msg = chatsModel.plainText(Emoji.deparse(statusUpdateInput.textInput.text))
|
||||
if (msg.length > 0){
|
||||
|
|
|
@ -15,6 +15,7 @@ import Qt.labs.settings 1.0
|
|||
|
||||
RowLayout {
|
||||
id: appMain
|
||||
property int currentView: sLayout.currentIndex
|
||||
spacing: 0
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
|
|
@ -124,4 +124,10 @@ QtObject {
|
|||
|
||||
readonly property string deepLinkPrefix: 'statusim://'
|
||||
readonly property string joinStatusLink: 'join.status.im'
|
||||
|
||||
readonly property int maxUploadFiles: 5
|
||||
readonly property double maxUploadFilesizeMB: 0.5
|
||||
|
||||
readonly property var acceptedImageExtensions: [".png", ".jpg", ".jpeg", ".svg", ".gif"]
|
||||
readonly property var acceptedDragNDropImageExtensions: [".png", ".jpg", ".jpeg", ".heif", "tif", ".tiff"]
|
||||
}
|
||||
|
|
|
@ -404,6 +404,14 @@ QtObject {
|
|||
}
|
||||
|
||||
function hasImageExtension(url) {
|
||||
return [".png", ".jpg", ".jpeg", ".svg", ".gif"].some(ext => url.includes(ext))
|
||||
return Constants.acceptedImageExtensions.some(ext => url.includes(ext))
|
||||
}
|
||||
|
||||
function hasDragNDropImageExtension(url) {
|
||||
return Constants.acceptedDragNDropImageExtensions.some(ext => url.includes(ext))
|
||||
}
|
||||
|
||||
function deduplicate(array) {
|
||||
return Array.from(new Set(array))
|
||||
}
|
||||
}
|
||||
|
|
80
ui/main.qml
80
ui/main.qml
|
@ -18,6 +18,7 @@ import "./imports"
|
|||
ApplicationWindow {
|
||||
property bool hasAccounts: !!loginModel.rowCount()
|
||||
property bool removeMnemonicAfterLogin: false
|
||||
property alias dragAndDrop: dragTarget
|
||||
|
||||
Universal.theme: Universal.System
|
||||
|
||||
|
@ -247,6 +248,85 @@ ApplicationWindow {
|
|||
property var appSettings
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dragTarget
|
||||
|
||||
signal droppedOnValidScreen(var drop)
|
||||
property alias droppedUrls: rptDraggedPreviews.model
|
||||
readonly property int chatView: Utils.getAppSectionIndex(Constants.chat)
|
||||
readonly property int timelineView: Utils.getAppSectionIndex(Constants.timeline)
|
||||
property bool enabled: containsDrag && loader.item &&
|
||||
(
|
||||
// in chat view
|
||||
(loader.item.currentView === chatView &&
|
||||
(
|
||||
// in a one-to-one chat
|
||||
chatsModel.activeChannel.chatType === Constants.chatTypeOneToOne ||
|
||||
// in a private group chat
|
||||
chatsModel.activeChannel.chatType === Constants.chatTypePrivateGroupChat
|
||||
)
|
||||
) ||
|
||||
// in timeline view
|
||||
loader.item.currentView === timelineView
|
||||
)
|
||||
|
||||
width: applicationWindow.width
|
||||
height: applicationWindow.height
|
||||
|
||||
function cleanup() {
|
||||
rptDraggedPreviews.model = []
|
||||
}
|
||||
|
||||
onDropped: (drop) => {
|
||||
if (enabled) {
|
||||
droppedOnValidScreen(drop)
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
onEntered: {
|
||||
// needed because drag.urls is not a normal js array
|
||||
rptDraggedPreviews.model = drag.urls.filter(img => Utils.hasDragNDropImageExtension(img))
|
||||
}
|
||||
onPositionChanged: {
|
||||
rptDraggedPreviews.x = drag.x
|
||||
rptDraggedPreviews.y = drag.y
|
||||
}
|
||||
onExited: cleanup()
|
||||
Rectangle {
|
||||
id: dropRectangle
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: Style.current.transparent
|
||||
opacity: 0.8
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: dragTarget.enabled
|
||||
PropertyChanges {
|
||||
target: dropRectangle
|
||||
color: Style.current.background
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Repeater {
|
||||
id: rptDraggedPreviews
|
||||
|
||||
Image {
|
||||
source: modelData
|
||||
width: 80
|
||||
height: 80
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
fillMode: Image.PreserveAspectFit
|
||||
x: index * 10 + rptDraggedPreviews.x
|
||||
y: index * 10 + rptDraggedPreviews.y
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: app
|
||||
AppMain {}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../imports"
|
||||
import ".."
|
||||
|
||||
StatusChatImageValidator {
|
||||
id: root
|
||||
|
||||
errorMessage: qsTr("Format not supported.")
|
||||
secondaryErrorMessage: qsTr("Upload %1 only").arg(Constants.acceptedDragNDropImageExtensions.map(ext => ext.replace(".", "").toUpperCase() + "s").join(", "))
|
||||
|
||||
onImagesChanged: {
|
||||
let isValid = true
|
||||
root.validImages = images.filter(img => {
|
||||
const isImage = Utils.hasDragNDropImageExtension(img)
|
||||
isValid = isValid && isImage
|
||||
return isImage
|
||||
})
|
||||
root.isValid = isValid
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../imports"
|
||||
import ".."
|
||||
|
||||
StatusChatImageValidator {
|
||||
id: root
|
||||
errorMessage: qsTr("You can only upload %1 images at a time").arg(Constants.maxUploadFiles)
|
||||
|
||||
onImagesChanged: {
|
||||
let isValid = true
|
||||
if (images.length > Constants.maxUploadFiles) {
|
||||
isValid = false
|
||||
}
|
||||
root.isValid = isValid
|
||||
root.validImages = images.slice(0, Constants.maxUploadFiles)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../imports"
|
||||
import ".."
|
||||
|
||||
StatusChatImageValidator {
|
||||
id: root
|
||||
readonly property int maxImgSizeBytes: Constants.maxUploadFilesizeMB * 1048576 /* 1 MB in bytes */
|
||||
|
||||
onImagesChanged: {
|
||||
let isValid = true
|
||||
root.validImages = images.filter(img => {
|
||||
let size = parseInt(utilsModel.getFileSize(img))
|
||||
const isSmallEnough = size <= maxImgSizeBytes
|
||||
isValid = isValid && isSmallEnough
|
||||
return isSmallEnough
|
||||
})
|
||||
root.isValid = isValid
|
||||
}
|
||||
errorMessage: qsTr("Max image size is %1 MB").arg(Constants.maxUploadFilesizeMB)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../imports"
|
||||
import ".."
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool isValid: true
|
||||
property alias errorMessage: txtValidationError.text
|
||||
property alias secondaryErrorMessage: txtValidationExtraInfo.text
|
||||
property var images: []
|
||||
property var validImages: []
|
||||
|
||||
visible: !isValid
|
||||
width: imgExclamation.width + txtValidationError.width + txtValidationExtraInfo.width + 24
|
||||
height: txtValidationError.height + 14
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Style.current.background
|
||||
radius: Style.current.halfPadding
|
||||
layer.enabled: true
|
||||
layer.effect: DropShadow {
|
||||
verticalOffset: 3
|
||||
radius: 8
|
||||
samples: 15
|
||||
fast: true
|
||||
cached: true
|
||||
color: "#22000000"
|
||||
}
|
||||
|
||||
SVGImage {
|
||||
id: imgExclamation
|
||||
width: 20
|
||||
height: 20
|
||||
sourceSize.height: height * 2
|
||||
sourceSize.width: width * 2
|
||||
verticalAlignment: Image.AlignVCenter
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 6
|
||||
anchors.leftMargin: 6
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../../app/img/exclamation_outline.svg"
|
||||
}
|
||||
StyledText {
|
||||
id: txtValidationError
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.top: parent.top
|
||||
anchors.left: imgExclamation.right
|
||||
anchors.topMargin: 7
|
||||
anchors.leftMargin: 6
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 13
|
||||
height: 18
|
||||
color: Style.current.danger
|
||||
}
|
||||
StyledText {
|
||||
id: txtValidationExtraInfo
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.top: parent.top
|
||||
anchors.left: txtValidationError.right
|
||||
anchors.topMargin: 7
|
||||
anchors.leftMargin: 6
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 13
|
||||
height: 18
|
||||
color: Style.current.textColor
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,6 +44,13 @@ Rectangle {
|
|||
property alias suggestionsList: suggestions
|
||||
property alias suggestions: suggestionsBox
|
||||
|
||||
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top
|
||||
|
||||
enum ImageErrorMessageLocation {
|
||||
Top,
|
||||
Bottom
|
||||
}
|
||||
|
||||
height: {
|
||||
if (extendedArea.visible) {
|
||||
return messageInput.height + extendedArea.height + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding)
|
||||
|
@ -120,6 +127,7 @@ Rectangle {
|
|||
if (messageInputField.length < messageLimit) {
|
||||
control.sendMessage(event)
|
||||
control.hideExtendedArea();
|
||||
event.accepted = true
|
||||
return;
|
||||
}
|
||||
if(event) event.accepted = true
|
||||
|
@ -255,7 +263,7 @@ Rectangle {
|
|||
|
||||
if (madeChanges) {
|
||||
messageInputField.remove(0, messageInputField.length);
|
||||
insertInTextInput(0, Emoji.parse(words.join(' ')));
|
||||
insertInTextInput(0, Emoji.parse(words.join(' ')));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -388,17 +396,33 @@ Rectangle {
|
|||
isImage = false;
|
||||
isReply = false;
|
||||
control.fileUrls = []
|
||||
imageArea.imageSource = "";
|
||||
imageArea.imageSource = [];
|
||||
replyArea.userName = ""
|
||||
replyArea.identicon = ""
|
||||
replyArea.message = ""
|
||||
for (let i=0; i<validators.children.length; i++) {
|
||||
const validator = validators.children[i]
|
||||
validator.images = []
|
||||
}
|
||||
}
|
||||
|
||||
function showImageArea(imagePath) {
|
||||
function validateImages(imagePaths) {
|
||||
// needed because imageArea.imageSource is not a normal js array
|
||||
const existing = (imageArea.imageSource || []).map(x => x.toString())
|
||||
let validImages = Utils.deduplicate(existing.concat(imagePaths))
|
||||
for (let i=0; i<validators.children.length; i++) {
|
||||
const validator = validators.children[i]
|
||||
validator.images = validImages
|
||||
validImages = validImages.filter(validImage => validator.validImages.includes(validImage))
|
||||
}
|
||||
return validImages
|
||||
}
|
||||
|
||||
function showImageArea(imagePaths) {
|
||||
isImage = true;
|
||||
isReply = false;
|
||||
control.fileUrls = imageDialog.fileUrls
|
||||
imageArea.imageSource = control.fileUrls[0]
|
||||
imageArea.imageSource = imagePaths
|
||||
control.fileUrls = imageArea.imageSource
|
||||
}
|
||||
|
||||
function showReplyArea(userName, message, identicon) {
|
||||
|
@ -409,24 +433,37 @@ Rectangle {
|
|||
messageInputField.forceActiveFocus();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: applicationWindow.dragAndDrop
|
||||
onDroppedOnValidScreen: (drop) => {
|
||||
let validImages = validateImages(drop.urls)
|
||||
if (validImages.length > 0) {
|
||||
showImageArea(validImages)
|
||||
drop.acceptProposedAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: suggestions
|
||||
}
|
||||
|
||||
|
||||
FileDialog {
|
||||
id: imageDialog
|
||||
//% "Please choose an image"
|
||||
title: qsTrId("please-choose-an-image")
|
||||
folder: shortcuts.pictures
|
||||
selectMultiple: true
|
||||
nameFilters: [
|
||||
//% "Image files (*.jpg *.jpeg *.png)"
|
||||
qsTrId("image-files----jpg---jpeg---png-")
|
||||
qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))
|
||||
]
|
||||
onAccepted: {
|
||||
imageBtn.highlighted = false
|
||||
imageBtn2.highlighted = false
|
||||
control.showImageArea()
|
||||
let validImages = validateImages(imageDialog.fileUrls)
|
||||
if (validImages.length > 0) {
|
||||
control.showImageArea(validImages)
|
||||
}
|
||||
messageInputField.forceActiveFocus();
|
||||
}
|
||||
onRejected: {
|
||||
|
@ -582,6 +619,26 @@ Rectangle {
|
|||
radius: control.isStatusUpdateInput ? 36 :
|
||||
height > defaultInputFieldHeight + 1 || extendedArea.visible ? 16 : 32
|
||||
|
||||
ColumnLayout {
|
||||
id: validators
|
||||
anchors.bottom: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? extendedArea.top : undefined
|
||||
anchors.bottomMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? -4 : undefined
|
||||
anchors.top: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? extendedArea.bottom : undefined
|
||||
anchors.topMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? (isImage ? -4 : 4) : undefined
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width
|
||||
z: 1
|
||||
StatusChatImageExtensionValidator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
StatusChatImageSizeValidator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
StatusChatImageQtyValidator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: extendedArea
|
||||
visible: isImage || isReply
|
||||
|
@ -625,9 +682,13 @@ Rectangle {
|
|||
anchors.top: parent.top
|
||||
anchors.topMargin: control.isStatusUpdateInput ? 0 : Style.current.halfPadding
|
||||
visible: isImage
|
||||
width: messageInputField.width - actions.width
|
||||
onImageRemoved: {
|
||||
control.fileUrls = []
|
||||
isImage = false
|
||||
if (control.fileUrls.length > index && control.fileUrls[index]) {
|
||||
control.fileUrls.splice(index, 1)
|
||||
}
|
||||
isImage = control.fileUrls.length > 0
|
||||
validateImages(control.fileUrls)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,90 +4,93 @@ import QtQuick.Controls 2.13
|
|||
import "../../imports"
|
||||
import "../../shared"
|
||||
|
||||
Rectangle {
|
||||
Row {
|
||||
id: imageArea
|
||||
height: chatImage.height
|
||||
spacing: Style.current.halfPadding
|
||||
|
||||
signal imageRemoved()
|
||||
property url imageSource: ""
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: chatImage
|
||||
property bool hovered: false
|
||||
height: 64
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
smooth: false
|
||||
antialiasing: true
|
||||
source: parent.imageSource
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
chatImage.hovered = true
|
||||
}
|
||||
onExited: {
|
||||
chatImage.hovered = false
|
||||
}
|
||||
}
|
||||
signal imageRemoved(int index)
|
||||
property alias imageSource: rptImages.model
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Item {
|
||||
width: chatImage.width
|
||||
height: chatImage.height
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: chatImage.width
|
||||
height: chatImage.height
|
||||
radius: 16
|
||||
Repeater {
|
||||
id: rptImages
|
||||
Item {
|
||||
height: chatImage.paintedHeight + closeBtn.height - 5
|
||||
width: chatImage.width
|
||||
Image {
|
||||
id: chatImage
|
||||
property bool hovered: false
|
||||
width: 64
|
||||
height: 64
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
mipmap: true
|
||||
smooth: false
|
||||
antialiasing: true
|
||||
source: modelData
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
chatImage.hovered = true
|
||||
}
|
||||
onExited: {
|
||||
chatImage.hovered = false
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 4
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Item {
|
||||
width: chatImage.width
|
||||
height: chatImage.height
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: chatImage.width
|
||||
height: chatImage.height
|
||||
radius: 16
|
||||
}
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RoundButton {
|
||||
id: closeBtn
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
padding: 0
|
||||
anchors.top: chatImage.top
|
||||
anchors.topMargin: -5
|
||||
anchors.right: chatImage.right
|
||||
anchors.rightMargin: -Style.current.halfPadding
|
||||
visible: chatImage.hovered || hovered
|
||||
contentItem: SVGImage {
|
||||
source: !closeBtn.hovered ?
|
||||
"../../app/img/close-filled.svg" : "../../app/img/close-filled-hovered.svg"
|
||||
width: closeBtn.width
|
||||
height: closeBtn.height
|
||||
}
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
onClicked: {
|
||||
imageArea.imageRemoved(index)
|
||||
const tmp = imageArea.imageSource.filter((url, idx) => idx !== index)
|
||||
rptImages.model = tmp
|
||||
}
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
onPressed: mouse.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RoundButton {
|
||||
id: closeBtn
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
padding: 0
|
||||
anchors.top: chatImage.top
|
||||
anchors.topMargin: -5
|
||||
anchors.right: chatImage.right
|
||||
anchors.rightMargin: -Style.current.halfPadding
|
||||
visible: chatImage.hovered || hovered
|
||||
contentItem: SVGImage {
|
||||
source: !closeBtn.hovered ?
|
||||
"../../app/img/close-filled.svg" : "../../app/img/close-filled-hovered.svg"
|
||||
width: closeBtn.width
|
||||
height: closeBtn.height
|
||||
}
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
onClicked: {
|
||||
imageArea.imageRemoved()
|
||||
imageArea.imageSource = ""
|
||||
}
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
onPressed: mouse.accepted = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue