Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85825846
D377.1793269678.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
16 KB
Referenced Files
None
Subscribers
None
D377.1793269678.diff
View Options
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -251,7 +251,9 @@
media/PlayButton.qml
media/MediaViewerPage.qml
media/ZoomableImage.qml
+ media/ZoomableVideo.qml
media/ZoomableControl.qml
+ media/VideoWithControl.qml
ActionSettingsPage.qml
CreateRoomPage.qml
diff --git a/src/contents/ui/actions/MediaViewerActions.qml b/src/contents/ui/actions/MediaViewerActions.qml
--- a/src/contents/ui/actions/MediaViewerActions.qml
+++ b/src/contents/ui/actions/MediaViewerActions.qml
@@ -29,6 +29,10 @@
icon.name: 'zoom-out-symbolic'
text: l10n.get('media-viewer-zoom-out')
property var defaultShortcut: StandardKey.ZoomOut
+ },
+ Kirigami.Action {
+ objectName: 'playOrPause'
+ text: l10n.get('media-viewer-play-or-pause')
}
]
}
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
@@ -13,6 +13,7 @@
import '.' as Types
import '..' as Kazv
+import '../matrix-helpers.js' as MatrixHelpers
Simple {
id: upper
@@ -95,12 +96,15 @@
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)
+ property var preferredSize: MatrixHelpers.getPreferredSize({
+ iWidth: Math.max(upper.thumbnailWidth || upper.imageWidth || image.implicitWidth || 1, Kirigami.Units.gridUnit),
+ iHeight: Math.max(upper.thumbnailHeight || upper.imageHeight || image.implicitHeight || 1, Kirigami.Units.gridUnit),
+ maxHeight: upper.imageMaxHeight,
+ minWidth: upper.imageMinWidth,
+ minHeight: upper.imageMinHeight,
+ })
+ width: preferredSize.width
+ height: preferredSize.height
source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
horizontalAlignment: Image.AlignLeft
fillMode: Image.PreserveAspectCrop
diff --git a/src/contents/ui/event-types/Video.qml b/src/contents/ui/event-types/Video.qml
--- a/src/contents/ui/event-types/Video.qml
+++ b/src/contents/ui/event-types/Video.qml
@@ -15,6 +15,7 @@
import '.' as Types
import '..' as Kazv
import '../media' as M
+import '../matrix-helpers.js' as MatrixHelpers
Simple {
id: upper
@@ -23,10 +24,12 @@
property var body: event.content.body
property var videoInfo: event.content.info || {}
- property var videoResolution: video.metaData.value(MediaMetaData.Resolution)
+ property var videoResolution: mediaPlayer.metaData.value(MediaMetaData.Resolution)
property var videoWidth: videoResolution && videoResolution.width || (videoInfo.w || 1)
property var videoHeight: videoResolution && videoResolution.height || (videoInfo.h || 1)
- property var videoMaxHeight: compactMode ? Kirigami.Units.gridUnit * 5 : Infinity
+ property var videoMaxHeight: compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.enormous
+ property var videoMinHeight: compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.huge
+ property var videoMinWidth: compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.huge
property var thumbnailInfo: videoInfo.thumbnail_info || {}
property var thumbnailMxcUri: videoInfo.thumbnail_url
property var thumbnailUri: matrixSession.mxcUriToHttp(thumbnailMxcUri)
@@ -69,94 +72,68 @@
text: l10n.get('event-message-video-sent', { gender, body })
visible: !compactMode
}
- property var video: Video {
- id: video
- objectName: 'video'
+ property var mediaPlayer: MediaPlayer {
+ id: mediaPlayer
source: fileHandler.kazvIOJob ? "" : fileHandler.cachedFile
autoPlay: !hasAudio
loops: MediaPlayer.Infinite
- Layout.fillWidth: true
- property double videoWidth: upper.thumbnailWidth || upper.videoWidth || video.implicitWidth || 1
- property double videoHeight: upper.thumbnailHeight || upper.videoHeight || video.implicitHeight || 1
- property double privHeight: Math.min(videoMaxHeight, videoHeight)
- property double privWidth: privHeight / videoHeight * videoWidth
- Layout.preferredWidth: privWidth
- Layout.preferredHeight: width < privWidth ? privHeight / privWidth * width : privHeight
- fillMode: VideoOutput.PreserveAspectFit
-
- M.PlayButton {
- id: playButton
- z: 10
- anchors.centerIn: parent
- height: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
- width: Math.min(video.height, video.width, Kirigami.Units.iconSizes.large)
- states: [
- State {
- name: 'shown'
- when: video.playbackState !== MediaPlayer.PlayingState
- PropertyChanges {
- target: playButton
- visible: true
- opacity: 1
- }
- },
- State {
- name: 'hidden'
- when: video.playbackState === MediaPlayer.PlayingState
- PropertyChanges {
- target: playButton
- opacity: 0
- visible: false
- }
- }
- ]
- transitions: [
- Transition {
- NumberAnimation {
- properties: "opacity,visible"
- duration: Kirigami.Units.shortDuration
- }
- }
- ]
- onClicked: video.playOrPause();
- }
-
- TapHandler {
- onSingleTapped: {
- video.playOrPause();
- }
+ videoOutput: video
+ }
+ property var videoBtn: AbstractButton {
+ id: videoBtn
+ Accessible.name: l10n.get('event-video-open-viewer-action')
+ Accessible.role: Accessible.Button
+ Accessible.onPressAction: pushMediaViewerPage(event)
+ hoverEnabled: true
+ focusPolicy: Qt.StrongFocus
+ topInset: 0
+ bottomInset: 0
+ leftInset: 0
+ rightInset: 0
+ padding: Kirigami.Units.smallSpacing
+ onClicked: pushMediaViewerPage(event)
+ background: Kazv.FocusRingBackground {
+ target: videoBtn
}
- Loader {
- anchors.fill: parent
- property Component thumbnail: Image {
- source: thumbnailUri
- fillMode: Image.PreserveAspectFit
- data: [bubble.mediaFileMenu]
- }
-
- property Component cover: Rectangle {
- color: Kirigami.Theme.negativeBackgroundColor //Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
- }
+ contentItem: Item {
+ implicitWidth: video.width
+ implicitHeight: video.height
+ M.VideoWithControl {
+ id: video
+ objectName: 'video'
+ player: mediaPlayer
+ property var preferredSize: MatrixHelpers.getPreferredSize({
+ iWidth: Math.max(upper.thumbnailWidth || upper.videoWidth || video.implicitWidth || 1, Kirigami.Units.gridUnit),
+ iHeight: Math.max(upper.thumbnailHeight || upper.videoHeight || video.implicitHeight || 1, Kirigami.Units.gridUnit),
+ maxHeight: upper.videoMaxHeight,
+ minWidth: upper.videoMinWidth,
+ minHeight: upper.videoMinHeight,
+ })
+ width: preferredSize.width
+ height: preferredSize.height
+ fillMode: VideoOutput.PreserveAspectCrop
+
+ Loader {
+ anchors.fill: parent
+ property Component thumbnail: Image {
+ source: thumbnailUri
+ fillMode: Image.PreserveAspectFit
+ data: [bubble.mediaFileMenu]
+ }
- sourceComponent: (video.playbackState !== MediaPlayer.StoppedState) ? undefined : (upper.hasThumbnail ? thumbnail : cover)
- }
+ property Component cover: Rectangle {
+ color: Kirigami.Theme.negativeBackgroundColor //Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
+ }
- function playOrPause() {
- console.log('playback state ==', video.playbackState);
- if (video.playbackState == MediaPlayer.PlayingState) {
- video.pause();
- } else {
- if (video.playbackState == MediaPlayer.StoppedState) {
- video.seek(0);
+ sourceComponent: (mediaPlayer.playbackState !== MediaPlayer.StoppedState) ? undefined : (upper.hasThumbnail ? thumbnail : cover)
}
- video.play();
}
}
}
data: [
label,
- video,
+ videoBtn,
bubble.progressBar
]
}
diff --git a/src/contents/ui/media/MediaViewerPage.qml b/src/contents/ui/media/MediaViewerPage.qml
--- a/src/contents/ui/media/MediaViewerPage.qml
+++ b/src/contents/ui/media/MediaViewerPage.qml
@@ -7,6 +7,7 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
+import QtMultimedia
import moe.kazv.mxc.kazv as MK
import org.kde.kirigami as Kirigami
@@ -17,6 +18,7 @@
required property var event
property var jobManager: kazvIOManager
property var session: matrixSession
+ property bool isVideo: event.content?.msgtype === 'm.video'
title: l10n.get('media-viewer-page-title')
padding: 0
@@ -33,6 +35,14 @@
objectName: 'zoomOut'
onTriggered: loader.item?.zoomOut()
},
+ Kazv.KazvAction {
+ objectName: 'playOrPause'
+ visible: mediaViewerPage.isVideo
+ property bool isPlaying: loader.item?.mediaPlayer && loader.item?.mediaPlayer.playbackState === MediaPlayer.PlayingState
+ icon.name: isPlaying ? 'media-playback-pause' : 'media-playback-start'
+ text: isPlaying ? l10n.get('media-viewer-pause') : l10n.get('media-viewer-play')
+ onTriggered: loader.item?.itemForScaling.playOrPause()
+ }
]
Kazv.FileHandler {
@@ -49,9 +59,18 @@
imageAlt: event.content?.body
}
+ property Component videoComp: ZoomableVideo {
+ videoUrl: fileHandler.kazvIOJob ? '' : fileHandler.cachedFile
+ videoAlt: event.content?.body
+ }
+
Loader {
id: loader
anchors.fill: parent
- sourceComponent: fileHandler.kazvIOJob ? undefined : mediaViewerPage.imageComp
+ sourceComponent: fileHandler.kazvIOJob ? undefined : (
+ mediaViewerPage.isVideo
+ ? mediaViewerPage.videoComp
+ : mediaViewerPage.imageComp
+ )
}
}
diff --git a/src/contents/ui/media/VideoWithControl.qml b/src/contents/ui/media/VideoWithControl.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/media/VideoWithControl.qml
@@ -0,0 +1,29 @@
+/*
+ * 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 QtMultimedia
+import moe.kazv.mxc.kazv as MK
+import org.kde.kirigami as Kirigami
+
+VideoOutput {
+ id: video
+ required property var player
+
+ function playOrPause() {
+ console.log('playback state ==', video.player.playbackState);
+ if (video.player.playbackState == MediaPlayer.PlayingState) {
+ video.player.pause();
+ } else {
+ if (video.player.playbackState == MediaPlayer.StoppedState) {
+ video.player.position = 0;
+ }
+ video.player.play();
+ }
+ }
+}
diff --git a/src/contents/ui/media/ZoomableVideo.qml b/src/contents/ui/media/ZoomableVideo.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/media/ZoomableVideo.qml
@@ -0,0 +1,102 @@
+/*
+ * 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 QtMultimedia
+import moe.kazv.mxc.kazv as MK
+
+import org.kde.kirigami as Kirigami
+import '..' as Kazv
+
+ColumnLayout {
+ id: cont
+ required property string videoUrl
+ required property string videoAlt
+ readonly property var itemForScaling: video
+ readonly property var mediaPlayer: mediaPlayer
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ PlayButton {
+ id: playButton
+ z: 10
+ anchors.centerIn: parent
+ height: Kirigami.Units.iconSizes.large
+ width: Kirigami.Units.iconSizes.large
+ states: [
+ State {
+ name: 'shown'
+ when: video.player.playbackState !== MediaPlayer.PlayingState
+ PropertyChanges {
+ target: playButton
+ visible: true
+ opacity: 1
+ }
+ },
+ State {
+ name: 'hidden'
+ when: video.player.playbackState === MediaPlayer.PlayingState
+ PropertyChanges {
+ target: playButton
+ opacity: 0
+ visible: false
+ }
+ }
+ ]
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "opacity,visible"
+ duration: Kirigami.Units.shortDuration
+ }
+ }
+ ]
+ onClicked: video.playOrPause();
+ }
+
+ ZoomableControl {
+ id: zoomableCtrl
+ anchors.fill: parent
+ property var mediaPlayer: MediaPlayer {
+ id: mediaPlayer
+ source: cont.videoUrl
+ autoPlay: true
+ loops: MediaPlayer.Infinite
+ videoOutput: video
+ audioOutput: AudioOutput {}
+ }
+
+ VideoWithControl {
+ id: video
+ player: mediaPlayer
+ width: implicitWidth
+ height: implicitHeight
+ anchors.centerIn: parent
+ Accessible.name: cont.videoAlt
+ Accessible.role: Accessible.Canvas
+ }
+
+ property var tapHandler: TapHandler {
+ onTapped: video.playOrPause()
+ }
+ }
+ }
+
+ function toOriginalSize() {
+ zoomableCtrl.toOriginalSize();
+ }
+
+ function zoomIn() {
+ zoomableCtrl.zoomIn();
+ }
+
+ function zoomOut() {
+ zoomableCtrl.zoomOut();
+ }
+}
diff --git a/src/js/matrix-helpers.js b/src/js/matrix-helpers.js
--- a/src/js/matrix-helpers.js
+++ b/src/js/matrix-helpers.js
@@ -125,3 +125,31 @@
}
const toolTipTimeout = 5000;
+
+function getPreferredSize({
+ // original (implicit) size
+ iWidth,
+ iHeight,
+ // maximum size
+ maxWidth,
+ maxHeight,
+ // minimum size
+ minWidth,
+ minHeight,
+})
+{
+ // first, bound the height by its maximum
+ const privHeight = Math.min(maxHeight, iHeight);
+ // the width is scaled to proportion
+ const privWidth = privHeight / iHeight * iWidth;
+ // then, bound the width by the minimum
+ const width = Math.max(privWidth, minWidth);
+ const height = Math.min(
+ Math.max(
+ // if width is clamped to the minimum, scale height accordingly
+ width > privWidth ? privHeight / privWidth * width : privHeight,
+ // then clamp the height
+ minHeight
+ ), maxHeight);
+ return { width, height };
+}
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
@@ -240,6 +240,7 @@
event-message-file-sent = 发送了文件「{ $body }」。
event-message-video-sent = 发送了视频「{ $body }」。
event-summary-video-sent = 发送了视频「{ $body }」。
+event-video-open-viewer-action = 放大
event-message-audio-sent = 发送了音频「{ $body }」。
event-message-audio-play-audio = 播放音频
event-sending = 发送中...
@@ -500,3 +501,6 @@
media-viewer-page-title = 媒体查看器
media-viewer-zoomable = 媒体大小控制
media-viewer-zoomable-desc = 使用放大和缩小操作以改变大小。使用方向键以滚动。
+media-viewer-play-or-pause = 播放或暂停
+media-viewer-play = 播放
+media-viewer-pause = 暂停
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
@@ -259,6 +259,7 @@
event-message-file-sent = sent a file "{ $body }".
event-message-video-sent = sent a video "{ $body }".
event-summary-video-sent = sent a video "{ $body }".
+event-video-open-viewer-action = Enlarge
event-message-audio-sent = sent an audio "{ $body }".
event-message-audio-play-audio = Play audio
event-sending = Sending...
@@ -522,3 +523,6 @@
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.
+media-viewer-play-or-pause = Play or pause
+media-viewer-play = Play
+media-viewer-pause = Pause
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Oct 29, 3:27 AM (22 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799333
Default Alt Text
D377.1793269678.diff (16 KB)
Attached To
Mode
D377: Allow playing video in media viewer
Attached
Detach File
Event Timeline