Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85805260
D367.1791633417.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
14 KB
Referenced Files
None
Subscribers
None
D367.1791633417.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/FocusRingBackground.qml b/src/contents/ui/FocusRingBackground.qml
--- a/src/contents/ui/FocusRingBackground.qml
+++ b/src/contents/ui/FocusRingBackground.qml
@@ -9,6 +9,7 @@
Rectangle {
required property var target
+ color: 'transparent'
radius: Kirigami.Units.cornerRadius
border.width: 1
border.color: (target.hovered || target.visualFocus) ? Kirigami.Theme.hoverColor : 'transparent'
diff --git a/src/contents/ui/KazvAction.qml b/src/contents/ui/KazvAction.qml
--- a/src/contents/ui/KazvAction.qml
+++ b/src/contents/ui/KazvAction.qml
@@ -21,7 +21,7 @@
if (!action.enabled) {
return;
}
- action.onTriggered();
+ action.trigger();
}
}
}
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,34 @@
+/*
+ * 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')
+ },
+ Kirigami.Action {
+ objectName: 'zoomIn'
+ icon.name: 'zoom-in-symbolic'
+ text: l10n.get('media-viewer-zoom-in')
+ property var defaultShortcut: StandardKey.ZoomIn
+ },
+ Kirigami.Action {
+ objectName: 'zoomOut'
+ icon.name: 'zoom-out-symbolic'
+ text: l10n.get('media-viewer-zoom-out')
+ property var defaultShortcut: StandardKey.ZoomOut
+ }
+ ]
+}
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
@@ -30,7 +30,9 @@
property var imageFile: event.content.file || {}
property var imageWidth: imageInfo.w
property var imageHeight: imageInfo.h
- property var imageMaxHeight: compactMode ? Kirigami.Units.gridUnit * 5 : Infinity
+ property var imageMaxHeight: compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.enormous
+ property var imageMinHeight: compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.huge
+ property var imageMinWidth: compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.huge
property var imageMxcUri: fileHandler.getFile().mxcUri
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
@@ -70,19 +72,40 @@
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 {
+ id: imgButton
+ Accessible.name: l10n.get('event-image-open-viewer-action')
+ Accessible.role: Accessible.Button
+ Accessible.onPressAction: pushMediaViewerPage(event)
+ hoverEnabled: true
+ focusPolicy: Qt.StrongFocus
+ background: Kazv.FocusRingBackground {
+ target: imgButton
+ }
+ topInset: 0
+ bottomInset: 0
+ leftInset: 0
+ rightInset: 0
+ padding: Kirigami.Units.smallSpacing
+ onClicked: pushMediaViewerPage(event)
+ contentItem: Item {
+ implicitWidth: image.width
+ implicitHeight: image.height
+ 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(upper.imageMaxHeight, imageHeight)
+ property double privWidth: privHeight / imageHeight * imageWidth
+ width: Math.max(privWidth, upper.imageMinWidth)
+ height: Math.min(Math.max(width < privWidth ? privHeight / privWidth * width : privHeight, upper.imageMinHeight), upper.imageMaxHeight)
+ source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
+ horizontalAlignment: Image.AlignLeft
+ fillMode: Image.PreserveAspectCrop
+ }
+ }
}
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,56 @@
+/*
+ * 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
+
+ padding: 0
+ actions: [
+ Kazv.KazvAction {
+ objectName: 'toOriginalSize'
+ onTriggered: loader.item?.toOriginalSize()
+ },
+ Kazv.KazvAction {
+ objectName: 'zoomIn'
+ onTriggered: loader.item?.zoomIn()
+ },
+ Kazv.KazvAction {
+ objectName: 'zoomOut'
+ onTriggered: loader.item?.zoomOut()
+ },
+ ]
+
+ 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,147 @@
+/*
+ * 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
+
+Control {
+ id: imageCont
+ anchors.fill: parent
+
+ required property string imageUrl
+ required property string imageAlt
+ Accessible.focusable: true
+ topInset: 0
+ bottomInset: 0
+ leftInset: 0
+ rightInset: 0
+ padding: Kirigami.Units.smallSpacing
+ focusPolicy: Qt.StrongFocus
+ Keys.onRightPressed: smooth(() => { imageItem.x -= height / 5; })
+ Keys.onDownPressed: smooth(() => { imageItem.y -= width / 5; })
+ Keys.onLeftPressed: smooth(() => { imageItem.x += height / 5; })
+ Keys.onUpPressed: smooth(() => { imageItem.y += width / 5; })
+
+ background: Kazv.FocusRingBackground {
+ target: imageCont
+ border.width: Kirigami.Units.smallSpacing / 2
+ }
+
+ // 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.
+ contentItem: Item {
+ id: controlContent
+ property var fitScale: Math.min(1, width/image.implicitWidth, height/image.implicitHeight)
+ clip: true
+ Item {
+ id: imageItem
+ width: parent.width
+ height: parent.height
+ scale: controlContent.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 <= controlContent.fitScale) {
+ returning = true;
+ scale = controlContent.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: controlContent.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 {
+ id: wheelHandler
+ target: imageItem
+ property: 'scale'
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ onActiveChanged: {
+ if (!active) { imageItem.returnBounds() }
+ }
+ }
+
+ function toOriginalSize() {
+ imageItem.scale = 1;
+ }
+
+ function zoomIn() {
+ imageItem.scale *= wheelHandler.targetScaleMultiplier;
+ }
+
+ function zoomOut() {
+ imageItem.scale /= wheelHandler.targetScaleMultiplier;
+ imageItem.returnBounds();
+ }
+
+ function smooth(f) {
+ imageItem.returning = true;
+ f();
+ imageItem.returning = false;
+ imageItem.returnBounds();
+ }
+}
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -24,6 +24,7 @@
action-group-room-list-context-menu-actions = 房间列表操作
action-group-dialog-actions = 对话框操作
action-group-settings-actions = 设置操作
+action-group-media-viewer-actions = 媒体查看器操作
dialog-accept-action = 确认
dialog-reject-action = 取消
@@ -234,6 +235,7 @@
event-message-image-sent = 发送了图片「{ $body }」。
event-message-sticker-sent = 发送了贴纸「{ $body }」。
event-summary-image-sent = 发送了图片「{ $body }」。
+event-image-open-viewer-action = 放大
event-summary-sticker-sent = 发送了贴纸「{ $body }」。
event-message-file-sent = 发送了文件「{ $body }」。
event-message-video-sent = 发送了视频「{ $body }」。
@@ -491,3 +493,7 @@
verification-action-deny-sas-match = 并不一致
verification-show-numbers = 转而显示数字
verification-show-emojis = 转而显示表情文字
+
+media-viewer-to-original-size = 原始大小
+media-viewer-zoom-in = 放大
+media-viewer-zoom-out = 缩小
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
@@ -253,6 +254,7 @@
event-message-image-sent = sent an image "{ $body }".
event-message-sticker-sent = sent a sticker "{ $body }".
event-summary-image-sent = sent an image "{ $body }".
+event-image-open-viewer-action = Enlarge
event-summary-sticker-sent = sent a sticker "{ $body }".
event-message-file-sent = sent a file "{ $body }".
event-message-video-sent = sent a video "{ $body }".
@@ -513,3 +515,7 @@
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
+media-viewer-zoom-in = Zoom in
+media-viewer-zoom-out = Zoom out
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Oct 10, 4:56 AM (12 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785609
Default Alt Text
D367.1791633417.diff (14 KB)
Attached To
Mode
D367: Allow opening images in a dedicated viewer
Attached
Detach File
Event Timeline
Log In to Comment