Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85804368
D367.1791546030.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
8 KB
Referenced Files
None
Subscribers
None
D367.1791546030.diff
View Options
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -243,6 +243,8 @@
ReactToEventPopup.qml
FileHandler.qml
media/PlayButton.qml
+ media/MediaViewerPage.qml
+ media/ZoomableImage.qml
ActionSettingsPage.qml
CreateRoomPage.qml
@@ -285,6 +287,7 @@
actions/RoomListActions.qml
actions/DialogActions.qml
actions/SettingsActions.qml
+ actions/MediaViewerActions.qml
actions/AllActions.qml
MessageNotification.qml
diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml
--- a/src/contents/ui/Main.qml
+++ b/src/contents/ui/Main.qml
@@ -327,6 +327,10 @@
pageStack.layers.push(Qt.resolvedUrl("JoinRoomPage.qml"), {});
}
+ function pushMediaViewerPage(event) {
+ pageStack.layers.push(Qt.resolvedUrl("media/MediaViewerPage.qml"), { event });
+ }
+
function sessionNameFor(userId, deviceId) {
return userId + '/' + deviceId;
}
diff --git a/src/contents/ui/actions/AllActions.qml b/src/contents/ui/actions/AllActions.qml
--- a/src/contents/ui/actions/AllActions.qml
+++ b/src/contents/ui/actions/AllActions.qml
@@ -16,6 +16,7 @@
KazvActions.BubbleActions {},
KazvActions.RoomListActions {},
KazvActions.DialogActions {},
- KazvActions.SettingsActions {}
+ KazvActions.SettingsActions {},
+ KazvActions.MediaViewerActions {},
]
}
diff --git a/src/contents/ui/actions/MediaViewerActions.qml b/src/contents/ui/actions/MediaViewerActions.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/actions/MediaViewerActions.qml
@@ -0,0 +1,22 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQml
+
+import org.kde.kirigami as Kirigami
+
+import '../shortcuts' as Shortcuts
+
+Shortcuts.ActionCollection {
+ actionGroupName: l10n.get('action-group-media-viewer-actions')
+ actions: [
+ Kirigami.Action {
+ objectName: 'toOriginalSize'
+ icon.name: 'zoom-original-symbolic'
+ text: l10n.get('media-viewer-to-original-size')
+ }
+ ]
+}
diff --git a/src/contents/ui/event-types/Image.qml b/src/contents/ui/event-types/Image.qml
--- a/src/contents/ui/event-types/Image.qml
+++ b/src/contents/ui/event-types/Image.qml
@@ -70,19 +70,22 @@
visible: !compactMode
}
- property var image: Image {
- id: image
- objectName: 'mainImage'
- Layout.fillWidth: true
- property double imageWidth: upper.thumbnailWidth || upper.imageWidth || image.implicitWidth || 1
- property double imageHeight: upper.thumbnailHeight || upper.imageHeight || image.implicitHeight || 1
- property double privHeight: Math.min(imageMaxHeight, imageHeight)
- property double privWidth: privHeight / imageHeight * imageWidth
- Layout.preferredWidth: privWidth
- Layout.preferredHeight: width < privWidth ? privHeight / privWidth * width : privHeight
- source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
- horizontalAlignment: Image.AlignLeft
- fillMode: Image.PreserveAspectFit
+ property var image: AbstractButton {
+ onClicked: pushMediaViewerPage(event)
+ contentItem: Image {
+ id: image
+ objectName: 'mainImage'
+ Layout.fillWidth: true
+ property double imageWidth: upper.thumbnailWidth || upper.imageWidth || image.implicitWidth || 1
+ property double imageHeight: upper.thumbnailHeight || upper.imageHeight || image.implicitHeight || 1
+ property double privHeight: Math.min(imageMaxHeight, imageHeight)
+ property double privWidth: privHeight / imageHeight * imageWidth
+ Layout.preferredWidth: privWidth
+ Layout.preferredHeight: width < privWidth ? privHeight / privWidth * width : privHeight
+ source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
+ horizontalAlignment: Image.AlignLeft
+ fillMode: Image.PreserveAspectFit
+ }
}
property var canAddSticker: !!event.content.url
diff --git a/src/contents/ui/media/MediaViewerPage.qml b/src/contents/ui/media/MediaViewerPage.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/media/MediaViewerPage.qml
@@ -0,0 +1,47 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import moe.kazv.mxc.kazv as MK
+
+import org.kde.kirigami as Kirigami
+import '..' as Kazv
+
+Kirigami.Page {
+ id: mediaViewerPage
+ required property var event
+ property var jobManager: kazvIOManager
+ property var session: matrixSession
+
+ actions: [
+ Kazv.KazvAction {
+ objectName: 'toOriginalSize'
+ onTriggered: loader.item?.toOriginalSize()
+ }
+ ]
+
+ Kazv.FileHandler {
+ id: fileHandler
+ // Remove thumbnail info from content, so that FileHandler only downloads the original.
+ eventContent: ({ file: event.content?.file, url: event.content?.url })
+ autoCache: true
+ kazvIOManager: mediaViewerPage.jobManager
+ matrixSession: mediaViewerPage.session
+ }
+
+ property Component imageComp: ZoomableImage {
+ imageUrl: fileHandler.kazvIOJob ? '' : fileHandler.cachedFile
+ imageAlt: event.content?.body
+ }
+
+ Loader {
+ id: loader
+ anchors.fill: parent
+ sourceComponent: fileHandler.kazvIOJob ? undefined : mediaViewerPage.imageComp
+ }
+}
diff --git a/src/contents/ui/media/ZoomableImage.qml b/src/contents/ui/media/ZoomableImage.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/media/ZoomableImage.qml
@@ -0,0 +1,110 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import moe.kazv.mxc.kazv as MK
+
+import org.kde.kirigami as Kirigami
+import '..' as Kazv
+
+Item {
+ id: imageCont
+ anchors.fill: parent
+
+ required property string imageUrl
+ required property string imageAlt
+ property var fitScale: Math.min(1, width/image.implicitWidth, height/image.implicitHeight)
+
+ // image holds the unscaled original image.
+ // imageItem is used for scaling.
+ // We do not scale image directly because we want the position 0,0 to be
+ // such that the image is centered in the page.
+ Item {
+ id: imageItem
+ width: parent.width
+ height: parent.height
+ scale: imageCont.fitScale
+ property bool returning: false
+
+ Behavior on scale {
+ id: scaleRule
+ enabled: imageItem.returning
+ NumberAnimation {
+ duration: Kirigami.Units.shortDuration
+ }
+ }
+ Behavior on x {
+ id: xRule
+ enabled: imageItem.returning
+ NumberAnimation {
+ duration: Kirigami.Units.shortDuration
+ }
+ }
+ Behavior on y {
+ id: yRule
+ enabled: imageItem.returning
+ NumberAnimation {
+ duration: Kirigami.Units.shortDuration
+ }
+ }
+ function returnBounds() {
+ if (scale <= imageCont.fitScale) {
+ returning = true;
+ scale = imageCont.fitScale;
+ x = 0;
+ y = 0;
+ returning = false;
+ }
+ }
+ Image {
+ id: image
+ width: implicitWidth
+ height: implicitHeight
+ anchors.centerIn: parent
+ source: imageCont.imageUrl
+ fillMode: Image.Pad
+ Accessible.name: imageCont.imageAlt
+ }
+ }
+
+ PinchHandler {
+ target: imageItem
+ rotationAxis.enabled: false
+ scaleAxis.minimum: imageCont.fitScale
+ onActiveChanged: {
+ if (!active) { imageItem.returnBounds() }
+ }
+ }
+
+ DragHandler {
+ id: dragHandler
+ // If set to imageItem, it will cause translation to be reset whenever
+ // the pointer clicks somewhere out of imageItem's bounds.
+ // But imageItem is smaller than the original image, so this behaviour
+ // is not reasonable.
+ target: null
+ xAxis.onActiveValueChanged: delta => imageItem.x += delta
+ yAxis.onActiveValueChanged: delta => imageItem.y += delta
+ onActiveChanged: {
+ if (!active) { imageItem.returnBounds() }
+ }
+ }
+
+ WheelHandler {
+ target: imageItem
+ property: 'scale'
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ onActiveChanged: {
+ if (!active) { imageItem.returnBounds() }
+ }
+ }
+
+ function toOriginalSize() {
+ imageItem.scale = 1;
+ }
+}
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -24,6 +24,7 @@
action-group-room-list-context-menu-actions = Room List Actions
action-group-dialog-actions = Dialog Actions
action-group-settings-actions = Settings Actions
+action-group-media-viewer-actions = Media Viewer Actions
dialog-accept-action = Confirm
dialog-reject-action = Cancel
@@ -513,3 +514,5 @@
verification-action-deny-sas-match = They do not match
verification-show-numbers = Show numbers instead
verification-show-emojis = Show emojis instead
+
+media-viewer-to-original-size = Original size
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Oct 9, 4:40 AM (14 h, 16 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784994
Default Alt Text
D367.1791546030.diff (8 KB)
Attached To
Mode
D367: Allow opening images in a dedicated viewer
Attached
Detach File
Event Timeline
Log In to Comment