diff --git a/imports/Spectral/Component/Timeline/FileDelegate.qml b/imports/Spectral/Component/Timeline/FileDelegate.qml index f8bd0d7..1fa994c 100644 --- a/imports/Spectral/Component/Timeline/FileDelegate.qml +++ b/imports/Spectral/Component/Timeline/FileDelegate.qml @@ -14,7 +14,7 @@ import Spectral.Menu.Timeline 2.0 import Spectral.Font 0.1 import Spectral.Effect 2.0 -ColumnLayout { +RowLayout { readonly property bool avatarVisible: !sentByMe && showAuthor readonly property bool sentByMe: author === currentRoom.localUser @@ -23,196 +23,175 @@ ColumnLayout { id: root - spacing: 0 + spacing: 4 onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile() - Label { - Layout.leftMargin: 48 + z: -5 - text: author.displayName + Avatar { + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + Layout.alignment: Qt.AlignBottom visible: avatarVisible + hint: author.displayName + source: author.avatarMediaId - font.pixelSize: 13 - verticalAlignment: Text.AlignVCenter + Component { + id: userDetailDialog + + UserDetailDialog {} + } + + RippleEffect { + anchors.fill: parent + + circular: true + + onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open() + } } - RowLayout { - z: -5 + Label { + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 - id: messageRow + visible: !(sentByMe || avatarVisible) + } - spacing: 4 + Control { + Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48 - Avatar { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - Layout.alignment: Qt.AlignTop + padding: 12 - visible: avatarVisible - hint: author.displayName - source: author.avatarMediaId + contentItem: RowLayout { + ToolButton { + contentItem: MaterialIcon { + icon: progressInfo.completed ? "\ue5ca" : "\ue2c4" + } - Component { - id: userDetailDialog - - UserDetailDialog {} + onClicked: progressInfo.completed ? openSavedFile() : saveFileAs() } - RippleEffect { + ColumnLayout { + Label { + Layout.fillWidth: true + + text: display + wrapMode: Label.Wrap + font.pixelSize: 18 + font.weight: Font.Medium + font.capitalization: Font.AllUppercase + } + + Label { + Layout.fillWidth: true + + text: progressInfo.active ? (humanSize(progressInfo.progress) + "/" + humanSize(progressInfo.total)) : humanSize(content.info ? content.info.size : 0) + color: MPalette.lighter + wrapMode: Label.Wrap + } + } + } + + background: Rectangle { + color: MPalette.background + radius: 18 + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + + width: parent.width / 2 + height: parent.height / 2 + + visible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 + } + + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + + width: parent.width / 2 + height: parent.height / 2 + + visible: sentByMe && (bubbleShape == 3 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + + width: parent.width / 2 + height: parent.height / 2 + + visible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.right: parent.right + + width: parent.width / 2 + height: parent.height / 2 + + visible: sentByMe && (bubbleShape == 1 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 + } + + AutoMouseArea { anchors.fill: parent - circular: true + id: messageMouseArea - onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open() - } - } - - Label { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - Layout.alignment: Qt.AlignTop - - visible: !(sentByMe || avatarVisible) - - text: Qt.formatTime(time, "hh:mm AP") - color: "#5B7480" - - font.pixelSize: 10 - horizontalAlignment: Label.AlignHCenter - verticalAlignment: Label.AlignVCenter - } - - Control { - Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48 - - padding: 12 - - contentItem: RowLayout { - ToolButton { - contentItem: MaterialIcon { - icon: progressInfo.completed ? "\ue5ca" : "\ue2c4" - } - - onClicked: progressInfo.completed ? openSavedFile() : saveFileAs() + onSecondaryClicked: { + var contextMenu = fileDelegateContextMenu.createObject(ApplicationWindow.overlay) + contextMenu.viewSource.connect(function() { + messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() + }) + contextMenu.downloadAndOpen.connect(downloadAndOpen) + contextMenu.saveFileAs.connect(saveFileAs) + contextMenu.reply.connect(function() { + roomPanelInput.replyUser = author + roomPanelInput.replyEventID = eventId + roomPanelInput.replyContent = message + roomPanelInput.isReply = true + roomPanelInput.focus() + }) + contextMenu.redact.connect(function() { + currentRoom.redactEvent(eventId) + }) + contextMenu.popup() } - ColumnLayout { - Label { - Layout.alignment: Qt.AlignVCenter + Component { + id: messageSourceDialog - text: display - font.pixelSize: 18 - font.weight: Font.Medium - font.capitalization: Font.AllUppercase - } - - Label { - text: progressInfo.active ? (humanSize(progressInfo.progress) + "/" + humanSize(progressInfo.total)) : humanSize(content.info ? content.info.size : 0) - color: MPalette.lighter - } - } - } - - background: Rectangle { - color: MPalette.background - radius: 18 - - Rectangle { - anchors.top: parent.top - anchors.left: parent.left - - width: parent.width / 2 - height: parent.height / 2 - - visible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 + MessageSourceDialog {} } - Rectangle { - anchors.top: parent.top - anchors.right: parent.right + Component { + id: openFolderDialog - width: parent.width / 2 - height: parent.height / 2 - - visible: sentByMe && (bubbleShape == 3 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 + OpenFolderDialog {} } - Rectangle { - anchors.bottom: parent.bottom - anchors.left: parent.left + Component { + id: fileDelegateContextMenu - width: parent.width / 2 - height: parent.height / 2 - - visible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 - } - - Rectangle { - anchors.bottom: parent.bottom - anchors.right: parent.right - - width: parent.width / 2 - height: parent.height / 2 - - visible: sentByMe && (bubbleShape == 1 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 - } - - AutoMouseArea { - anchors.fill: parent - - id: messageMouseArea - - onSecondaryClicked: { - var contextMenu = fileDelegateContextMenu.createObject(ApplicationWindow.overlay) - contextMenu.viewSource.connect(function() { - messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() - }) - contextMenu.downloadAndOpen.connect(downloadAndOpen) - contextMenu.saveFileAs.connect(saveFileAs) - contextMenu.reply.connect(function() { - roomPanelInput.replyUser = author - roomPanelInput.replyEventID = eventId - roomPanelInput.replyContent = message - roomPanelInput.isReply = true - roomPanelInput.focus() - }) - contextMenu.redact.connect(function() { - currentRoom.redactEvent(eventId) - }) - contextMenu.popup() - } - - Component { - id: messageSourceDialog - - MessageSourceDialog {} - } - - Component { - id: openFolderDialog - - OpenFolderDialog {} - } - - Component { - id: fileDelegateContextMenu - - FileDelegateContextMenu {} - } + FileDelegateContextMenu {} } } } @@ -246,7 +225,6 @@ ColumnLayout { if (Qt.openUrlExternally(progressInfo.localDir)) return; } - function humanSize(bytes) { if (!bytes) diff --git a/imports/Spectral/Component/Timeline/ImageDelegate.qml b/imports/Spectral/Component/Timeline/ImageDelegate.qml index d46ede4..3ee9713 100644 --- a/imports/Spectral/Component/Timeline/ImageDelegate.qml +++ b/imports/Spectral/Component/Timeline/ImageDelegate.qml @@ -35,8 +35,8 @@ RowLayout { } Avatar { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 Layout.alignment: Qt.AlignBottom visible: avatarVisible @@ -59,8 +59,8 @@ RowLayout { } Label { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 visible: !(sentByMe || avatarVisible) } @@ -73,7 +73,7 @@ RowLayout { } Image { - Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + root.spacing : 0) - 48 + Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48 id: img diff --git a/imports/Spectral/Component/Timeline/MessageDelegate.qml b/imports/Spectral/Component/Timeline/MessageDelegate.qml index 7f74742..2219993 100644 --- a/imports/Spectral/Component/Timeline/MessageDelegate.qml +++ b/imports/Spectral/Component/Timeline/MessageDelegate.qml @@ -34,8 +34,8 @@ ColumnLayout { spacing: 4 Avatar { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 Layout.alignment: Qt.AlignBottom visible: avatarVisible @@ -58,14 +58,14 @@ ColumnLayout { } Item { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 visible: !(sentByMe || avatarVisible) } Control { - Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48 + Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + messageRow.spacing : 0) - 48 verticalPadding: 8 horizontalPadding: 16 @@ -259,7 +259,7 @@ ColumnLayout { RowLayout { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft - Layout.leftMargin: sentByMe ? undefined : 32 + messageRow.spacing + 12 + Layout.leftMargin: sentByMe ? undefined : 36 + messageRow.spacing + 12 Layout.rightMargin: sentByMe ? 12 : undefined Layout.bottomMargin: 4 diff --git a/imports/Spectral/Component/Timeline/VideoDelegate.qml b/imports/Spectral/Component/Timeline/VideoDelegate.qml new file mode 100644 index 0000000..0dcc456 --- /dev/null +++ b/imports/Spectral/Component/Timeline/VideoDelegate.qml @@ -0,0 +1,251 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 +import QtGraphicalEffects 1.0 +import QtMultimedia 5.12 +import Qt.labs.platform 1.0 as Platform + +import Spectral 0.1 +import Spectral.Setting 0.1 + +import Spectral.Component 2.0 +import Spectral.Dialog 2.0 +import Spectral.Menu.Timeline 2.0 +import Spectral.Effect 2.0 +import Spectral.Font 0.1 + +RowLayout { + readonly property bool avatarVisible: showAuthor && !sentByMe + readonly property bool sentByMe: author === currentRoom.localUser + + property bool openOnFinished: false + property bool playOnFinished: false + readonly property bool downloaded: progressInfo && progressInfo.completed + + id: root + + spacing: 4 + + z: -5 + + onDownloadedChanged: { + if (downloaded && openOnFinished) { + openSavedFile() + openOnFinished = false + } + if (downloaded && playOnFinished) { + playSavedFile() + playOnFinished = false + } + } + + Avatar { + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + Layout.alignment: Qt.AlignBottom + + visible: avatarVisible + hint: author.displayName + source: author.avatarMediaId + + Component { + id: userDetailDialog + + UserDetailDialog {} + } + + RippleEffect { + anchors.fill: parent + + circular: true + + onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open() + } + } + + Label { + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + + visible: !(sentByMe || avatarVisible) + } + + Video { + Layout.fillWidth: true + Layout.preferredHeight: width + + id: vid + + source: progressInfo.localPath + + loops: MediaPlayer.Infinite + autoPlay: true + + fillMode: VideoOutput.PreserveAspectFit + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: vid.width + height: vid.height + radius: 18 + } + } + + Label { + anchors.centerIn: parent + + visible: vid.playbackState != MediaPlayer.PlayingState + color: "white" + text: "Video" + font.pixelSize: 16 + + padding: 8 + + background: Rectangle { + radius: height / 2 + color: "black" + opacity: 0.3 + } + } + + Control { + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 8 + + horizontalPadding: 8 + verticalPadding: 4 + + contentItem: RowLayout { + Label { + text: Qt.formatTime(time, "hh:mm AP") + color: "white" + font.pixelSize: 12 + } + + Label { + text: author.displayName + color: "white" + font.pixelSize: 12 + } + } + + background: Rectangle { + radius: height / 2 + color: "black" + opacity: 0.3 + } + } + + Rectangle { + anchors.fill: parent + + visible: progressInfo.active && !downloaded + + color: "#BB000000" + + ProgressBar { + anchors.centerIn: parent + + width: parent.width * 0.8 + + from: 0 + to: progressInfo.total + value: progressInfo.progress + } + } + + RippleEffect { + anchors.fill: parent + + id: messageMouseArea + + onPrimaryClicked: downloadAndPlay() + + onSecondaryClicked: { + var contextMenu = imageDelegateContextMenu.createObject(ApplicationWindow.overlay) + contextMenu.viewSource.connect(function() { + messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() + }) + contextMenu.downloadAndOpen.connect(downloadAndOpen) + contextMenu.saveFileAs.connect(saveFileAs) + contextMenu.reply.connect(function() { + roomPanelInput.replyUser = author + roomPanelInput.replyEventID = eventId + roomPanelInput.replyContent = message + roomPanelInput.isReply = true + roomPanelInput.focus() + }) + contextMenu.redact.connect(function() { + currentRoom.redactEvent(eventId) + }) + contextMenu.popup() + } + + Component { + id: messageSourceDialog + + MessageSourceDialog {} + } + + Component { + id: openFolderDialog + + OpenFolderDialog {} + } + + Component { + id: imageDelegateContextMenu + + FileDelegateContextMenu {} + } + } + } + + function saveFileAs() { + var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) + + folderDialog.chosen.connect(function(path) { + if (!path) return + + currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) + }) + + folderDialog.open() + } + + function downloadAndOpen() + { + if (downloaded) openSavedFile() + else + { + openOnFinished = true + currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + } + } + + function downloadAndPlay() + { + if (downloaded) playSavedFile() + else + { + playOnFinished = true + currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + } + } + + function openSavedFile() + { + if (Qt.openUrlExternally(progressInfo.localPath)) return; + if (Qt.openUrlExternally(progressInfo.localDir)) return; + } + + function playSavedFile() + { + vid.stop() + vid.play() + } +} diff --git a/imports/Spectral/Component/Timeline/qmldir b/imports/Spectral/Component/Timeline/qmldir index 527b337..03ed875 100644 --- a/imports/Spectral/Component/Timeline/qmldir +++ b/imports/Spectral/Component/Timeline/qmldir @@ -4,3 +4,4 @@ StateDelegate 2.0 StateDelegate.qml SectionDelegate 2.0 SectionDelegate.qml ImageDelegate 2.0 ImageDelegate.qml FileDelegate 2.0 FileDelegate.qml +VideoDelegate 2.0 VideoDelegate.qml diff --git a/imports/Spectral/Panel/RoomPanel.qml b/imports/Spectral/Panel/RoomPanel.qml index ae31075..a142495 100644 --- a/imports/Spectral/Panel/RoomPanel.qml +++ b/imports/Spectral/Panel/RoomPanel.qml @@ -219,6 +219,24 @@ Item { } } + DelegateChoice { + roleValue: "video" + delegate: ColumnLayout { + width: messageListView.width + + SectionDelegate { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width + + visible: showSection + } + + VideoDelegate { + Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft + } + } + } + DelegateChoice { roleValue: "file" delegate: ColumnLayout { diff --git a/res.qrc b/res.qrc index 033f2d6..a07c611 100644 --- a/res.qrc +++ b/res.qrc @@ -57,5 +57,6 @@ imports/Spectral/Dialog/AccountDetailDialog.qml imports/Spectral/Dialog/OpenFileDialog.qml imports/Spectral/Dialog/OpenFolderDialog.qml + imports/Spectral/Component/Timeline/VideoDelegate.qml diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index a90067b..3ce3ed7 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -289,6 +289,8 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const { return "image"; case MessageEventType::Audio: return "audio"; + case MessageEventType::Video: + return "video"; } if (e->hasFileContent()) return "file";