Rebrand files names Spectral -> NeoChat

This commit is contained in:
Carl Schwan
2020-11-08 23:17:30 +01:00
parent c2c49c69fe
commit 122a7cdd2f
71 changed files with 787 additions and 273 deletions

View File

@@ -0,0 +1,211 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.0 as Platform
import QtMultimedia 5.12
import org.kde.kirigami 2.13 as Kirigami
import org.kde.neochat 0.1
import NeoChat.Setting 0.1
import NeoChat.Component 2.0
import NeoChat.Dialog 2.0
import NeoChat.Menu.Timeline 2.0
import NeoChat.Effect 2.0
RowLayout {
readonly property bool avatarVisible: !sentByMe && showAuthor
readonly property bool sentByMe: author.isLocalUser
id: root
spacing: 4
z: -5
Kirigami.Avatar {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
Layout.alignment: Qt.AlignBottom
visible: avatarVisible
name: author.displayName
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
}
}
Item {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
visible: !(sentByMe || avatarVisible)
}
Control {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48
padding: 12
Audio {
id: audio
source: currentRoom.urlToMxcUrl(content.url)
}
contentItem: RowLayout {
ToolButton {
icon.name: audio.playbackState == Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
onClicked: {
if (audio.playbackState == Audio.PlayingState) {
audio.pause()
} else {
audio.play()
}
}
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: display
color: MPalette.foreground
wrapMode: Label.Wrap
font.pixelSize: 18
font.weight: Font.Medium
font.capitalization: Font.AllUppercase
}
Label {
readonly property int duration: content.info.duration ?? audio.duration ?? 0
Layout.fillWidth: true
visible: duration
text: humanSize(duration)
color: MPalette.lighter
wrapMode: Label.Wrap
}
}
}
background: AutoRectangle {
readonly property int minorRadius: 8
id: bubbleBackground
color: MPalette.background
radius: 18
topLeftVisible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2)
topRightVisible: sentByMe && (bubbleShape == 3 || bubbleShape == 2)
bottomLeftVisible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2)
bottomRightVisible: sentByMe && (bubbleShape == 1 || bubbleShape == 2)
topLeftRadius: minorRadius
topRightRadius: minorRadius
bottomLeftRadius: minorRadius
bottomRightRadius: minorRadius
AutoMouseArea {
anchors.fill: parent
id: messageMouseArea
onSecondaryClicked: {
var contextMenu = fileDelegateContextMenu.createObject(root)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
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 {}
}
}
}
}
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 openSavedFile() {
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
function humanSize(duration) {
if (!duration)
return qsTr("Unknown", "Unknown duration")
if (duration < 1000)
return qsTr("An instant")
duration = Math.round(duration / 100) / 10
if (duration < 60)
return qsTr("%1 sec.").arg(duration)
duration = Math.round(duration / 6) / 10
if (duration < 60)
return qsTr("%1 min.").arg(duration)
return qsTr("%1 hrs.").arg(Math.round(duration / 6) / 10)
}
}

View File

@@ -0,0 +1,240 @@
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 Qt.labs.platform 1.0 as Platform
import org.kde.kirigami 2.13 as Kirigami
import org.kde.neochat 0.1
import NeoChat.Setting 0.1
import NeoChat.Component 2.0
import NeoChat.Dialog 2.0
import NeoChat.Menu.Timeline 2.0
import NeoChat.Effect 2.0
RowLayout {
readonly property bool avatarVisible: !sentByMe && showAuthor
readonly property bool sentByMe: author.isLocalUser
property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
id: root
spacing: 4
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile()
z: -5
Kirigami.Avatar {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
Layout.alignment: Qt.AlignBottom
visible: avatarVisible
name: author.displayName
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
}
}
Item {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
visible: !(sentByMe || avatarVisible)
}
Control {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48
padding: 12
contentItem: RowLayout {
ToolButton {
icon.name: progressInfo.completed ? "document-open" : "document-save"
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: display
color: MPalette.foreground
wrapMode: Label.Wrap
font.pixelSize: 18
font.weight: Font.Medium
font.capitalization: Font.AllUppercase
}
Label {
Layout.fillWidth: true
text: !progressInfo.completed && 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
id: messageMouseArea
onSecondaryClicked: {
var contextMenu = fileDelegateContextMenu.createObject(root)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
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 {}
}
}
}
}
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 openSavedFile()
{
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
function humanSize(bytes)
{
if (!bytes)
return qsTr("Unknown", "Unknown attachment size")
if (bytes < 4000)
return qsTr("%1 bytes").arg(bytes)
bytes = Math.round(bytes / 100) / 10
if (bytes < 2000)
return qsTr("%1 KB").arg(bytes)
bytes = Math.round(bytes / 100) / 10
if (bytes < 2000)
return qsTr("%1 MB").arg(bytes)
return qsTr("%1 GB").arg(Math.round(bytes / 100) / 10)
}
}

