diff --git a/imports/NeoChat/Component/FullScreenImage.qml b/imports/NeoChat/Component/FullScreenImage.qml index 234ee62db..48cc9dcc2 100644 --- a/imports/NeoChat/Component/FullScreenImage.qml +++ b/imports/NeoChat/Component/FullScreenImage.qml @@ -3,10 +3,12 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import Qt.labs.platform 1.1 import org.kde.kirigami 2.15 as Kirigami -ApplicationWindow { +Popup { id: root property alias source: image.source @@ -16,74 +18,291 @@ ApplicationWindow { property int imageHeight: -1 property var modelData - flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground + parent: Overlay.overlay + closePolicy: Popup.CloseOnEscape + width: parent.width + height: parent.height + modal: true + padding: 0 + background: null - title: i18n("Image View - %1", filename) + ColumnLayout { + anchors.fill: parent + spacing: Kirigami.Units.largeSpacing - Shortcut { - sequence: "Escape" - onActivated: root.destroy() - } + Control { + Layout.fillWidth: true - color: Kirigami.Theme.backgroundColor + contentItem: RowLayout { + spacing: Kirigami.Units.largeSpacing - background: AbstractButton { - onClicked: root.destroy() - } + Kirigami.Avatar { + id: avatar - BusyIndicator { - visible: image.status !== Image.Ready && root.blurhash === "" - anchors.centerIn: parent - running: visible - } + Layout.preferredWidth: Kirigami.Units.iconSizes.medium + Layout.preferredHeight: Kirigami.Units.iconSizes.medium - AnimatedImage { - id: image - anchors.centerIn: parent + name: modelData.author.name ?? modelData.author.displayName + source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : "" + color: modelData.author.color + } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 - width: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, root.width) - height: Math.min(root.imageHeight !== -1 ? root.imageWidth : sourceSize.height, root.height) + Label { + id: nameLabel - fillMode: Image.PreserveAspectFit + text: modelData.author.displayName + textFormat: Text.PlainText + font.weight: Font.Bold + color: author.color + } + Label { + id: timeLabel - Image { - anchors.centerIn: parent - width: image.width - height: image.height - source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : "" - visible: root.blurhash !== "" && parent.status !== Image.Ready + text: time.toLocaleString(Qt.locale(), Locale.ShortFormat) + } + } + Label { + id: imageLabel + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + + text: modelData.display + font.weight: Font.Bold + elide: Text.ElideRight + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Zoom in") + Accessible.name: text + icon.name: "zoom-in" + display: AbstractButton.IconOnly + onClicked: { + image.scaleFactor = image.scaleFactor + 0.25 + if (image.scaleFactor > 3) { + image.scaleFactor = 3 + } + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Zoom out") + Accessible.name: text + icon.name: "zoom-out" + display: AbstractButton.IconOnly + onClicked: { + image.scaleFactor = image.scaleFactor - 0.25 + if (image.scaleFactor < 0.25) { + image.scaleFactor = 0.25 + } + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Rotate left") + Accessible.name: text + icon.name: "image-rotate-left-symbolic" + display: AbstractButton.IconOnly + onClicked: image.rotationAngle = image.rotationAngle - 90 + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Rotate right") + Accessible.name: text + icon.name: "image-rotate-right-symbolic" + display: AbstractButton.IconOnly + onClicked: image.rotationAngle = image.rotationAngle + 90 + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Save as") + Accessible.name: text + icon.name: "document-save" + display: AbstractButton.IconOnly + onClicked: { + var dialog = saveAsDialog.createObject(ApplicationWindow.overlay) + dialog.open() + dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Close") + Accessible.name: text + icon.name: "dialog-close" + display: AbstractButton.IconOnly + onClicked: { + root.close() + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + } + + background: Rectangle { + color: Kirigami.Theme.alternateBackgroundColor + } + + Kirigami.Separator { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 1 + } } - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: { - const contextMenu = fileDelegateContextMenu.createObject(parent, { - author: modelData.author, - message: modelData.message, - eventId: modelData.eventId, - source: modelData.source, - file: root.parent, - mimeType: modelData.mimeType, - progressInfo: modelData.progressInfo, - plainMessage: modelData.message, - }); - contextMenu.closeFullscreen.connect(root.destroy) - contextMenu.open(); + BusyIndicator { + Layout.fillWidth: true + visible: image.status !== Image.Ready && root.blurhash === "" + running: visible + } + // Provides container to fill the space that isn't taken up by the top controls and clips the image when zooming makes it larger than the available area. + Item { + id: imageContainer + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.bottomMargin: Kirigami.Units.largeSpacing + clip: true + + Image { + id: image + + property var scaleFactor: 1 + property int rotationAngle: 0 + property var rotationInsensitiveWidth: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, imageContainer.width - Kirigami.Units.largeSpacing * 2) + property var rotationInsensitiveHeight: Math.min(root.imageHeight !== -1 ? root.imageHeight : sourceSize.height, imageContainer.height - Kirigami.Units.largeSpacing * 2) + + anchors.centerIn: parent + width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight + height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth + fillMode: Image.PreserveAspectFit + clip: true + + Behavior on width { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + Behavior on height { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + + Image { + anchors.centerIn: parent + width: image.width + height: image.height + source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : "" + visible: root.blurhash !== "" && parent.status !== Image.Ready + } + + transform: [ + Rotation { + origin.x: image.width / 2 + origin.y: image.height / 2 + angle: image.rotationAngle + + Behavior on angle { + RotationAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + }, + Scale { + origin.x: image.width / 2 + origin.y: image.height / 2 + xScale: image.scaleFactor + yScale: image.scaleFactor + + Behavior on xScale { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + Behavior on yScale { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + } + ] + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + const contextMenu = fileDelegateContextMenu.createObject(parent, { + author: modelData.author, + message: modelData.message, + eventId: modelData.eventId, + source: modelData.source, + file: root.parent, + mimeType: modelData.mimeType, + progressInfo: modelData.progressInfo, + plainMessage: modelData.message, + }); + contextMenu.closeFullscreen.connect(root.close) + contextMenu.open(); + } + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + root.close() + } } } } - Button { - anchors.top: parent.top - anchors.right: parent.right + Component { + id: saveAsDialog + FileDialog { + fileMode: FileDialog.SaveFile + folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + onAccepted: { + if (!currentFile) { + return; + } + currentRoom.downloadFile(eventId, currentFile) + } + } + } - text: i18n("Close") - icon.name: "dialog-close" - display: AbstractButton.IconOnly - - width: Kirigami.Units.gridUnit * 2 - height: Kirigami.Units.gridUnit * 2 - - onClicked: root.destroy() + onClosed: { + image.scaleFactor = 1 + image.rotationAngle = 0 } } diff --git a/imports/NeoChat/Component/Timeline/ImageDelegate.qml b/imports/NeoChat/Component/Timeline/ImageDelegate.qml index c03493d2d..f77e9a8f5 100644 --- a/imports/NeoChat/Component/Timeline/ImageDelegate.qml +++ b/imports/NeoChat/Component/Timeline/ImageDelegate.qml @@ -6,6 +6,8 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import Qt.labs.platform 1.1 +import org.kde.kirigami 2.15 as Kirigami + import org.kde.neochat 1.0 import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 @@ -50,6 +52,7 @@ TimelineContainer { ToolTip.text: model.display ToolTip.visible: hoverHandler.hovered + ToolTip.delay: Kirigami.Units.toolTipDelay HoverHandler { id: hoverHandler @@ -89,17 +92,21 @@ TimelineContainer { acceptedButtons: Qt.LeftButton onLongPressed: openFileContext(model, parent) onTapped: { - fullScreenImage.createObject(parent, { - filename: eventId, - source: mediaUrl, - blurhash: model.content.info["xyz.amorgan.blurhash"], - imageWidth: content.info.w, - imageHeight: content.info.h, - modelData: model - }).showFullScreen(); + img.ToolTip.hide() + fullScreenImage.open() } } + FullScreenImage { + id: fullScreenImage + filename: eventId + source: mediaUrl + blurhash: model.content.info["xyz.amorgan.blurhash"] + imageWidth: content.info.w + imageHeight: content.info.h + modelData: model + } + function downloadAndOpen() { if (downloaded) { openSavedFile() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b70f35ef..19e8a3b13 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,6 +141,10 @@ if(ANDROID) "preferences-system-users" "preferences-desktop-theme-global" "notifications" + "zoom-in" + "zoom-out" + "image-rotate-left-symbolic" + "image-rotate-right-symbolic" ) else() target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)