Timeline required properties
Move to using required properties for timeline delegates.
This commit is contained in:
committed by
Tobias Fella
parent
a94f46f904
commit
8ad23e7a40
@@ -14,16 +14,34 @@ import org.kde.neochat 1.0
|
|||||||
Components.AlbumMaximizeComponent {
|
Components.AlbumMaximizeComponent {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var modelData
|
required property string eventId
|
||||||
|
|
||||||
|
required property var time
|
||||||
|
|
||||||
|
required property var author
|
||||||
|
|
||||||
|
required property int delegateType
|
||||||
|
|
||||||
|
required property string plainText
|
||||||
|
|
||||||
|
required property string caption
|
||||||
|
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
required property var progressInfo
|
||||||
|
|
||||||
|
required property var mimeType
|
||||||
|
|
||||||
|
required property var source
|
||||||
|
|
||||||
property list<Components.AlbumModelItem> items: [
|
property list<Components.AlbumModelItem> items: [
|
||||||
Components.AlbumModelItem {
|
Components.AlbumModelItem {
|
||||||
type: root.modelData.delegateType === MessageEventModel.Image || root.modelData.delegateType === MessageEventModel.Sticker ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video
|
type: root.delegateType === MessageEventModel.Image || root.delegateType === MessageEventModel.Sticker ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video
|
||||||
source: root.modelData.delegateType === MessageEventModel.Video ? modelData.progressInfo.localPath : modelData.mediaInfo.source
|
source: root.delegateType === MessageEventModel.Video ? root.progressInfo.localPath : root.mediaInfo.source
|
||||||
tempSource: modelData.mediaInfo.tempInfo.source
|
tempSource: root.mediaInfo.tempInfo.source
|
||||||
caption: modelData.display
|
caption: root.caption
|
||||||
sourceWidth: modelData.mediaInfo.width
|
sourceWidth: root.mediaInfo.width
|
||||||
sourceHeight: modelData.mediaInfo.height
|
sourceHeight: root.mediaInfo.height
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -36,22 +54,22 @@ Components.AlbumMaximizeComponent {
|
|||||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||||
|
|
||||||
name: modelData.author.name ?? modelData.author.displayName
|
name: root.author.name ?? root.author.displayName
|
||||||
source: modelData.author.avatarSource
|
source: root.author.avatarSource
|
||||||
color: modelData.author.color
|
color: root.author.color
|
||||||
}
|
}
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: userLabel
|
id: userLabel
|
||||||
text: modelData.author.name ?? modelData.author.displayName
|
text: root.author.name ?? root.author.displayName
|
||||||
color: modelData.author.color
|
color: root.author.color
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: dateTimeLabel
|
id: dateTimeLabel
|
||||||
text: modelData.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
color: Kirigami.Theme.disabledTextColor
|
color: Kirigami.Theme.disabledTextColor
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
@@ -59,14 +77,14 @@ Components.AlbumMaximizeComponent {
|
|||||||
}
|
}
|
||||||
onItemRightClicked: {
|
onItemRightClicked: {
|
||||||
const contextMenu = fileDelegateContextMenu.createObject(parent, {
|
const contextMenu = fileDelegateContextMenu.createObject(parent, {
|
||||||
author: modelData.author,
|
author: root.author,
|
||||||
message: modelData.plainText,
|
message: root.plainText,
|
||||||
eventId: modelData.eventId,
|
eventId: root.eventId,
|
||||||
source: modelData.source,
|
source: root.source,
|
||||||
file: parent,
|
file: parent,
|
||||||
mimeType: modelData.mimeType,
|
mimeType: root.mimeType,
|
||||||
progressInfo: modelData.progressInfo,
|
progressInfo: root.progressInfo,
|
||||||
plainMessage: modelData.plainText,
|
plainMessage: root.plainText,
|
||||||
});
|
});
|
||||||
contextMenu.closeFullscreen.connect(root.close)
|
contextMenu.closeFullscreen.connect(root.close)
|
||||||
contextMenu.open();
|
contextMenu.open();
|
||||||
@@ -74,7 +92,7 @@ Components.AlbumMaximizeComponent {
|
|||||||
onSaveItem: {
|
onSaveItem: {
|
||||||
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
|
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||||
dialog.open()
|
dialog.open()
|
||||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(modelData.eventId)
|
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
@@ -88,7 +106,7 @@ Components.AlbumMaximizeComponent {
|
|||||||
if (!currentFile) {
|
if (!currentFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentRoom.downloadFile(eventId, currentFile)
|
currentRoom.downloadFile(rooteventId, currentFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,38 +10,58 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for an audio message.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: audioDelegate
|
id: root
|
||||||
|
|
||||||
onOpenContextMenu: openFileContext(model, audioDelegate)
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be audio/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be audio-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - duration - The length in seconds of the audio media.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
|
/**
|
||||||
|
* @brief Whether the media has been downloaded.
|
||||||
|
*/
|
||||||
|
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||||
onDownloadedChanged: audio.play()
|
onDownloadedChanged: audio.play()
|
||||||
|
|
||||||
|
onOpenContextMenu: openFileContext(root)
|
||||||
|
|
||||||
innerObject: ColumnLayout {
|
innerObject: ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumWidth: audioDelegate.contentMaxWidth
|
Layout.maximumWidth: root.contentMaxWidth
|
||||||
|
|
||||||
Audio {
|
Audio {
|
||||||
id: audio
|
id: audio
|
||||||
source: model.progressInfo.localPath
|
source: root.progressInfo.localPath
|
||||||
autoLoad: false
|
autoLoad: false
|
||||||
}
|
}
|
||||||
|
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: "notDownloaded"
|
name: "notDownloaded"
|
||||||
when: !model.progressInfo.completed && !model.progressInfo.active
|
when: !root.progressInfo.completed && !root.progressInfo.active
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playButton
|
target: playButton
|
||||||
icon.name: "media-playback-start"
|
icon.name: "media-playback-start"
|
||||||
onClicked: currentRoom.downloadFile(model.eventId)
|
onClicked: currentRoom.downloadFile(root.eventId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "downloading"
|
name: "downloading"
|
||||||
when: model.progressInfo.active && !model.progressInfo.completed
|
when: root.progressInfo.active && !root.progressInfo.completed
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: downloadBar
|
target: downloadBar
|
||||||
visible: true
|
visible: true
|
||||||
@@ -50,13 +70,13 @@ TimelineContainer {
|
|||||||
target: playButton
|
target: playButton
|
||||||
icon.name: "media-playback-stop"
|
icon.name: "media-playback-stop"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
currentRoom.cancelFileTransfer(model.eventId)
|
currentRoom.cancelFileTransfer(root.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "paused"
|
name: "paused"
|
||||||
when: model.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
|
when: root.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playButton
|
target: playButton
|
||||||
icon.name: "media-playback-start"
|
icon.name: "media-playback-start"
|
||||||
@@ -67,7 +87,7 @@ TimelineContainer {
|
|||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "playing"
|
name: "playing"
|
||||||
when: model.progressInfo.completed && audio.playbackState === Audio.PlayingState
|
when: root.progressInfo.completed && audio.playbackState === Audio.PlayingState
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playButton
|
target: playButton
|
||||||
@@ -84,7 +104,7 @@ TimelineContainer {
|
|||||||
id: playButton
|
id: playButton
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
text: model.display
|
text: root.display
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
@@ -94,8 +114,8 @@ TimelineContainer {
|
|||||||
visible: false
|
visible: false
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
from: 0
|
from: 0
|
||||||
to: model.mediaInfo.size
|
to: root.mediaInfo.size
|
||||||
value: model.progressInfo.progress
|
value: root.progressInfo.progress
|
||||||
}
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
visible: audio.hasAudio
|
visible: audio.hasAudio
|
||||||
@@ -109,7 +129,7 @@ TimelineContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
visible: audioDelegate.contentMaxWidth > Kirigami.Units.gridUnit * 12
|
visible: root.contentMaxWidth > Kirigami.Units.gridUnit * 12
|
||||||
|
|
||||||
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
|
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
|
||||||
}
|
}
|
||||||
@@ -117,7 +137,7 @@ TimelineContainer {
|
|||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||||
visible: audio.hasAudio && audioDelegate.contentMaxWidth < Kirigami.Units.gridUnit * 12
|
visible: audio.hasAudio && root.contentMaxWidth < Kirigami.Units.gridUnit * 12
|
||||||
|
|
||||||
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
|
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import QtQuick.Layouts 1.15
|
|||||||
import org.kde.kirigami 2.15 as Kirigami
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for an encrypted message that can't be decrypted.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: encryptedDelegate
|
id: encryptedDelegate
|
||||||
|
|
||||||
|
|||||||
@@ -10,37 +10,60 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for an file message.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: fileDelegate
|
id: root
|
||||||
|
|
||||||
onOpenContextMenu: openFileContext(model, fileDelegate)
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media.
|
||||||
|
* - mimeIcon - The MIME icon name.
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
readonly property bool downloaded: progressInfo && progressInfo.completed
|
/**
|
||||||
|
* @brief Whether the media has been downloaded.
|
||||||
|
*/
|
||||||
|
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the file should be automatically opened when downloaded.
|
||||||
|
*/
|
||||||
property bool autoOpenFile: false
|
property bool autoOpenFile: false
|
||||||
|
|
||||||
onDownloadedChanged: if (autoOpenFile) {
|
onDownloadedChanged: if (autoOpenFile) {
|
||||||
openSavedFile();
|
openSavedFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenContextMenu: openFileContext(root)
|
||||||
|
|
||||||
function saveFileAs() {
|
function saveFileAs() {
|
||||||
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||||
dialog.open()
|
dialog.open()
|
||||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
|
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSavedFile() {
|
function openSavedFile() {
|
||||||
UrlHelper.openUrl(progressInfo.localPath);
|
UrlHelper.openUrl(root.progressInfo.localPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
innerObject: RowLayout {
|
innerObject: RowLayout {
|
||||||
Layout.maximumWidth: Math.min(fileDelegate.contentMaxWidth, implicitWidth)
|
Layout.maximumWidth: Math.min(root.contentMaxWidth, implicitWidth)
|
||||||
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: "downloadedInstant"
|
name: "downloadedInstant"
|
||||||
when: progressInfo.completed && autoOpenFile
|
when: root.progressInfo.completed && autoOpenFile
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: openButton
|
target: openButton
|
||||||
@@ -57,7 +80,7 @@ TimelineContainer {
|
|||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "downloaded"
|
name: "downloaded"
|
||||||
when: progressInfo.completed && !autoOpenFile
|
when: root.progressInfo.completed && !autoOpenFile
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: openButton
|
target: openButton
|
||||||
@@ -73,7 +96,7 @@ TimelineContainer {
|
|||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "downloading"
|
name: "downloading"
|
||||||
when: progressInfo.active
|
when: root.progressInfo.active
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: openButton
|
target: openButton
|
||||||
@@ -82,13 +105,13 @@ TimelineContainer {
|
|||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: sizeLabel
|
target: sizeLabel
|
||||||
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
|
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(root.progressInfo.progress), Controller.formatByteSize(root.progressInfo.total))
|
||||||
}
|
}
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: downloadButton
|
target: downloadButton
|
||||||
icon.name: "media-playback-stop"
|
icon.name: "media-playback-stop"
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
||||||
onClicked: currentRoom.cancelFileTransfer(eventId)
|
onClicked: currentRoom.cancelFileTransfer(root.eventId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
@@ -97,13 +120,13 @@ TimelineContainer {
|
|||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: downloadButton
|
target: downloadButton
|
||||||
onClicked: fileDelegate.saveFileAs()
|
onClicked: root.saveFileAs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
Kirigami.Icon {
|
Kirigami.Icon {
|
||||||
source: model.mediaInfo.mimeIcon
|
source: root.mediaInfo.mimeIcon
|
||||||
fallback: "unknown"
|
fallback: "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,14 +134,14 @@ TimelineContainer {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: model.display
|
text: root.display
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: sizeLabel
|
id: sizeLabel
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: Controller.formatByteSize(model.mediaInfo.size)
|
text: Controller.formatByteSize(root.mediaInfo.size)
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
@@ -130,7 +153,7 @@ TimelineContainer {
|
|||||||
icon.name: "document-open"
|
icon.name: "document-open"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
autoOpenFile = true;
|
autoOpenFile = true;
|
||||||
currentRoom.downloadTempFile(eventId);
|
currentRoom.downloadTempFile(root.eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||||
@@ -157,9 +180,9 @@ TimelineContainer {
|
|||||||
Config.lastSaveDirectory = folder
|
Config.lastSaveDirectory = folder
|
||||||
Config.save()
|
Config.save()
|
||||||
if (autoOpenFile) {
|
if (autoOpenFile) {
|
||||||
UrlHelper.copyTo(progressInfo.localPath, file)
|
UrlHelper.copyTo(root.progressInfo.localPath, file)
|
||||||
} else {
|
} else {
|
||||||
currentRoom.download(eventId, file);
|
currentRoom.download(root.eventId, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,37 +11,70 @@ import org.kde.kirigamiaddons.labs.components 1.0 as Components
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for an image message.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: imageDelegate
|
id: root
|
||||||
|
|
||||||
onOpenContextMenu: openFileContext(model, imageDelegate)
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the media has been downloaded.
|
||||||
|
*/
|
||||||
|
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the image should be automatically opened when downloaded.
|
||||||
|
*/
|
||||||
property bool openOnFinished: false
|
property bool openOnFinished: false
|
||||||
readonly property bool downloaded: progressInfo && progressInfo.completed
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width of the image.
|
||||||
|
*/
|
||||||
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum height of the image.
|
||||||
|
*/
|
||||||
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
||||||
|
|
||||||
|
onOpenContextMenu: openFileContext(root)
|
||||||
|
|
||||||
innerObject: AnimatedImage {
|
innerObject: AnimatedImage {
|
||||||
id: img
|
id: img
|
||||||
|
|
||||||
property var imageWidth: {
|
property var imageWidth: {
|
||||||
if (model.mediaInfo.width > 0) {
|
if (root.mediaInfo.width > 0) {
|
||||||
return model.mediaInfo.width;
|
return root.mediaInfo.width;
|
||||||
} else if (sourceSize.width && sourceSize.width > 0) {
|
} else if (sourceSize.width && sourceSize.width > 0) {
|
||||||
return sourceSize.width;
|
return sourceSize.width;
|
||||||
} else {
|
} else {
|
||||||
return imageDelegate.contentMaxWidth;
|
return root.contentMaxWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property var imageHeight: {
|
property var imageHeight: {
|
||||||
if (model.mediaInfo.height > 0) {
|
if (root.mediaInfo.height > 0) {
|
||||||
return model.mediaInfo.height;
|
return root.mediaInfo.height;
|
||||||
} else if (sourceSize.height && sourceSize.height > 0) {
|
} else if (sourceSize.height && sourceSize.height > 0) {
|
||||||
return sourceSize.height;
|
return sourceSize.height;
|
||||||
} else {
|
} else {
|
||||||
// Default to a 16:9 placeholder
|
// Default to a 16:9 placeholder
|
||||||
return imageDelegate.contentMaxWidth / 16 * 9;
|
return root.contentMaxWidth / 16 * 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +89,11 @@ TimelineContainer {
|
|||||||
|
|
||||||
readonly property size maxSize: {
|
readonly property size maxSize: {
|
||||||
if (limitWidth) {
|
if (limitWidth) {
|
||||||
let width = Math.min(imageDelegate.contentMaxWidth, imageDelegate.maxWidth);
|
let width = Math.min(root.contentMaxWidth, root.maxWidth);
|
||||||
let height = width / aspectRatio;
|
let height = width / aspectRatio;
|
||||||
return Qt.size(width, height);
|
return Qt.size(width, height);
|
||||||
} else {
|
} else {
|
||||||
let height = Math.min(imageDelegate.maxHeight, imageDelegate.contentMaxWidth / aspectRatio);
|
let height = Math.min(root.maxHeight, root.contentMaxWidth / aspectRatio);
|
||||||
let width = height * aspectRatio;
|
let width = height * aspectRatio;
|
||||||
return Qt.size(width, height);
|
return Qt.size(width, height);
|
||||||
}
|
}
|
||||||
@@ -70,17 +103,17 @@ TimelineContainer {
|
|||||||
Layout.maximumHeight: maxSize.height
|
Layout.maximumHeight: maxSize.height
|
||||||
Layout.preferredWidth: imageWidth
|
Layout.preferredWidth: imageWidth
|
||||||
Layout.preferredHeight: imageHeight
|
Layout.preferredHeight: imageHeight
|
||||||
source: model.mediaInfo.source
|
source: root.mediaInfo.source
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: model.mediaInfo.tempInfo.source
|
source: root.mediaInfo.tempInfo.source
|
||||||
visible: parent.status !== Image.Ready
|
visible: parent.status !== Image.Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|
||||||
QQC2.ToolTip.text: model.display
|
QQC2.ToolTip.text: root.display
|
||||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
@@ -93,7 +126,7 @@ TimelineContainer {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
visible: progressInfo.active && !downloaded
|
visible: root.progressInfo.active && !downloaded
|
||||||
|
|
||||||
color: "#BB000000"
|
color: "#BB000000"
|
||||||
|
|
||||||
@@ -103,8 +136,8 @@ TimelineContainer {
|
|||||||
width: parent.width * 0.8
|
width: parent.width * 0.8
|
||||||
|
|
||||||
from: 0
|
from: 0
|
||||||
to: progressInfo.total
|
to: root.progressInfo.total
|
||||||
value: progressInfo.progress
|
value: root.progressInfo.progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,12 +146,21 @@ TimelineContainer {
|
|||||||
onTapped: {
|
onTapped: {
|
||||||
img.QQC2.ToolTip.hide()
|
img.QQC2.ToolTip.hide()
|
||||||
img.paused = true
|
img.paused = true
|
||||||
imageDelegate.ListView.view.interactive = false
|
root.ListView.view.interactive = false
|
||||||
var popup = maximizeImageComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
var popup = maximizeImageComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
modelData: model,
|
eventId: root.eventId,
|
||||||
|
time: root.time,
|
||||||
|
author: root.author,
|
||||||
|
delegateType: root.delegateType,
|
||||||
|
plainText: root.plainText,
|
||||||
|
caption: root.display,
|
||||||
|
mediaInfo: root.mediaInfo,
|
||||||
|
progressInfo: root.progressInfo,
|
||||||
|
mimeType: root.mimeType,
|
||||||
|
source: root.source
|
||||||
})
|
})
|
||||||
popup.closed.connect(() => {
|
popup.closed.connect(() => {
|
||||||
imageDelegate.ListView.view.interactive = true
|
root.ListView.view.interactive = true
|
||||||
img.paused = false
|
img.paused = false
|
||||||
popup.destroy()
|
popup.destroy()
|
||||||
})
|
})
|
||||||
@@ -136,13 +178,13 @@ TimelineContainer {
|
|||||||
openSavedFile()
|
openSavedFile()
|
||||||
} else {
|
} else {
|
||||||
openOnFinished = true
|
openOnFinished = true
|
||||||
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
|
currentRoom.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSavedFile() {
|
function openSavedFile() {
|
||||||
if (UrlHelper.openUrl(progressInfo.localPath)) return;
|
if (UrlHelper.openUrl(root.progressInfo.localPath)) return;
|
||||||
if (UrlHelper.openUrl(progressInfo.localDir)) return;
|
if (UrlHelper.openUrl(root.progressInfo.localDir)) return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,41 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for a location message.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: locationDelegate
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The latitude of the location marker in the message.
|
||||||
|
*/
|
||||||
|
required property real latitude
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The longitude of the location marker in the message.
|
||||||
|
*/
|
||||||
|
required property real longitude
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief What type of marker the location message is.
|
||||||
|
*
|
||||||
|
* The main options are m.pin for a general location or m.self for a pin to show
|
||||||
|
* a user's location.
|
||||||
|
*/
|
||||||
|
required property string asset
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.maximumWidth: locationDelegate.contentMaxWidth
|
Layout.maximumWidth: root.contentMaxWidth
|
||||||
Layout.preferredWidth: locationDelegate.contentMaxWidth
|
Layout.preferredWidth: root.contentMaxWidth
|
||||||
Map {
|
Map {
|
||||||
id: map
|
id: map
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: locationDelegate.contentMaxWidth / 16 * 9
|
Layout.preferredHeight: root.contentMaxWidth / 16 * 9
|
||||||
|
|
||||||
center: QtPositioning.coordinate(model.latitude, model.longitude)
|
center: QtPositioning.coordinate(root.latitude, root.longitude)
|
||||||
zoomLevel: 15
|
zoomLevel: 15
|
||||||
plugin: Plugin {
|
plugin: Plugin {
|
||||||
name: "osm"
|
name: "osm"
|
||||||
@@ -44,7 +67,7 @@ TimelineContainer {
|
|||||||
|
|
||||||
anchorPoint.x: sourceItem.width / 2
|
anchorPoint.x: sourceItem.width / 2
|
||||||
anchorPoint.y: sourceItem.height
|
anchorPoint.y: sourceItem.height
|
||||||
coordinate: QtPositioning.coordinate(model.latitude, model.longitude)
|
coordinate: QtPositioning.coordinate(rot.latitude, root.longitude)
|
||||||
autoFadeIn: false
|
autoFadeIn: false
|
||||||
|
|
||||||
sourceItem: Kirigami.Icon {
|
sourceItem: Kirigami.Icon {
|
||||||
@@ -67,23 +90,23 @@ TimelineContainer {
|
|||||||
Kirigami.Avatar {
|
Kirigami.Avatar {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
anchors.verticalCenterOffset: -parent.height / 8
|
anchors.verticalCenterOffset: -parent.height / 8
|
||||||
visible: model.asset === "m.self"
|
visible: root.asset === "m.self"
|
||||||
width: height
|
width: height
|
||||||
height: parent.height / 3 + 1
|
height: parent.height / 3 + 1
|
||||||
name: model.author.name ?? model.author.displayName
|
name: root.author.name ?? root.author.displayName
|
||||||
source: model.author.avatarSource
|
source: root.author.avatarSource
|
||||||
color: model.author.color
|
color: root.author.color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onLongPressed: openMessageContext(model, "", model.plainText)
|
onLongPressed: openMessageContext("")
|
||||||
}
|
}
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onTapped: openMessageContext(model, "", model.plainText)
|
onTapped: openMessageContext("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,34 +10,63 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for an text message.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: messageDelegate
|
id: root
|
||||||
|
|
||||||
onOpenContextMenu: openMessageContext(model, label.selectedText, model.plainText)
|
/**
|
||||||
|
* @brief The link preview properties.
|
||||||
|
*
|
||||||
|
* This is a list or object containing the following:
|
||||||
|
* - url - The URL being previewed.
|
||||||
|
* - loaded - Whether the URL preview has been loaded.
|
||||||
|
* - title - the title of the URL preview.
|
||||||
|
* - description - the description of the URL preview.
|
||||||
|
* - imageSource - a source URL for the preview image.
|
||||||
|
*
|
||||||
|
* @note An empty link previewer should be passed if there are no links to
|
||||||
|
* preview.
|
||||||
|
*/
|
||||||
|
required property var linkPreview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether there are any links to preview.
|
||||||
|
*/
|
||||||
|
required property bool showLinkPreview
|
||||||
|
|
||||||
|
onOpenContextMenu: openMessageContext(label.selectedText)
|
||||||
|
|
||||||
innerObject: ColumnLayout {
|
innerObject: ColumnLayout {
|
||||||
Layout.maximumWidth: messageDelegate.contentMaxWidth
|
Layout.maximumWidth: root.contentMaxWidth
|
||||||
RichLabel {
|
RichLabel {
|
||||||
id: label
|
id: label
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: currentRoom.chatBoxEditId !== model.eventId
|
visible: currentRoom.chatBoxEditId !== root.eventId
|
||||||
|
|
||||||
|
isReply: root.isReply
|
||||||
|
|
||||||
|
textMessage: root.display
|
||||||
}
|
}
|
||||||
Loader {
|
Loader {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: item ? item.minimumHeight : -1
|
Layout.minimumHeight: item ? item.minimumHeight : -1
|
||||||
Layout.preferredWidth: item ? item.preferredWidth : -1
|
Layout.preferredWidth: item ? item.preferredWidth : -1
|
||||||
visible: currentRoom.chatBoxEditId === model.eventId
|
visible: currentRoom.chatBoxEditId === root.eventId
|
||||||
active: visible
|
active: visible
|
||||||
sourceComponent: MessageEditComponent {
|
sourceComponent: MessageEditComponent {
|
||||||
room: currentRoom
|
room: currentRoom
|
||||||
messageId: model.eventId
|
messageId: root.eventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LinkPreviewDelegate {
|
LinkPreviewDelegate {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
active: !currentRoom.usesEncryption && currentRoom.urlPreviewEnabled && Config.showLinkPreview && model.showLinkPreview
|
active: !currentRoom.usesEncryption && currentRoom.urlPreviewEnabled && Config.showLinkPreview && root.showLinkPreview
|
||||||
linkPreviewer: model.linkPreview
|
linkPreviewer: root.linkPreview
|
||||||
indicatorEnabled: messageDelegate.isVisibleInTimeline()
|
indicatorEnabled: root.isVisibleInTimeline()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,25 +10,40 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for a poll message.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: pollDelegate
|
id: root
|
||||||
|
|
||||||
readonly property var data: model
|
/**
|
||||||
property var pollHandler: currentRoom.poll(model.eventId)
|
* @brief The matrix message content.
|
||||||
|
*/
|
||||||
|
required property var content
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The poll handler for this poll.
|
||||||
|
*
|
||||||
|
* This contains the required information like what the question, answers and
|
||||||
|
* current number of votes for each is.
|
||||||
|
*/
|
||||||
|
property var pollHandler: currentRoom.poll(root.eventId)
|
||||||
|
|
||||||
innerObject: ColumnLayout {
|
innerObject: ColumnLayout {
|
||||||
Label {
|
Label {
|
||||||
id: questionLabel
|
id: questionLabel
|
||||||
text: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["question"]["body"]
|
text: root.content["org.matrix.msc3381.poll.start"]["question"]["body"]
|
||||||
}
|
}
|
||||||
Repeater {
|
Repeater {
|
||||||
model: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["answers"]
|
model: root.content["org.matrix.msc3381.poll.start"]["answers"]
|
||||||
delegate: RowLayout {
|
delegate: RowLayout {
|
||||||
width: pollDelegate.innerObject.width
|
width: root.innerObject.width
|
||||||
CheckBox {
|
CheckBox {
|
||||||
checked: pollDelegate.pollHandler.answers[currentRoom.localUser.id] ? pollDelegate.pollHandler.answers[currentRoom.localUser.id].includes(modelData["id"]) : false
|
checked: root.pollHandler.answers[currentRoom.localUser.id] ? root.pollHandler.answers[currentRoom.localUser.id].includes(modelData["id"]) : false
|
||||||
onClicked: pollDelegate.pollHandler.sendPollAnswer(pollDelegate.data.eventId, modelData["id"])
|
onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"])
|
||||||
enabled: !pollDelegate.pollHandler.hasEnded
|
enabled: !root.pollHandler.hasEnded
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: modelData["org.matrix.msc1767.text"]
|
text: modelData["org.matrix.msc1767.text"]
|
||||||
@@ -37,15 +52,15 @@ TimelineContainer {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
visible: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["kind"] == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
|
visible: root.content["org.matrix.msc3381.poll.start"]["kind"] == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
|
||||||
Layout.preferredWidth: contentWidth
|
Layout.preferredWidth: contentWidth
|
||||||
text: pollDelegate.pollHandler.counts[modelData["id"]] ?? "0"
|
text: root.pollHandler.counts[modelData["id"]] ?? "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
visible: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["kind"] == "org.matrix.msc3381.poll.disclosed" || pollDelegate.pollHandler.hasEnded
|
visible: root.content["org.matrix.msc3381.poll.start"]["kind"] == "org.matrix.msc3381.poll.disclosed" || root.pollHandler.hasEnded
|
||||||
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", pollDelegate.pollHandler.answerCount) + (pollDelegate.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
|
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.answerCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
|
||||||
font.pointSize: questionLabel.font.pointSize * 0.8
|
font.pointSize: questionLabel.font.pointSize * 0.8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,67 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a message that has been replied to.
|
||||||
|
*
|
||||||
|
* Similar to the main timeline delegate a reply delegate is chosen based on the type
|
||||||
|
* of message being replied to. The main difference is that not all messages can be
|
||||||
|
* show in their original form and are instead visualised with a MIME type delegate
|
||||||
|
* e.g. Videos.
|
||||||
|
*/
|
||||||
Item {
|
Item {
|
||||||
id: replyComponent
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The reply author.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - id - The matrix ID of the reply author.
|
||||||
|
* - isLocalUser - Whether the reply author is the local user.
|
||||||
|
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
|
||||||
|
* - avatarMediaId - The media ID of the reply author's avatar.
|
||||||
|
* - avatarUrl - The mxc URL for the reply author's avatar.
|
||||||
|
* - displayName - The display name of the reply author.
|
||||||
|
* - display - The name of the reply author.
|
||||||
|
* - color - The color for the reply author.
|
||||||
|
* - object - The NeoChatUser object for the reply author.
|
||||||
|
*
|
||||||
|
* @sa NeoChatUser
|
||||||
|
*/
|
||||||
|
required property var author
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The delegate type of the reply message.
|
||||||
|
*/
|
||||||
|
required property int type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The media info for the reply event.
|
||||||
|
*
|
||||||
|
* This could be an image, audio, video or file.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media.
|
||||||
|
* - mimeIcon - The MIME icon name.
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - duration - The length in seconds of the audio media (audio/video only).
|
||||||
|
* - width - The width in pixels of the audio media (image/video only).
|
||||||
|
* - height - The height in pixels of the audio media (image/video only).
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The reply has been clicked.
|
||||||
|
*/
|
||||||
signal replyClicked()
|
signal replyClicked()
|
||||||
|
|
||||||
property var name
|
|
||||||
property alias avatar: replyAvatar.source
|
|
||||||
property var color
|
|
||||||
property var mediaInfo
|
|
||||||
|
|
||||||
implicitWidth: mainLayout.implicitWidth
|
implicitWidth: mainLayout.implicitWidth
|
||||||
implicitHeight: mainLayout.implicitHeight
|
implicitHeight: mainLayout.implicitHeight
|
||||||
|
|
||||||
@@ -40,7 +91,7 @@ Item {
|
|||||||
Layout.rowSpan: 2
|
Layout.rowSpan: 2
|
||||||
|
|
||||||
implicitWidth: Kirigami.Units.smallSpacing
|
implicitWidth: Kirigami.Units.smallSpacing
|
||||||
color: replyComponent.color
|
color: root.author.color
|
||||||
}
|
}
|
||||||
Kirigami.Avatar {
|
Kirigami.Avatar {
|
||||||
id: replyAvatar
|
id: replyAvatar
|
||||||
@@ -48,25 +99,26 @@ Item {
|
|||||||
implicitWidth: Kirigami.Units.iconSizes.small
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
implicitHeight: Kirigami.Units.iconSizes.small
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
|
||||||
name: replyComponent.name || ""
|
source: root.author.avatarSource
|
||||||
color: replyComponent.color
|
name: root.author.displayName || ""
|
||||||
|
color: root.author.color
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
color: replyComponent.color
|
color: root.author.color
|
||||||
text: replyComponent.name
|
text: root.author.displayName
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
Loader {
|
Loader {
|
||||||
id: loader
|
id: loader
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumHeight: loader.item && (model.reply.type == MessageEventModel.Image || model.reply.type == MessageEventModel.Sticker) ? loader.item.height : -1
|
Layout.maximumHeight: loader.item && (root.type == MessageEventModel.Image || root.type == MessageEventModel.Sticker) ? loader.item.height : -1
|
||||||
Layout.columnSpan: 2
|
Layout.columnSpan: 2
|
||||||
|
|
||||||
sourceComponent: {
|
sourceComponent: {
|
||||||
switch (model.reply.type) {
|
switch (root.type) {
|
||||||
case MessageEventModel.Image:
|
case MessageEventModel.Image:
|
||||||
case MessageEventModel.Sticker:
|
case MessageEventModel.Sticker:
|
||||||
return imageComponent;
|
return imageComponent;
|
||||||
@@ -89,14 +141,14 @@ Item {
|
|||||||
}
|
}
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onTapped: replyComponent.replyClicked()
|
onTapped: root.replyClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: textComponent
|
id: textComponent
|
||||||
RichLabel {
|
RichLabel {
|
||||||
textMessage: model.reply.display
|
textMessage: root.display
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
@@ -106,7 +158,7 @@ Item {
|
|||||||
TapHandler {
|
TapHandler {
|
||||||
enabled: !hoveredLink
|
enabled: !hoveredLink
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onTapped: replyComponent.replyClicked()
|
onTapped: root.replyClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,15 +168,15 @@ Item {
|
|||||||
id: image
|
id: image
|
||||||
|
|
||||||
property var imageWidth: {
|
property var imageWidth: {
|
||||||
if (replyComponent.mediaInfo.width > 0) {
|
if (root.mediaInfo.width > 0) {
|
||||||
return replyComponent.mediaInfo.width;
|
return root.mediaInfo.width;
|
||||||
} else {
|
} else {
|
||||||
return sourceSize.width;
|
return sourceSize.width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property var imageHeight: {
|
property var imageHeight: {
|
||||||
if (replyComponent.mediaInfo.height > 0) {
|
if (root.mediaInfo.height > 0) {
|
||||||
return replyComponent.mediaInfo.height;
|
return root.mediaInfo.height;
|
||||||
} else {
|
} else {
|
||||||
return sourceSize.height;
|
return sourceSize.height;
|
||||||
}
|
}
|
||||||
@@ -134,15 +186,15 @@ Item {
|
|||||||
|
|
||||||
height: width / aspectRatio
|
height: width / aspectRatio
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
source: mediaInfo.source
|
source: root.mediaInfo.source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component {
|
Component {
|
||||||
id: mimeComponent
|
id: mimeComponent
|
||||||
MimeComponent {
|
MimeComponent {
|
||||||
mimeIconSource: replyComponent.mediaInfo.mimeIcon
|
mimeIconSource: root.mediaInfo.mimeIcon
|
||||||
label: model.reply.display
|
label: root.display
|
||||||
subLabel: model.reply.type === MessageEventModel.File ? Controller.formatByteSize(replyComponent.mediaInfo.size) : Controller.formatDuration(replyComponent.mediaInfo.duration)
|
subLabel: root.type === MessageEventModel.File ? Controller.formatByteSize(root.mediaInfo.size) : Controller.formatDuration(root.mediaInfo.duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component {
|
Component {
|
||||||
|
|||||||
@@ -8,13 +8,35 @@ import QtQuick.Layouts 1.15
|
|||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
import org.kde.kirigami 2.15 as Kirigami
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show the rich display text of text message.
|
||||||
|
*/
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: contentLabel
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The rich text message to display.
|
||||||
|
*/
|
||||||
|
property string textMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether this message is replying to another.
|
||||||
|
*/
|
||||||
|
property bool isReply
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Regex for detecting a message with a single emoji.
|
||||||
|
*/
|
||||||
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u20D0-\u2fff]|[\u3190-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
|
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u20D0-\u2fff]|[\u3190-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Regex for detecting a message with a spoiler.
|
||||||
|
*/
|
||||||
readonly property var hasSpoiler: /data-mx-spoiler/g
|
readonly property var hasSpoiler: /data-mx-spoiler/g
|
||||||
|
|
||||||
property string textMessage: model.display
|
/**
|
||||||
|
* @brief Whether a spoiler should be revealed.
|
||||||
|
*/
|
||||||
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
||||||
|
|
||||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
||||||
@@ -23,7 +45,7 @@ TextEdit {
|
|||||||
|
|
||||||
// Work around QTBUG 93281
|
// Work around QTBUG 93281
|
||||||
Component.onCompleted: if (text.includes("<img")) {
|
Component.onCompleted: if (text.includes("<img")) {
|
||||||
Controller.forceRefreshTextDocument(contentLabel.textDocument, contentLabel)
|
Controller.forceRefreshTextDocument(root.textDocument, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
text: "<style>
|
text: "<style>
|
||||||
@@ -63,7 +85,7 @@ a{
|
|||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||||
selectionColor: Kirigami.Theme.highlightColor
|
selectionColor: Kirigami.Theme.highlightColor
|
||||||
font.pointSize: model.reply === undefined && isEmoji.test(model.display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
|
font.pointSize: !root.isReply && isEmoji.test(textMessage) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
|
||||||
selectByMouse: !Kirigami.Settings.isMobile
|
selectByMouse: !Kirigami.Settings.isMobile
|
||||||
readOnly: true
|
readOnly: true
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
|
|||||||
@@ -9,34 +9,276 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The base delegate for all messages in the timeline.
|
||||||
|
*
|
||||||
|
* This supports a message bubble plus sender avatar for each message as well as reactions
|
||||||
|
* and read markers. A date section can be show for when the message is on a different
|
||||||
|
* day to the previous one.
|
||||||
|
*
|
||||||
|
* The component is designed for all messages, positioning them in the timeline with
|
||||||
|
* variable padding depending on the window width. Local user messages are highlighted
|
||||||
|
* and can also be aligned to the right if configured.
|
||||||
|
*
|
||||||
|
* This component also supports a compact mode where the padding is adjusted, the
|
||||||
|
* background is hidden and the delegate spans the full width of the timeline.
|
||||||
|
*/
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string eventId: model.eventId
|
/**
|
||||||
|
* @brief The index of the delegate in the model.
|
||||||
|
*/
|
||||||
|
required property var index
|
||||||
|
|
||||||
property var author: model.author
|
/**
|
||||||
|
* @brief The matrix ID of the message event.
|
||||||
|
*/
|
||||||
|
required property string eventId
|
||||||
|
|
||||||
property int delegateType: model.delegateType
|
/**
|
||||||
|
* @brief The timestamp of the message.
|
||||||
|
*/
|
||||||
|
required property var time
|
||||||
|
|
||||||
property bool verified: model.verified
|
/**
|
||||||
|
* @brief The message author.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - id - The matrix ID of the author.
|
||||||
|
* - isLocalUser - Whether the author is the local user.
|
||||||
|
* - avatarSource - The mxc URL for the author's avatar in the current room.
|
||||||
|
* - avatarMediaId - The media ID of the author's avatar.
|
||||||
|
* - avatarUrl - The mxc URL for the author's avatar.
|
||||||
|
* - displayName - The display name of the author.
|
||||||
|
* - display - The name of the author.
|
||||||
|
* - color - The color for the author.
|
||||||
|
* - object - The NeoChatUser object for the author.
|
||||||
|
*
|
||||||
|
* @sa NeoChatUser
|
||||||
|
*/
|
||||||
|
required property var author
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the author should be shown.
|
||||||
|
*/
|
||||||
|
required property bool showAuthor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The delegate type of the message.
|
||||||
|
*/
|
||||||
|
required property int delegateType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message.
|
||||||
|
*/
|
||||||
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message as plain text.
|
||||||
|
*/
|
||||||
|
required property string plainText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The formatted body of the message.
|
||||||
|
*/
|
||||||
|
required property string formattedBody
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The date of the event as a string.
|
||||||
|
*/
|
||||||
|
required property string section
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the section header should be shown.
|
||||||
|
*/
|
||||||
|
required property bool showSection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A model with the reactions to the message in.
|
||||||
|
*/
|
||||||
|
required property var reaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the reaction component should be shown.
|
||||||
|
*/
|
||||||
|
required property bool showReactions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A model with the first 5 other user read markers for this message.
|
||||||
|
*/
|
||||||
|
required property var readMarkers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief String with the display name and matrix ID of the other user read markers.
|
||||||
|
*/
|
||||||
|
required property string readMarkersString
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The number of other users at the event after the first 5.
|
||||||
|
*/
|
||||||
|
required property var excessReadMarkers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the other user read marker component should be shown.
|
||||||
|
*/
|
||||||
|
required property bool showReadMarkers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The reply author.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - id - The matrix ID of the reply author.
|
||||||
|
* - isLocalUser - Whether the reply author is the local user.
|
||||||
|
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
|
||||||
|
* - avatarMediaId - The media ID of the reply author's avatar.
|
||||||
|
* - avatarUrl - The mxc URL for the reply author's avatar.
|
||||||
|
* - displayName - The display name of the reply author.
|
||||||
|
* - display - The name of the reply author.
|
||||||
|
* - color - The color for the reply author.
|
||||||
|
* - object - The NeoChatUser object for the reply author.
|
||||||
|
*
|
||||||
|
* @sa NeoChatUser
|
||||||
|
*/
|
||||||
|
required property var replyAuthor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The reply content.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - display - The display text of the reply.
|
||||||
|
* - type - The delegate type of the reply.
|
||||||
|
*/
|
||||||
|
required property var reply
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The media info for the reply event.
|
||||||
|
*
|
||||||
|
* This could be an image, audio, video or file.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media.
|
||||||
|
* - mimeIcon - The MIME icon name.
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - duration - The length in seconds of the audio media (audio/video only).
|
||||||
|
* - width - The width in pixels of the audio media (image/video only).
|
||||||
|
* - height - The height in pixels of the audio media (image/video only).
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
||||||
|
*/
|
||||||
|
required property var replyMediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether this message is replying to another.
|
||||||
|
*/
|
||||||
|
required property bool isReply
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether this message has a local user mention.
|
||||||
|
*/
|
||||||
|
required property bool isHighlighted
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether an event is waiting to be accepted by the server.
|
||||||
|
*/
|
||||||
|
required property bool isPending
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Progress info when downloading files.
|
||||||
|
*
|
||||||
|
* @sa Quotient::FileTransferInfo
|
||||||
|
*/
|
||||||
|
required property var progressInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether an encrypted message is sent in a verified session.
|
||||||
|
*/
|
||||||
|
required property bool verified
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The mime type of the message's file or media.
|
||||||
|
*/
|
||||||
|
required property var mimeType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The full message source JSON.
|
||||||
|
*/
|
||||||
|
required property var source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The x position of the message bubble.
|
||||||
|
*
|
||||||
|
* @note Used for positioning the hover actions.
|
||||||
|
*/
|
||||||
readonly property real bubbleX: bubble.x + bubble.anchors.leftMargin
|
readonly property real bubbleX: bubble.x + bubble.anchors.leftMargin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The y position of the message bubble.
|
||||||
|
*
|
||||||
|
* @note Used for positioning the hover actions.
|
||||||
|
*/
|
||||||
readonly property alias bubbleY: mainContainer.y
|
readonly property alias bubbleY: mainContainer.y
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The width of the message bubble.
|
||||||
|
*
|
||||||
|
* @note Used for sizing the hover actions.
|
||||||
|
*/
|
||||||
readonly property alias bubbleWidth: bubble.width
|
readonly property alias bubbleWidth: bubble.width
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether this message is hovered.
|
||||||
|
*/
|
||||||
readonly property alias hovered: bubble.hovered
|
readonly property alias hovered: bubble.hovered
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Open the context menu for the message.
|
||||||
|
*/
|
||||||
signal openContextMenu
|
signal openContextMenu
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Open the any message media externally.
|
||||||
|
*/
|
||||||
signal openExternally()
|
signal openExternally()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The reply has been clicked.
|
||||||
|
*/
|
||||||
signal replyClicked(string eventID)
|
signal replyClicked(string eventID)
|
||||||
|
|
||||||
onReplyClicked: ListView.view.goToEvent(eventID)
|
onReplyClicked: ListView.view.goToEvent(eventID)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The component to display the delegate type.
|
||||||
|
*
|
||||||
|
* This is used by the inherited delegates to assign a component to visualise
|
||||||
|
* the message content for that delegate type.
|
||||||
|
*/
|
||||||
default property alias innerObject : column.children
|
default property alias innerObject : column.children
|
||||||
|
|
||||||
property Item hoverComponent: hoverActions ?? null
|
/**
|
||||||
|
* @brief Whether the bubble background is enabled.
|
||||||
|
*/
|
||||||
property bool cardBackground: true
|
property bool cardBackground: true
|
||||||
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout
|
|
||||||
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
|
/**
|
||||||
|
* @brief Whether local user messages should be aligned right.
|
||||||
|
*
|
||||||
|
* TODO: make private
|
||||||
|
*/
|
||||||
|
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the message should be highlighted.
|
||||||
|
*/
|
||||||
|
property bool showHighlight: root.isHighlighted || isTemporaryHighlighted
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the message should temporarily be highlighted.
|
||||||
|
*
|
||||||
|
* Normally triggered when jumping to the event in the timeline, e.g. when a reply
|
||||||
|
* is clicked.
|
||||||
|
*/
|
||||||
property bool isTemporaryHighlighted: false
|
property bool isTemporaryHighlighted: false
|
||||||
|
|
||||||
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
|
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
|
||||||
@@ -48,6 +290,7 @@ ColumnLayout {
|
|||||||
onTriggered: isTemporaryHighlighted = false
|
onTriggered: isTemporaryHighlighted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: make these private
|
||||||
// The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size
|
// The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size
|
||||||
// extraWidth defines this as the excess after a certain ListView width, capped to a max value
|
// extraWidth defines this as the excess after a certain ListView width, capped to a max value
|
||||||
readonly property int extraWidth: parent ? (parent.width >= Kirigami.Units.gridUnit * 46 ? Math.min((parent.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0) : 0
|
readonly property int extraWidth: parent ? (parent.width >= Kirigami.Units.gridUnit * 46 ? Math.min((parent.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0) : 0
|
||||||
@@ -89,23 +332,23 @@ ColumnLayout {
|
|||||||
id: sectionDelegate
|
id: sectionDelegate
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: model.showSection
|
visible: root.showSection
|
||||||
labelText: model.showSection ? section : ""
|
labelText: root.section
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ItemDelegate {
|
QQC2.ItemDelegate {
|
||||||
id: mainContainer
|
id: mainContainer
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
|
Layout.topMargin: root.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
|
||||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height)
|
implicitHeight: Math.max(root.showAuthor ? avatar.implicitHeight : 0, bubble.height)
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (model.isReply && model.reply === undefined) {
|
if (root.isReply && root.reply === undefined) {
|
||||||
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(collapseStateProxyModel.mapToSource(collapseStateProxyModel.index(model.index, 0))))
|
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(collapseStateProxyModel.mapToSource(collapseStateProxyModel.index(root.index, 0))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,20 +373,20 @@ ColumnLayout {
|
|||||||
leftMargin: Kirigami.Units.smallSpacing
|
leftMargin: Kirigami.Units.smallSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: model.showAuthor &&
|
visible: root.showAuthor &&
|
||||||
Config.showAvatarInTimeline &&
|
Config.showAvatarInTimeline &&
|
||||||
(Config.compactLayout || !showUserMessageOnRight)
|
(Config.compactLayout || !showUserMessageOnRight)
|
||||||
name: model.author.name ?? model.author.displayName
|
name: root.author.name ?? root.author.displayName
|
||||||
source: model.author.avatarSource
|
source: root.author.avatarSource
|
||||||
color: model.author.color
|
color: root.author.color
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: {
|
onClicked: {
|
||||||
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
|
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
room: currentRoom,
|
room: currentRoom,
|
||||||
user: author.object,
|
user: root.author.object,
|
||||||
displayName: author.displayName
|
displayName: root.author.displayName
|
||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -199,7 +442,7 @@ ColumnLayout {
|
|||||||
width: height
|
width: height
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||||
visible: model.isPending && Config.showLocalMessagesOnRight
|
visible: root.isPending && Config.showLocalMessagesOnRight
|
||||||
}
|
}
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: column
|
id: column
|
||||||
@@ -208,17 +451,17 @@ ColumnLayout {
|
|||||||
id: rowLayout
|
id: rowLayout
|
||||||
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
visible: model.showAuthor
|
visible: root.showAuthor
|
||||||
|
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: nameLabel
|
id: nameLabel
|
||||||
|
|
||||||
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
|
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
|
||||||
|
|
||||||
text: visible ? author.displayName : ""
|
text: visible ? root.author.displayName : ""
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
color: author.color
|
color: root.author.color
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -226,9 +469,9 @@ ColumnLayout {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
|
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
room: currentRoom,
|
room: currentRoom,
|
||||||
user: author.object,
|
user: root.author.object,
|
||||||
displayName: author.displayName,
|
displayName: root.author.displayName,
|
||||||
avatarSource: author.avatarSource
|
avatarSource: root.author.avatarSource
|
||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,10 +479,10 @@ ColumnLayout {
|
|||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: timeLabel
|
id: timeLabel
|
||||||
|
|
||||||
text: visible ? model.time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
|
text: visible ? root.time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
|
||||||
color: Kirigami.Theme.disabledTextColor
|
color: Kirigami.Theme.disabledTextColor
|
||||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||||
QQC2.ToolTip.text: model.time.toLocaleString(Qt.locale(), Locale.LongFormat)
|
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat)
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
@@ -252,20 +495,20 @@ ColumnLayout {
|
|||||||
|
|
||||||
Layout.maximumWidth: contentMaxWidth
|
Layout.maximumWidth: contentMaxWidth
|
||||||
|
|
||||||
active: model.isReply
|
active: root.isReply
|
||||||
visible: active
|
visible: active
|
||||||
|
|
||||||
sourceComponent: ReplyComponent {
|
sourceComponent: ReplyComponent {
|
||||||
name: currentRoom.htmlSafeMemberName(model.replyAuthor.id)
|
author: root.replyAuthor
|
||||||
avatar: model.replyAuthor.avatarSource
|
type: root.reply.type
|
||||||
color: model.replyAuthor.color
|
display: root.reply.display
|
||||||
mediaInfo: model.replyMediaInfo
|
mediaInfo: root.replyMediaInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: replyLoader.item
|
target: replyLoader.item
|
||||||
function onReplyClicked() {
|
function onReplyClicked() {
|
||||||
replyClicked(reply.eventId)
|
replyClicked(root.reply.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +518,7 @@ ColumnLayout {
|
|||||||
width: height
|
width: height
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||||
visible: model.isPending && !Config.showLocalMessagesOnRight
|
visible: root.isPending && !Config.showLocalMessagesOnRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,9 +529,9 @@ ColumnLayout {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
color: {
|
color: {
|
||||||
if (model.author.isLocalUser) {
|
if (root.author.isLocalUser) {
|
||||||
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
|
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
|
||||||
} else if (root.isHighlighted) {
|
} else if (root.showHighlight) {
|
||||||
return Kirigami.Theme.positiveBackgroundColor
|
return Kirigami.Theme.positiveBackgroundColor
|
||||||
} else {
|
} else {
|
||||||
return Kirigami.Theme.backgroundColor
|
return Kirigami.Theme.backgroundColor
|
||||||
@@ -296,7 +539,7 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
radius: Kirigami.Units.smallSpacing
|
radius: Kirigami.Units.smallSpacing
|
||||||
shadow.size: Kirigami.Units.smallSpacing
|
shadow.size: Kirigami.Units.smallSpacing
|
||||||
shadow.color: root.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
shadow.color: root.showHighlight ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||||
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
@@ -332,18 +575,18 @@ ColumnLayout {
|
|||||||
Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
|
Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
|
||||||
Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
|
Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
|
||||||
|
|
||||||
visible: showReactions
|
visible: root.showReactions
|
||||||
model: reaction
|
model: root.reaction
|
||||||
|
|
||||||
onReactionClicked: (reaction) => currentRoom.toggleReaction(eventId, reaction)
|
onReactionClicked: (reaction) => currentRoom.toggleReaction(root.eventId, reaction)
|
||||||
}
|
}
|
||||||
AvatarFlow {
|
AvatarFlow {
|
||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
visible: showReadMarkers
|
visible: root.showReadMarkers
|
||||||
model: readMarkers
|
model: root.readMarkers
|
||||||
toolTipText: readMarkersString
|
toolTipText: root.readMarkersString
|
||||||
excessAvatars: excessReadMarkers
|
excessAvatars: root.excessReadMarkers
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVisibleInTimeline() {
|
function isVisibleInTimeline() {
|
||||||
@@ -352,31 +595,31 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Open message context dialog for file and videos
|
/// Open message context dialog for file and videos
|
||||||
function openFileContext(event, file) {
|
function openFileContext(file) {
|
||||||
const contextMenu = fileDelegateContextMenu.createObject(root, {
|
const contextMenu = fileDelegateContextMenu.createObject(root, {
|
||||||
author: event.author,
|
author: root.author,
|
||||||
message: event.plainText,
|
message: root.plainText,
|
||||||
eventId: event.eventId,
|
eventId: root.eventId,
|
||||||
source: event.source,
|
source: root.source,
|
||||||
file: file,
|
file: file,
|
||||||
mimeType: event.mimeType,
|
mimeType: root.mimeType,
|
||||||
progressInfo: event.progressInfo,
|
progressInfo: root.progressInfo,
|
||||||
plainMessage: event.plainText,
|
plainMessage: root.plainText,
|
||||||
});
|
});
|
||||||
contextMenu.open();
|
contextMenu.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open context menu for normal message
|
/// Open context menu for normal message
|
||||||
function openMessageContext(event, selectedText, plainMessage) {
|
function openMessageContext(selectedText) {
|
||||||
const contextMenu = messageDelegateContextMenu.createObject(root, {
|
const contextMenu = messageDelegateContextMenu.createObject(root, {
|
||||||
selectedText: selectedText,
|
selectedText: selectedText,
|
||||||
author: event.author,
|
author: root.author,
|
||||||
message: event.plainText,
|
message: root.plainText,
|
||||||
eventId: event.eventId,
|
eventId: root.eventId,
|
||||||
formattedBody: event.formattedBody,
|
formattedBody: root.formattedBody,
|
||||||
source: event.source,
|
source: root.source,
|
||||||
eventType: event.eventType,
|
eventType: root.delegateType,
|
||||||
plainMessage: event.plainText,
|
plainMessage: root.plainText,
|
||||||
});
|
});
|
||||||
contextMenu.open();
|
contextMenu.open();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,21 +12,59 @@ import org.kde.kirigamiaddons.labs.components 1.0 as Components
|
|||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A timeline delegate for a video message.
|
||||||
|
*
|
||||||
|
* @inherit TimelineContainer
|
||||||
|
*/
|
||||||
TimelineContainer {
|
TimelineContainer {
|
||||||
id: videoDelegate
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The media info for the event.
|
||||||
|
*
|
||||||
|
* This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be video/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be video-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - duration - The length in seconds of the audio media.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
required property var mediaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the media has been downloaded.
|
||||||
|
*/
|
||||||
|
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the video should be played when downloaded.
|
||||||
|
*/
|
||||||
property bool playOnFinished: false
|
property bool playOnFinished: false
|
||||||
readonly property bool downloaded: progressInfo && progressInfo.completed
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the video can be streamed.
|
||||||
|
*/
|
||||||
property bool supportStreaming: true
|
property bool supportStreaming: true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width of the image.
|
||||||
|
*/
|
||||||
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum height of the image.
|
||||||
|
*/
|
||||||
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
||||||
|
|
||||||
onOpenContextMenu: openFileContext(model, vid)
|
onOpenContextMenu: openFileContext(vid)
|
||||||
|
|
||||||
onDownloadedChanged: {
|
onDownloadedChanged: {
|
||||||
if (downloaded) {
|
if (downloaded) {
|
||||||
vid.source = progressInfo.localPath
|
vid.source = root.progressInfo.localPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloaded && playOnFinished) {
|
if (downloaded && playOnFinished) {
|
||||||
@@ -39,22 +77,22 @@ TimelineContainer {
|
|||||||
id: vid
|
id: vid
|
||||||
|
|
||||||
property var videoWidth: {
|
property var videoWidth: {
|
||||||
if (model.mediaInfo.width > 0) {
|
if (root.mediaInfo.width > 0) {
|
||||||
return model.mediaInfo.width;
|
return root.mediaInfo.width;
|
||||||
} else if (metaData.resolution && metaData.resolution.width) {
|
} else if (metaData.resolution && metaData.resolution.width) {
|
||||||
return metaData.resolution.width;
|
return metaData.resolution.width;
|
||||||
} else {
|
} else {
|
||||||
return videoDelegate.contentMaxWidth;
|
return root.contentMaxWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property var videoHeight: {
|
property var videoHeight: {
|
||||||
if (model.mediaInfo.height > 0) {
|
if (root.mediaInfo.height > 0) {
|
||||||
return model.mediaInfo.height;
|
return root.mediaInfo.height;
|
||||||
} else if (metaData.resolution && metaData.resolution.height) {
|
} else if (metaData.resolution && metaData.resolution.height) {
|
||||||
return metaData.resolution.height;
|
return metaData.resolution.height;
|
||||||
} else {
|
} else {
|
||||||
// Default to a 16:9 placeholder
|
// Default to a 16:9 placeholder
|
||||||
return videoDelegate.contentMaxWidth / 16 * 9;
|
return root.contentMaxWidth / 16 * 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,11 +107,11 @@ TimelineContainer {
|
|||||||
|
|
||||||
readonly property size maxSize: {
|
readonly property size maxSize: {
|
||||||
if (limitWidth) {
|
if (limitWidth) {
|
||||||
let width = Math.min(videoDelegate.contentMaxWidth, videoDelegate.maxWidth);
|
let width = Math.min(root.contentMaxWidth, root.maxWidth);
|
||||||
let height = width / aspectRatio;
|
let height = width / aspectRatio;
|
||||||
return Qt.size(width, height);
|
return Qt.size(width, height);
|
||||||
} else {
|
} else {
|
||||||
let height = Math.min(videoDelegate.maxHeight, videoDelegate.contentMaxWidth / aspectRatio);
|
let height = Math.min(root.maxHeight, root.contentMaxWidth / aspectRatio);
|
||||||
let width = height * aspectRatio;
|
let width = height * aspectRatio;
|
||||||
return Qt.size(width, height);
|
return Qt.size(width, height);
|
||||||
}
|
}
|
||||||
@@ -91,7 +129,7 @@ TimelineContainer {
|
|||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: "notDownloaded"
|
name: "notDownloaded"
|
||||||
when: !model.progressInfo.completed && !model.progressInfo.active
|
when: !root.progressInfo.completed && !root.progressInfo.active
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: noDownloadLabel
|
target: noDownloadLabel
|
||||||
visible: true
|
visible: true
|
||||||
@@ -103,7 +141,7 @@ TimelineContainer {
|
|||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "downloading"
|
name: "downloading"
|
||||||
when: model.progressInfo.active && !model.progressInfo.completed
|
when: root.progressInfo.active && !root.progressInfo.completed
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: downloadBar
|
target: downloadBar
|
||||||
visible: true
|
visible: true
|
||||||
@@ -111,7 +149,7 @@ TimelineContainer {
|
|||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "paused"
|
name: "paused"
|
||||||
when: model.progressInfo.completed && (vid.playbackState === MediaPlayer.StoppedState || vid.playbackState === MediaPlayer.PausedState)
|
when: root.progressInfo.completed && (vid.playbackState === MediaPlayer.StoppedState || vid.playbackState === MediaPlayer.PausedState)
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: videoControls
|
target: videoControls
|
||||||
stateVisible: true
|
stateVisible: true
|
||||||
@@ -124,7 +162,7 @@ TimelineContainer {
|
|||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "playing"
|
name: "playing"
|
||||||
when: model.progressInfo.completed && vid.playbackState === MediaPlayer.PlayingState
|
when: root.progressInfo.completed && vid.playbackState === MediaPlayer.PlayingState
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: videoControls
|
target: videoControls
|
||||||
stateVisible: true
|
stateVisible: true
|
||||||
@@ -154,7 +192,7 @@ TimelineContainer {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
source: model.mediaInfo.tempInfo.source
|
source: root.mediaInfo.tempInfo.source
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,8 +228,8 @@ TimelineContainer {
|
|||||||
width: parent.width * 0.8
|
width: parent.width * 0.8
|
||||||
|
|
||||||
from: 0
|
from: 0
|
||||||
to: progressInfo.total
|
to: root.progressInfo.total
|
||||||
value: progressInfo.progress
|
value: root.progressInfo.progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,13 +353,22 @@ TimelineContainer {
|
|||||||
text: i18n("Maximize")
|
text: i18n("Maximize")
|
||||||
icon.name: "view-fullscreen"
|
icon.name: "view-fullscreen"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
videoDelegate.ListView.view.interactive = false
|
root.ListView.view.interactive = false
|
||||||
vid.pause()
|
vid.pause()
|
||||||
var popup = maximizeVideoComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
var popup = maximizeVideoComponent.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
modelData: model,
|
eventId: root.eventId,
|
||||||
|
time: root.time,
|
||||||
|
author: root.author,
|
||||||
|
delegateType: root.delegateType,
|
||||||
|
plainText: root.plainText,
|
||||||
|
caption: root.display,
|
||||||
|
mediaInfo: root.mediaInfo,
|
||||||
|
progressInfo: root.progressInfo,
|
||||||
|
mimeType: root.mimeType,
|
||||||
|
source: root.source
|
||||||
})
|
})
|
||||||
popup.closed.connect(() => {
|
popup.closed.connect(() => {
|
||||||
videoDelegate.ListView.view.interactive = true
|
root.ListView.view.interactive = true
|
||||||
popup.destroy()
|
popup.destroy()
|
||||||
})
|
})
|
||||||
popup.open()
|
popup.open()
|
||||||
@@ -364,14 +411,14 @@ TimelineContainer {
|
|||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onTapped: if (vid.supportStreaming || progressInfo.completed) {
|
onTapped: if (vid.supportStreaming || root.progressInfo.completed) {
|
||||||
if (vid.playbackState == MediaPlayer.PlayingState) {
|
if (vid.playbackState == MediaPlayer.PlayingState) {
|
||||||
vid.pause()
|
vid.pause()
|
||||||
} else {
|
} else {
|
||||||
vid.play()
|
vid.play()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
videoDelegate.downloadAndPlay()
|
root.downloadAndPlay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +428,7 @@ TimelineContainer {
|
|||||||
playSavedFile()
|
playSavedFile()
|
||||||
} else {
|
} else {
|
||||||
playOnFinished = true
|
playOnFinished = true
|
||||||
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
|
currentRoom.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user