Page MenuHomePhorge

D367.1791633417.diff
No OneTemporary

Size
14 KB
Referenced Files
None
Subscribers
None

D367.1791633417.diff

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

Mime Type
text/plain
Expires
Sat, Oct 10, 4:56 AM (22 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785609
Default Alt Text
D367.1791633417.diff (14 KB)

Event Timeline