Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85805985
D367.1791745595.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
D367.1791745595.diff
View Options
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -243,6 +243,9 @@
ReactToEventPopup.qml
FileHandler.qml
media/PlayButton.qml
+ media/MediaViewerPage.qml
+ media/ZoomableImage.qml
+ media/ZoomableControl.qml
ActionSettingsPage.qml
CreateRoomPage.qml
@@ -285,6 +288,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 QtQuick
+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,57 @@
+/*
+ * 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
+ title: l10n.get('media-viewer-page-title')
+
+ 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/ZoomableControl.qml b/src/contents/ui/media/ZoomableControl.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/media/ZoomableControl.qml
@@ -0,0 +1,151 @@
+/*
+ * 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: zoomableControl
+
+ default property alias itemForScaling: imageItem.itemForScaling
+
+ Accessible.focusable: true
+ Accessible.name: l10n.get('media-viewer-zoomable')
+ Accessible.description: l10n.get('media-viewer-zoomable-desc')
+ Accessible.role: Accessible.Pane
+ topInset: 0
+ bottomInset: 0
+ leftInset: 0
+ rightInset: 0
+ padding: Kirigami.Units.smallSpacing
+ focusPolicy: Qt.StrongFocus
+ Keys.onRightPressed: scrollRight()
+ Keys.onDownPressed: scrollDown()
+ Keys.onLeftPressed: scrollLeft()
+ Keys.onUpPressed: scrollUp()
+ Accessible.onScrollRightAction: scrollRight()
+ Accessible.onScrollDownAction: scrollDown()
+ Accessible.onScrollLeftAction: scrollLeft()
+ Accessible.onScrollUpAction: scrollUp()
+
+ background: Kazv.FocusRingBackground {
+ target: zoomableControl
+ 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/imageItem.itemForScaling.implicitWidth, height/imageItem.itemForScaling.implicitHeight)
+ clip: true
+ Item {
+ id: imageItem
+ width: parent.width
+ height: parent.height
+ scale: controlContent.fitScale
+ property bool returning: false
+ required property Item itemForScaling
+ children: [itemForScaling]
+
+ 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;
+ }
+ }
+ }
+ }
+
+ 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();
+ }
+
+ function scrollRight() { smooth(() => { imageItem.x -= height / 5; }); }
+ function scrollDown() { smooth(() => { imageItem.y -= width / 5; }); }
+ function scrollLeft() { smooth(() => { imageItem.x += height / 5; }); }
+ function scrollUp() { smooth(() => { imageItem.y += width / 5; }); }
+}
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,31 @@
+/*
+ * 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
+
+ZoomableControl {
+ id: imageCont
+
+ required property string imageUrl
+ required property string imageAlt
+
+ Image {
+ id: image
+ width: implicitWidth
+ height: implicitHeight
+ anchors.centerIn: parent
+ source: imageCont.imageUrl
+ fillMode: Image.Pad
+ Accessible.name: imageCont.imageAlt
+ Accessible.role: Accessible.Graphic
+ }
+}
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,10 @@
verification-action-deny-sas-match = 并不一致
verification-show-numbers = 转而显示数字
verification-show-emojis = 转而显示表情文字
+
+media-viewer-to-original-size = 原始大小
+media-viewer-zoom-in = 放大
+media-viewer-zoom-out = 缩小
+media-viewer-page-title = 媒体查看器
+media-viewer-zoomable = 媒体大小控制
+media-viewer-zoomable-desc = 使用放大和缩小操作以改变大小。使用方向键以滚动。
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,10 @@
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
+media-viewer-page-title = Media Viewer
+media-viewer-zoomable = Media size control
+media-viewer-zoomable-desc = Use zoom in and out actions to change size. Use arrow keys to scroll.
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Oct 11, 12:06 PM (20 h, 32 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786171
Default Alt Text
D367.1791745595.diff (15 KB)
Attached To
Mode
D367: Allow opening images in a dedicated viewer
Attached
Detach File
Event Timeline
Log In to Comment