View File

@@ -0,0 +1,169 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.0 as Platform
import org.kde.neochat 0.1
import NeoChat.Setting 0.1
import NeoChat.Component 2.0
import NeoChat.Dialog 2.0
import NeoChat.Menu.Timeline 2.0
import NeoChat.Effect 2.0
Image {
readonly property bool isAnimated: contentType === "image/gif"
property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null)
// readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
readonly property var info: content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
id: img
source: "image://mxc/" + mediaId
sourceSize.width: info.w
sourceSize.height: info.h
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: img.width
height: img.height
radius: 18
}
}
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)
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: fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen()
onSecondaryClicked: {
var contextMenu = imageDelegateContextMenu.createObject(root)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
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 {}
}
Component {
id: fullScreenImage
FullScreenImage {}
}
}
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 openSavedFile()
{
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
}

View File

@@ -0,0 +1,125 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-LicenseIdentifier: GPL-3.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.12
import org.kde.kirigami 2.13 as Kirigami
import org.kde.neochat 0.1
import NeoChat.Setting 0.1
import NeoChat.Component 2.0
RowLayout {
default property alias innerObject : column.children
readonly property bool sentByMe: author.isLocalUser
readonly property bool darkBackground: !sentByMe
readonly property bool replyVisible: reply ?? false
readonly property bool failed: marks == EventStatus.SendingFailed
readonly property color authorColor: eventType == "notice" ? MPalette.primary : author.color
readonly property color replyAuthorColor: replyVisible ? reply.author.color : MPalette.accent
property alias mouseArea: controlContainer.children
signal saveFileAs()
signal openExternally()
id: root
spacing: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
Kirigami.Avatar {
Layout.minimumWidth: Kirigami.Units.iconSizes.medium
Layout.minimumHeight: Kirigami.Units.iconSizes.medium
Layout.maximumWidth: Kirigami.Units.iconSizes.medium
Layout.maximumHeight: Kirigami.Units.iconSizes.medium
Layout.alignment: Qt.AlignTop
visible: showAuthor
name: author.displayName
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
color: author.color
}
Item {
Layout.minimumWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: 1
visible: !showAuthor
}
QQC2.Control {
id: controlContainer
Layout.fillWidth: true
contentItem: ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
visible: showAuthor
text: author.displayName
font.bold: true
color: author.color
wrapMode: Text.Wrap
}
RowLayout {
Layout.fillWidth: true
visible: replyVisible
Rectangle {
Layout.preferredWidth: Kirigami.Units.smallSpacing
Layout.fillHeight: true
color: Kirigami.Theme.highlightColor
}
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
Layout.alignment: Qt.AlignTop
source: replyVisible && reply.author.avatarMediaId ? "image://mxc/" + reply.author.avatarMediaId : ""
name: replyVisible ? reply.author.displayName : "H"
color: replyVisible ? reply.author.color : Kirigami.Theme.highlightColor
}
ColumnLayout {
Layout.fillWidth: true
QQC2.Label {
Layout.fillWidth: true
text: replyVisible ? reply.author.displayName : ""
color: replyVisible ? reply.author.color: null
wrapMode: Text.Wrap
}
Text {
Layout.fillWidth: true
text: replyVisible ? reply.display : ""
color: Kirigami.Theme.textColor
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
textFormat: Text.RichText
}
}
}
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-LicenseIdentifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.13 as Kirigami
Flow {
visible: (reaction && reaction.length > 0) ?? false
spacing: 8
Repeater {
model: reaction
delegate: Control {
width: Math.max(implicitWidth, Kirigami.Units.largeSpacing * 3)
height: Kirigami.Units.largeSpacing * 3
horizontalPadding: 6
verticalPadding: 0
contentItem: Label {
height: Kirigami.Units.largeSpacing * 3
text: modelData.reaction + (modelData.count > 1 ? " " + modelData.count : "")
font.family: "emoji"
elide: Text.ElideRight
}
background: Rectangle {
radius: height / 2
Kirigami.Theme.colorSet: Kirigami.Theme.View
color: Kirigami.Theme.backgroundColor
MouseArea {
anchors.fill: parent
hoverEnabled: true
ToolTip {
visible: parent.containsMouse
font.family: Kirigami.Theme.defaultFont.family + ", emoji"
text: {
var text = "";
for (var i = 0; i < modelData.authors.length; i++) {
if (i === modelData.authors.length - 1 && i !== 0) {
text += i18nc("Seperate the usernames of users", " and ")
} else if (i !== 0) {
text += ", "
}
text += modelData.authors[i].displayName
}
text = i18ncp("%1 is the users who reacted and %2 the emoji that was given", "%2 reacted with %3", "%2 reacted with %3", modelData.authors.length, text, modelData.reaction)
return text
}
}
onClicked: currentRoom.toggleReaction(eventId, modelData.reaction)
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-LicenseIdentifier: GPL-3.0-or-later
*/
import QtQuick 2.12
import org.kde.kirigami 2.13 as Kirigami
Kirigami.Heading {
level: 4
text: section + " • " + Qt.formatTime(time)
font.capitalization: Font.AllUppercase
verticalAlignment: Text.AlignVCenter
topPadding: Kirigami.Units.largeSpacing * 2
bottomPadding: Kirigami.Units.smallSpacing
}

View File

@@ -0,0 +1,59 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import org.kde.kirigami 2.13 as Kirigami
import NeoChat.Component 2.0
import NeoChat.Dialog 2.0
import NeoChat.Effect 2.0
import NeoChat.Setting 0.1
RowLayout {
id: row
Item {
Layout.minimumWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: 1
}
Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
name: author.displayName
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
}
}
Label {
Layout.alignment: Qt.AlignVCenter
text: author.displayName
color: Kirigami.Theme.disabledTextColor
}
Label {
Layout.fillWidth: true
text: display
color: Kirigami.Theme.disabledTextColor
font.weight: Font.Medium
wrapMode: Label.Wrap
onLinkActivated: Qt.openUrlExternally(link)
}
}

View File

@@ -0,0 +1,14 @@
import QtQuick 2.12
import org.kde.kirigami 2.4 as Kirigami
Text {
text: "<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + display
font.family: Kirigami.Theme.defaultFont.family + ", emoji"
wrapMode: Text.WordWrap
width: parent.width
textFormat: Text.RichText
}

View File

@@ -0,0 +1,24 @@
import QtQuick 2.12
import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.4 as Kirigami
Item {
default property alias innerObject : column.children
height: column.implicitHeight
ColumnLayout {
id: column
x: horizontalPadding
width: parent.width - Kirigami.Units.largeSpacing * 2
SectionDelegate {
Layout.maximumWidth: parent.width
Layout.alignment: Qt.AlignHCenter
visible: showSection
}
}
}

View File

@@ -0,0 +1,302 @@
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 org.kde.kirigami 2.13 as Kirigami
import org.kde.neochat 0.1
import NeoChat.Setting 0.1
import NeoChat.Component 2.0
import NeoChat.Dialog 2.0
import NeoChat.Menu.Timeline 2.0
import NeoChat.Effect 2.0
import NeoChat.Font 0.1
RowLayout {
readonly property bool avatarVisible: showAuthor && !sentByMe
readonly property bool sentByMe: author.isLocalUser
property bool openOnFinished: false
property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
property bool supportStreaming: true
id: root
spacing: 4
z: -5
onDownloadedChanged: {
if (downloaded) {
vid.source = progressInfo.localPath
}
if (downloaded && openOnFinished) {
openSavedFile()
openOnFinished = false
}
if (downloaded && playOnFinished) {
playSavedFile()
playOnFinished = false
}
}
Kirigami.Avatar {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
Layout.alignment: Qt.AlignBottom
visible: avatarVisible
name: author.displayName
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
}
}
Item {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
visible: !(sentByMe || avatarVisible)
}
Video {
readonly property int maxWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48
Layout.preferredWidth: content.info.w > maxWidth ? maxWidth : content.info.w
Layout.preferredHeight: content.info.w > maxWidth ? (content.info.h / content.info.w * maxWidth) : content.info.h
id: vid
loops: MediaPlayer.Infinite
fillMode: VideoOutput.PreserveAspectFit
Component.onCompleted: {
if (downloaded) {
source = progressInfo.localPath
} else {
source = currentRoom.urlToMxcUrl(content.url)
}
}
onDurationChanged: {
if (!duration) {
supportStreaming = false;
}
}
onErrorChanged: {
if (error != MediaPlayer.NoError) {
supportStreaming = false;
}
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: vid.width
height: vid.height
radius: 18
}
}
Image {
readonly property bool isThumbnail: content.info.thumbnail_info && content.thumbnailMediaId
readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
anchors.fill: parent
visible: isThumbnail && (vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError)
source: "image://mxc/" + (isThumbnail ? content.thumbnailMediaId : "")
sourceSize.width: info.w
sourceSize.height: info.h
fillMode: Image.PreserveAspectCrop
}
Label {
anchors.centerIn: parent
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
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)
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: {
if (supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
vid.play()
}
} else {
downloadAndPlay()
}
}
onSecondaryClicked: {
var contextMenu = imageDelegateContextMenu.createObject(root)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
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()
}
}

View File

@@ -0,0 +1,11 @@
module NeoChat.Component.Timeline
TimelineContainer 2.0 TimelineContainer.qml
MessageDelegate 2.0 MessageDelegate.qml
TextDelegate 2.0 TextDelegate.qml
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
ReactionDelegate 2.0 ReactionDelegate.qml
AudioDelegate 2.0 AudioDelegate.qml