Split message content into its own module

This is laying some groundwork for the rich text chatbar.
This commit is contained in:
James Graham
2025-06-29 12:43:48 +01:00
parent a1447ebd6f
commit f6e8491bf1
117 changed files with 122 additions and 104 deletions

View File

@@ -0,0 +1,177 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtMultimedia
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show audio from a message.
*/
ColumnLayout {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @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.
* - filename - original filename of the media
*/
required property var mediaInfo
/**
* @brief FileTransferInfo for any downloading files.
*/
required property var fileTransferInfo
/**
* @brief Whether the media has been downloaded.
*/
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
onDownloadedChanged: if (downloaded) {
audio.play();
}
MediaPlayer {
id: audio
onErrorOccurred: (error, errorString) => console.warn("Audio playback error:" + error + errorString)
audioOutput: AudioOutput {}
}
states: [
State {
name: "notDownloaded"
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: Message.room.downloadFile(root.eventId)
}
},
State {
name: "downloading"
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
PropertyChanges {
target: downloadBar
visible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-stop"
onClicked: {
Message.room.cancelFileTransfer(root.eventId);
}
}
},
State {
name: "paused"
when: root.fileTransferInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: {
audio.source = root.fileTransferInfo.localPath;
MediaManager.startPlayback();
audio.play();
}
}
},
State {
name: "playing"
when: root.fileTransferInfo.completed && audio.playbackState === MediaPlayer.PlayingState
PropertyChanges {
target: playButton
icon.name: "media-playback-pause"
onClicked: audio.pause()
}
}
]
Connections {
target: MediaManager
function onPlaybackStarted() {
if (audio.playbackState === MediaPlayer.PlayingState) {
audio.pause();
}
}
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
QQC2.ToolButton {
id: playButton
}
ColumnLayout {
spacing: 0
QQC2.Label {
text: root.mediaInfo.filename
wrapMode: Text.Wrap
Layout.fillWidth: true
}
QQC2.Label {
text: Format.formatDuration(root.mediaInfo.duration)
color: Kirigami.Theme.disabledTextColor
visible: !audio.hasAudio
Layout.fillWidth: true
}
}
}
QQC2.ProgressBar {
id: downloadBar
visible: false
Layout.fillWidth: true
from: 0
to: root.mediaInfo.size
value: root.fileTransferInfo.progress
}
RowLayout {
visible: audio.hasAudio
QQC2.Slider {
Layout.fillWidth: true
from: 0
to: audio.duration
value: audio.position
onMoved: audio.setPosition(value)
}
QQC2.Label {
visible: root.Message.maxContentWidth > Kirigami.Units.gridUnit * 12
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
}
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.smallSpacing
visible: audio.hasAudio && root.Message.maxContentWidth < Kirigami.Units.gridUnit * 12
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
}
}

View File

@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
RowLayout {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property var author
/**
* @brief The timestamp of the message.
*/
required property var time
/**
* @brief The timestamp of the message as a string.
*/
required property string timeString
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
implicitHeight: Math.max(nameButton.implicitHeight, timeLabel.implicitHeight)
QQC2.Label {
id: nameButton
text: root.author.disambiguatedName
color: root.author.color
textFormat: Text.PlainText
font.weight: Font.Bold
elide: Text.ElideRight
function openUserMenu(): void {
const menu = Qt.createComponent("org.kde.neochat", "UserMenu").createObject(root, {
window: QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow,
author: root.author,
});
menu.popup(root.QQC2.Overlay.overlay);
}
// tapping to open profile
TapHandler {
onTapped: RoomManager.resolveResource(root.author.uri)
}
// right-clicking/long-press for context menu
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
cursorShape: Qt.PointingHandCursor
onPressed: nameButton.openUserMenu()
}
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
onTapped: nameButton.openUserMenu()
}
}
Item {
Layout.fillWidth: true
}
QQC2.Label {
id: timeLabel
text: root.timeString
horizontalAlignment: Text.AlignRight
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: timeHoverHandler.hovered
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: timeHoverHandler
}
}
}

View File

@@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
DelegateChooser {
id: root
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
signal removeLinkPreview(int index)
/**
* @brief Request more events in the thread be loaded.
*/
signal fetchMoreEvents()
role: "componentType"
DelegateChoice {
roleValue: MessageComponentType.Author
delegate: AuthorComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
onSelectedTextChanged: root.selectedTextChanged(selectedText)
onHoveredLinkChanged: root.hoveredLinkChanged(hoveredLink)
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: ImageComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: VideoComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Code
delegate: CodeComponent {
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Quote
delegate: QuoteComponent {
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: AudioComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: FileComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Itinerary
delegate: ItineraryComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Pdf
delegate: PdfPreviewComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Poll
delegate: PollComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Location
delegate: LocationComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.LiveLocation
delegate: LiveLocationComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Encrypted
delegate: EncryptedComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Reply
delegate: ReplyComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Reaction
delegate: ReactionComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreview
delegate: LinkPreviewComponent {
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreviewLoad
delegate: LinkPreviewLoadComponent {
type: LinkPreviewLoadComponent.LinkPreview
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.ChatBar
delegate: ChatBarComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.ReplyButton
delegate: ReplyButtonComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.FetchButton
delegate: FetchButtonComponent {
onFetchMoreEvents: root.fetchMoreEvents()
}
}
DelegateChoice {
roleValue: MessageComponentType.Verification
delegate: MimeComponent {
mimeIconSource: "security-high"
label: i18n("%1 started a user verification", model.author.htmlSafeDisplayName)
}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Separator
delegate: Kirigami.Separator {
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}
}
}

View File

@@ -0,0 +1,107 @@
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause
qt_add_library(MessageContent STATIC)
ecm_add_qml_module(MessageContent GENERATE_PLUGIN_SOURCE
URI org.kde.neochat.messagecontent
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/messagecontent
QML_FILES
BaseMessageComponentChooser.qml
MessageComponentChooser.qml
ReplyMessageComponentChooser.qml
AuthorComponent.qml
AudioComponent.qml
ChatBarComponent.qml
CodeComponent.qml
EncryptedComponent.qml
FetchButtonComponent.qml
FileComponent.qml
ImageComponent.qml
ItineraryComponent.qml
ItineraryReservationComponent.qml
JourneySectionStopDelegateLineSegment.qml
TransportIcon.qml
FoodReservationComponent.qml
TrainReservationComponent.qml
FlightReservationComponent.qml
HotelReservationComponent.qml
LinkPreviewComponent.qml
LinkPreviewLoadComponent.qml
LiveLocationComponent.qml
LoadComponent.qml
LocationComponent.qml
MimeComponent.qml
PdfPreviewComponent.qml
PollComponent.qml
QuoteComponent.qml
ReactionComponent.qml
ReplyAuthorComponent.qml
ReplyButtonComponent.qml
ReplyComponent.qml
StateComponent.qml
TextComponent.qml
ThreadBodyComponent.qml
VideoComponent.qml
SOURCES
contentprovider.cpp
mediasizehelper.cpp
pollhandler.cpp
models/itinerarymodel.cpp
models/linemodel.cpp
models/messagecontentmodel.cpp
models/pollanswermodel.cpp
models/reactionmodel.cpp
models/threadmodel.cpp
RESOURCES
images/bike.svg
images/bus.svg
images/cablecar.svg
images/car.svg
images/coach.svg
images/couchettecar.svg
images/elevator.svg
images/escalator.svg
images/ferry.svg
images/flight.svg
images/foodestablishment.svg
images/funicular.svg
images/longdistancetrain.svg
images/rapidtransit.svg
images/seat.svg
images/shuttle.svg
images/sleepingcar.svg
images/stairs.svg
images/subway.svg
images/taxi.svg
images/train.svg
images/tramway.svg
images/transfer.svg
images/wait.svg
images/walk.svg
DEPENDENCIES
QtQuick
)
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
ecm_qt_declare_logging_category(MessageContent
HEADER "messagemodel_logging.h"
IDENTIFIER "Message"
CATEGORY_NAME "org.kde.neochat.messagemodel"
DESCRIPTION "Neochat: messagemodel"
DEFAULT_SEVERITY Info
EXPORT NEOCHAT
)
target_include_directories(MessageContent PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
target_link_libraries(MessageContent PRIVATE
Qt::Core
Qt::Quick
Qt::QuickControls2
KF6::Kirigami
LibNeoChat
)
if(NOT ANDROID)
target_link_libraries(MessageContent PUBLIC KF6::SyntaxHighlighting)
endif()

View File

@@ -0,0 +1,281 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtCore
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.neochat.chatbar
/**
* @brief A component to show a chat bar in a message bubble.
*/
QQC2.Control {
id: root
/**
* @brief The ChatBarCache to use.
*/
required property ChatBarCache chatBarCache
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
readonly property bool isBusy: root.Message.room && root.Message.room.hasFileUploading
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
contentItem: ColumnLayout {
Loader {
id: paneLoader
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Layout.preferredHeight: active ? item.implicitHeight : 0
active: visible
visible: root.chatBarCache.replyId.length > 0 || root.chatBarCache.attachmentPath.length > 0
sourceComponent: root.chatBarCache.replyId.length > 0 ? replyPane : attachmentPane
}
RowLayout {
Layout.fillWidth: true
spacing: 0
QQC2.ScrollView {
id: chatBarScrollView
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
QQC2.TextArea {
id: textArea
// Work around for BUG: 503846
// Seems to crash when we try to access textArea's text here. Even though we add a slight delay it's still instantaneous in the UI.
Component.onCompleted: Qt.callLater(() => _private.updateText())
Layout.fillWidth: true
color: Kirigami.Theme.textColor
verticalAlignment: TextEdit.AlignVCenter
wrapMode: TextEdit.Wrap
onTextChanged: {
root.chatBarCache.text = text;
}
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
textArea.insert(cursorPosition, "\n");
} else {
_private.post();
}
}
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
textArea.insert(cursorPosition, "\n");
} else {
_private.post();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex();
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex();
}
}
CompletionMenu {
id: completionMenu
width: Math.max(350, root.width - 1)
height: implicitHeight
y: -height - 5
z: 10
connection: root.Message.room.connection
chatDocumentHandler: documentHandler
margins: 0
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
// opt-out of whatever spell checker a styled TextArea might come with
Kirigami.SpellCheck.enabled: false
ChatDocumentHandler {
id: documentHandler
document: textArea.textDocument
cursorPosition: textArea.cursorPosition
selectionStart: textArea.selectionStart
selectionEnd: textArea.selectionEnd
room: root.Message.room // We don't care about saving for edits so this is OK.
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
}
TextMetrics {
id: textMetrics
text: textArea.text
}
Component {
id: openFileDialog
OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
Component {
id: attachDialog
AttachDialog {
anchors.centerIn: parent
}
}
background: null
}
}
PieProgressBar {
visible: root.isBusy
progress: root.Message.room.fileUploadingProgress
}
QQC2.ToolButton {
visible: !root.isBusy
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Attach an image or file")
icon.name: "mail-attachment"
onTriggered: {
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
dialog.open();
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
icon.name: "document-send"
onTriggered: {
_private.post();
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel")
icon.name: "dialog-close"
onTriggered: {
root.chatBarCache.clearRelations();
}
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
border {
width: 1
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
}
}
Component {
id: replyPane
Item {
implicitWidth: replyComponent.implicitWidth
implicitHeight: replyComponent.implicitHeight
ReplyComponent {
id: replyComponent
replyEventId: root.chatBarCache.replyId
replyAuthor: root.chatBarCache.relationAuthor
replyContentModel: ContentProvider.contentModelForEvent(root.Message.room, root.chatBarCache.replyId, true)
Message.maxContentWidth: paneLoader.item.width
}
QQC2.Button {
id: cancelButton
anchors.top: parent.top
anchors.right: parent.right
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: {
root.chatBarCache.replyId = "";
root.chatBarCache.attachmentPath = "";
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: root.chatBarCache.attachmentPath
onAttachmentCancelled: {
root.chatBarCache.attachmentPath = "";
root.forceActiveFocus();
}
}
}
QtObject {
id: _private
function updateText() {
// This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough.
documentHandler.document;
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
textArea.text = chatBarCache.relationMessage;
documentHandler.updateMentions(textArea.textDocument, chatBarCache.editId);
textArea.forceActiveFocus();
textArea.cursorPosition = textArea.text.length;
}
}
function post() {
root.chatBarCache.postMessage();
textArea.clear();
root.chatBarCache.clearRelations();
}
}
}

View File

@@ -0,0 +1,194 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.syntaxhighlighting
import org.kde.neochat
QQC2.Control {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property NeochatRoomMember author
/**
* @brief The timestamp of the message.
*/
required property var time
/**
* @brief The display text of the message.
*/
required property string display
/**
* @brief The attributes of the component.
*/
required property var componentAttributes
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: Message.maxContentWidth
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
contentItem: QQC2.ScrollView {
id: codeScrollView
contentWidth: root.Message.maxContentWidth
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
QQC2.TextArea {
id: codeText
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
text: root.display
readOnly: true
textFormat: TextEdit.PlainText
wrapMode: TextEdit.Wrap
color: Kirigami.Theme.textColor
font.family: "monospace"
Kirigami.SpellCheck.enabled: false
onWidthChanged: lineModel.resetModel()
onHeightChanged: lineModel.resetModel()
onSelectedTextChanged: root.selectedTextChanged(selectedText)
SyntaxHighlighter {
property string definitionName: Repository.definitionForName(root.componentAttributes.class).name
textEdit: definitionName == "None" ? null : codeText
definition: definitionName
}
ColumnLayout {
id: lineNumberColumn
anchors {
top: codeText.top
topMargin: codeText.topPadding
left: codeText.left
leftMargin: Kirigami.Units.smallSpacing
}
spacing: 0
Repeater {
id: repeater
model: LineModel {
id: lineModel
document: codeText.textDocument
}
delegate: QQC2.Label {
id: label
required property int index
required property int docLineHeight
Layout.fillWidth: true
Layout.preferredHeight: docLineHeight
horizontalAlignment: Text.AlignRight
text: index + 1
color: Kirigami.Theme.disabledTextColor
font.family: "monospace"
}
}
}
TapHandler {
enabled: root.time.toString() !== "Invalid Date"
acceptedButtons: Qt.LeftButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
onTapped: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class)
}
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
background: null
}
}
Kirigami.Separator {
anchors {
top: root.top
bottom: root.bottom
left: root.left
leftMargin: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing
}
}
RowLayout {
anchors {
top: parent.top
topMargin: Kirigami.Units.smallSpacing
right: parent.right
rightMargin: (codeScrollView.QQC2.ScrollBar.vertical.visible ? codeScrollView.QQC2.ScrollBar.vertical.width : 0) + Kirigami.Units.smallSpacing
}
visible: root.hovered
spacing: Kirigami.Units.mediumSpacing
QQC2.Button {
icon.name: "edit-copy"
text: i18n("Copy to clipboard")
display: QQC2.AbstractButton.IconOnly
onClicked: Clipboard.saveText(root.display);
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Button {
visible: root.time.toString() !== "Invalid Date"
icon.name: "view-fullscreen"
text: i18nc("@action:button", "Maximize")
display: QQC2.AbstractButton.IconOnly
onClicked: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class);
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
border {
width: 1
color: Kirigami.Theme.highlightColor
}
}
}

View File

@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component for an encrypted message that can't be decrypted.
*/
TextEdit {
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
color: Kirigami.Theme.disabledTextColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
}

View File

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
import org.kde.neochat.chatbar
/**
* @brief A component to show a reply button for threads in a message bubble.
*/
Delegates.RoundedItemDelegate {
id: root
/**
* @brief Request more events in the thread be loaded.
*/
signal fetchMoreEvents()
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
leftInset: 0
rightInset: 0
highlighted: true
icon.name: "arrow-up"
icon.width: Kirigami.Units.iconSizes.sizeForLabels
icon.height: Kirigami.Units.iconSizes.sizeForLabels
text: i18nc("@action:button", "Fetch More Events")
onClicked: {
root.fetchMoreEvents()
}
contentItem: Kirigami.Icon {
implicitWidth: root.icon.width
implicitHeight: root.icon.height
source: root.icon.name
}
}

View File

@@ -0,0 +1,200 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtCore as Core
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Dialogs as Dialogs
import Qt.labs.qmlmodels
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show a file from a message.
*/
ColumnLayout {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @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.
* - filename - original filename of the media
*/
required property var mediaInfo
/**
* @brief FileTransferInfo for any downloading files.
*/
required property var fileTransferInfo
/**
* @brief Whether the media has been downloaded.
*/
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
onDownloadedChanged: {
if (autoOpenFile) {
openSavedFile();
}
}
/**
* @brief Whether the file should be automatically opened when downloaded.
*/
property bool autoOpenFile: false
function saveFileAs() {
const dialog = fileDialog.createObject(QQC2.Overlay.overlay);
dialog.selectedFile = Message.room.fileNameToDownload(root.eventId);
dialog.open();
}
function openSavedFile() {
UrlHelper.openUrl(root.fileTransferInfo.localPath);
}
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
spacing: Kirigami.Units.largeSpacing
RowLayout {
spacing: Kirigami.Units.largeSpacing
states: [
State {
name: "downloadedInstant"
when: root.fileTransferInfo.completed && autoOpenFile
PropertyChanges {
target: openButton
icon.name: "document-open"
onClicked: openSavedFile()
}
PropertyChanges {
target: downloadButton
icon.name: "download"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
onClicked: saveFileAs()
}
},
State {
name: "downloaded"
when: root.fileTransferInfo.completed && !autoOpenFile
PropertyChanges {
target: openButton
visible: false
}
PropertyChanges {
target: downloadButton
icon.name: "document-open"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
onClicked: openSavedFile()
}
},
State {
name: "downloading"
when: root.fileTransferInfo.active
PropertyChanges {
target: openButton
visible: false
}
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.fileTransferInfo.progress), Format.formatByteSize(root.fileTransferInfo.total))
}
PropertyChanges {
target: downloadButton
icon.name: "media-playback-stop"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
onClicked: Message.room.cancelFileTransfer(root.eventId)
}
}
]
Kirigami.Icon {
source: root.mediaInfo.mimeIcon
fallback: "unknown"
}
ColumnLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: root.mediaInfo.filename
wrapMode: Text.Wrap
elide: Text.ElideRight
}
QQC2.Label {
id: sizeLabel
Layout.fillWidth: true
text: Format.formatByteSize(root.mediaInfo.size)
opacity: 0.7
elide: Text.ElideRight
maximumLineCount: 1
}
}
QQC2.Button {
id: openButton
icon.name: "document-open"
onClicked: {
autoOpenFile = true;
root.Message.room.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.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Button {
id: downloadButton
icon.name: "download"
onClicked: root.saveFileAs()
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Component {
id: fileDialog
Dialogs.FileDialog {
fileMode: Dialogs.FileDialog.SaveFile
currentFolder: NeoChatConfig.lastSaveDirectory.length > 0 ? NeoChatConfig.lastSaveDirectory : Core.StandardPaths.writableLocation(Core.StandardPaths.DownloadLocation)
onAccepted: {
NeoChatConfig.lastSaveDirectory = currentFolder;
NeoChatConfig.save();
if (autoOpenFile) {
UrlHelper.copyTo(root.fileTransferInfo.localPath, selectedFile);
} else {
root.Message.room.download(root.eventId, selectedFile);
}
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
/**
* @brief A component for a flight itinerary reservation.
*/
ItineraryReservationComponent {
id: root
/**
* @brief The name of the reservation.
*/
required property string name
/**
* @brief The boarding time of the flight.
*
* Includes date.
*/
required property string startTime
/**
* @brief The departure airport of the flight.
*/
required property string departureLocation
/**
* @brief The address of the departure airport of the flight.
*/
required property string departureAddress
/**
* @brief The arrival airport of the flight.
*/
required property string arrivalLocation
/**
* @brief The address of the arrival airport of the flight.
*/
required property string arrivalAddress
headerItem: RowLayout {
TransportIcon {
source: "qrc:/qt/qml/org/kde/neochat/timeline/images/flight.svg"
isMask: true
size: Kirigami.Units.iconSizes.smallMedium
}
QQC2.Label {
Layout.fillWidth: true
text: root.name
elide: Text.ElideRight
Accessible.ignored: true
}
QQC2.Label {
text: root.startTime
}
}
contentItem: ColumnLayout {
id: topLayout
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
text: i18nc("flight departure, %1 is airport, %2 is time", "Departure from %1",
root.departureLocation)
color: Kirigami.Theme.textColor
wrapMode: Text.WordWrap
}
QQC2.Label {
Layout.fillWidth: true
visible: text !== ""
text: root.departureAddress
}
Kirigami.Separator {
Layout.fillWidth: true
}
QQC2.Label {
Layout.fillWidth: true
text: i18nc("flight arrival, %1 is airport, %2 is time", "Arrival at %1",
root.arrivalLocation)
color: Kirigami.Theme.textColor
wrapMode: Text.WordWrap
}
QQC2.Label {
Layout.fillWidth: true
visible: text !== ""
text: root.arrivalAddress
}
}
}

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
/**
* @brief A component for a food itinerary reservation.
*/
ItineraryReservationComponent {
id: root
/**
* @brief The name of the reservation.
*/
required property string name
/**
* @brief The start time of the reservation.
*
* Includes date.
*/
required property string startTime
/**
* @brief The address of the hotel.
*/
required property string address
headerItem: RowLayout {
TransportIcon {
source: "qrc:/qt/qml/org/kde/neochat/timeline/images/foodestablishment.svg"
isMask: true
size: Kirigami.Units.iconSizes.smallMedium
}
QQC2.Label {
Layout.fillWidth: true
text: root.name
elide: Text.ElideRight
Accessible.ignored: true
}
QQC2.Label {
text: root.startTime
}
}
contentItem: QQC2.Label {
visible: text !== ""
text: root.address
wrapMode: Text.Wrap
}
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
/**
* @brief A component for a hotel itinerary reservation.
*/
ItineraryReservationComponent {
id: root
/**
* @brief The name of the reservation.
*/
required property string name
/**
* @brief The check-in time at the hotel.
*
* Includes date.
*/
required property string startTime
/**
* @brief The check-out time at the hotel.
*
* Includes date.
*/
required property string endTime
/**
* @brief The address of the hotel.
*/
required property string address
headerItem: RowLayout {
TransportIcon {
source: "go-home-symbolic"
isMask: true
size: Kirigami.Units.iconSizes.smallMedium
}
QQC2.Label {
text: root.name
elide: Text.ElideRight
Accessible.ignored: true
}
}
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
visible: text !== ""
text: root.address
wrapMode: Text.WordWrap
}
QQC2.Label {
text: i18n("Check-in time: %1", root.startTime)
color: Kirigami.Theme.textColor
}
QQC2.Label {
text: i18n("Check-out time: %1", root.endTime)
color: Kirigami.Theme.textColor
}
}
}

View File

@@ -0,0 +1,204 @@
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show the image from a message.
*/
Item {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The display text of the message.
*/
required property string display
/**
* @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.
* - isSticker - Whether the image is a sticker or not
*/
required property var mediaInfo
/**
* @brief FileTransferInfo for any downloading files.
*/
required property var fileTransferInfo
/**
* The maximum height of the image. Can be left undefined most of the times. Passed to MediaSizeHelper::contentMaxHeight.
*/
property var contentMaxHeight: undefined
implicitWidth: mediaSizeHelper.currentSize.width
implicitHeight: mediaSizeHelper.currentSize.height
QQC2.Button {
anchors.right: parent.right
anchors.top: parent.top
visible: !_private.hideImage
icon.name: "view-hidden"
text: i18nc("@action:button", "Hide Image")
display: QQC2.Button.IconOnly
z: 10
onClicked: {
_private.hideImage = true;
Controller.markImageHidden(root.eventId)
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Loader {
id: imageLoader
anchors.fill: parent
active: !root.mediaInfo.animated && !_private.hideImage
sourceComponent: Image {
source: root.mediaInfo.source
sourceSize.width: mediaSizeHelper.currentSize.width * Screen.devicePixelRatio
sourceSize.height: mediaSizeHelper.currentSize.height * Screen.devicePixelRatio
fillMode: Image.PreserveAspectFit
autoTransform: true
}
}
Loader {
id: animatedImageLoader
anchors.fill: parent
active: (root?.mediaInfo.animated ?? false) && !_private.hideImage
sourceComponent: AnimatedImage {
source: root.mediaInfo.source
fillMode: Image.PreserveAspectFit
autoTransform: true
paused: !applicationWindow().active
}
}
Image {
anchors.fill: parent
source: visible ? (root?.mediaInfo.tempInfo?.source ?? "") : ""
visible: _private.imageItem && _private.imageItem.status !== Image.Ready && !_private.hideImage
}
QQC2.ToolTip.text: root.display
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
}
Rectangle {
anchors.fill: parent
visible: _private.imageItem.status !== Image.Ready || _private.hideImage
color: "#BB000000"
QQC2.ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
visible: !_private.hideImage
from: 0
to: 1.0
value: _private.imageItem.progress
}
}
QQC2.Button {
anchors.centerIn: parent
text: i18nc("@action:button", "Show Image")
visible: _private.hideImage
onClicked: {
_private.hideImage = false;
Controller.markImageShown(root.eventId);
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
onTapped: {
root.QQC2.ToolTip.hide();
if (root.mediaInfo.animated) {
_private.imageItem.paused = true;
}
root.Message.timeline.interactive = false;
if (!root.mediaInfo.isSticker) {
// We need to make sure the index is that of the MediaMessageFilterModel.
if (root.Message.timeline.model instanceof MessageFilterModel) {
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
} else {
RoomManager.maximizeMedia(root.Message.index);
}
}
}
}
function downloadAndOpen() {
if (_private.downloaded) {
openSavedFile();
} else {
openOnFinished = true;
Message.room.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + Message.room.fileNameToDownload(root.eventId));
}
}
function openSavedFile() {
if (UrlHelper.openUrl(root.fileTransferInfo.localPath))
return;
if (UrlHelper.openUrl(root.fileTransferInfo.localDir))
return;
}
MediaSizeHelper {
id: mediaSizeHelper
contentMaxWidth: root.Message.maxContentWidth
contentMaxHeight: root.contentMaxHeight ?? -1
mediaWidth: root?.mediaInfo.isSticker ? 256 : (root?.mediaInfo.width ?? 0)
mediaHeight: root?.mediaInfo.isSticker ? 256 : (root?.mediaInfo.height ?? 0)
}
QtObject {
id: _private
readonly property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item
// The space available for the component after taking away the border
readonly property real downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
property bool hideImage: NeoChatConfig.hideImages && !Controller.isImageShown(root.eventId)
}
}

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show a preview of a file that can integrate with KDE itinerary.
*/
ColumnLayout {
id: root
/**
* @brief A model with the itinerary preview of the file.
*/
required property var itineraryModel
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
spacing: Kirigami.Units.largeSpacing
Repeater {
id: itinerary
model: root.itineraryModel
delegate: DelegateChooser {
role: "type"
DelegateChoice {
roleValue: "TrainReservation"
delegate: TrainReservationComponent {}
}
DelegateChoice {
roleValue: "LodgingReservation"
delegate: HotelReservationComponent {}
}
DelegateChoice {
roleValue: "FoodEstablishmentReservation"
delegate: FoodReservationComponent {}
}
DelegateChoice {
roleValue: "FlightReservation"
delegate: FlightReservationComponent {}
}
}
}
QQC2.Button {
icon.name: "map-globe"
text: i18nc("@action", "Send to KDE Itinerary")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: itineraryModel.sendToItinerary()
visible: itinerary.count > 0
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
/**
* @brief A base component for an itinerary reservation.
*/
FormCard.FormCard {
id: root
/**
* @brief An item with the header content.
*/
property alias headerItem: headerDelegate.contentItem
/**
* @brief An item with the main body content.
*/
property alias contentItem: content.contentItem
Layout.fillWidth: true
implicitWidth: Math.max(headerDelegate.implicitWidth, content.implicitWidth)
Component.onCompleted: children[0].radius = Kirigami.Units.cornerRadius
FormCard.AbstractFormDelegate {
id: headerDelegate
Layout.fillWidth: true
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.colorSet: Kirigami.Theme.Header
Kirigami.Theme.inherit: false
}
}
FormCard.AbstractFormDelegate {
id: content
Layout.fillWidth: true
background: null
}
}

View File

@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
/**
* @brief Line segment drawn on the left in the journey section view.
*
* Can be used in a fully drawn form, or partially drawn as a progress
* overlay over the full variant.
*/
Item {
id: root
/**
* @brief Whether the segment is representing the start of the journey.
*
* I.e. this will show a segment rounded at the top with a stop marker.
*/
property bool isDeparture: false
/**
* @brief Whether the segment is representing the end of the journey.
*
* I.e. this will show a segment rounded at the bottom with a stop marker.
*/
property bool isArrival: false
/**
* @brief The color of the segment.
*/
property color lineColor: Kirigami.Theme.textColor
/**
* @brief The width of the segment.
*/
property int lineWidth: Kirigami.Units.smallSpacing *4
implicitWidth: root.lineWidth * 2
clip: true
Kirigami.ShadowedRectangle {
id: line
x: root.lineWidth / 2
y: isDeparture? parent.height-height:0
width: root.lineWidth
color: root.lineColor
corners {
topRightRadius: isDeparture ? Math.round(width / 2) : 0
topLeftRadius: isDeparture ? Math.round(width / 2) : 0
bottomRightRadius: isArrival ? Math.round(width / 2) : 0
bottomLeftRadius: isArrival ? Math.round(width / 2) : 0
}
height:
if (isArrival) {
Math.round(parent.height / 2) + root.lineWidth / 2
} else if (isDeparture) {
Math.round(parent.height / 2) + root.lineWidth / 2
} else {
parent.height
}
}
Rectangle {
id: stopDot
x: line.x + (line.width - width) / 2
y: parent.height / 2 - width / 2
radius: width / 2
width: root.lineWidth * 0.6
height: width
color: Kirigami.Theme.backgroundColor
visible: root.isArrival || root.isDeparture
}
}

View File

@@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
// SPDX-FileCopyrightText: 2023-2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show a link preview from a message.
*/
QQC2.Control {
id: root
/**
* @brief The index of the delegate in the model.
*/
required property int index
/**
* @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.
*/
required property LinkPreviewer linkPreviewer
/**
* @brief Standard height for the link preview.
*
* When the content of the link preview is larger than this it will be
* elided/hidden until maximized.
*/
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.largeSpacing * 2
property bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
/**
* @brief Request for this delegate to be removed.
*/
signal remove(int index)
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
Layout.minimumHeight: root.defaultHeight
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: RowLayout {
id: contentRow
spacing: Kirigami.Units.smallSpacing
Rectangle {
id: separator
Layout.fillHeight: true
width: Kirigami.Units.smallSpacing
color: Kirigami.Theme.highlightColor
radius: Kirigami.Units.cornerRadius
}
Image {
id: previewImage
Layout.preferredWidth: root.defaultHeight
Layout.preferredHeight: root.defaultHeight
Layout.fillWidth: true
Layout.fillHeight: true
visible: root.linkPreviewer.imageSource.toString().length > 0
source: root.linkPreviewer.imageSource
fillMode: Image.PreserveAspectFit
}
ColumnLayout {
id: column
implicitWidth: Math.max(linkPreviewTitle.implicitWidth, linkPreviewDescription.implicitWidth)
spacing: Kirigami.Units.smallSpacing
visible: root.linkPreviewer.title.length > 0 || root.linkPreviewer.description.length > 0
Kirigami.Heading {
id: linkPreviewTitle
Layout.fillWidth: true
level: 3
wrapMode: Text.Wrap
textFormat: Text.RichText
text: "<style>
a {
text-decoration: none;
}
</style>
<a href=\"" + root.linkPreviewer.url + "\">" + (maximizeButton.checked ? root.linkPreviewer.title : titleTextMetrics.elidedText).replace("&ndash;", "—") + "</a>"
TextMetrics {
id: titleTextMetrics
text: root.linkPreviewer.title
font: linkPreviewTitle.font
elide: Text.ElideRight
elideWidth: linkPreviewTitle.width
}
}
QQC2.Label {
id: linkPreviewDescription
Layout.fillWidth: true
Layout.maximumHeight: maximizeButton.checked ? -1 : root.defaultHeight - linkPreviewTitle.height - column.spacing
visible: linkPreviewTitle.height + column.spacing + font.pointSize <= root.defaultHeight || maximizeButton.checked
text: linkPreviewer.description
wrapMode: Text.Wrap
elide: Text.ElideRight
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: RoomManager.resolveResource(root.linkPreviewer.url, "join")
}
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
QQC2.Button {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
visible: root.hovered
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: root.remove(root.index)
QQC2.ToolTip {
text: closeButton.text
visible: closeButton.hovered
delay: Kirigami.Units.toolTipDelay
}
}
QQC2.Button {
id: maximizeButton
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: root.hovered && (root.truncated || checked)
checkable: true
text: checked ? i18n("Shrink preview") : i18n("Expand preview")
icon.name: checked ? "go-up" : "go-down"
display: QQC2.AbstractButton.IconOnly
QQC2.ToolTip {
text: maximizeButton.text
visible: maximizeButton.hovered
delay: Kirigami.Units.toolTipDelay
}
}
}

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show a link preview loading from a message.
*/
QQC2.Control {
id: root
/**
* @brief The index of the delegate in the model.
*/
required property int index
required property int type
/**
* @brief Standard height for the link preview.
*
* When the content of the link preview is larger than this it will be
* elided/hidden until maximized.
*/
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
/**
* @brief Request for this delegate to be removed.
*/
signal remove(int index)
enum Type {
Reply,
LinkPreview
}
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
contentItem : RowLayout {
spacing: Kirigami.Units.smallSpacing
Rectangle {
Layout.fillHeight: true
width: Kirigami.Units.smallSpacing
color: Kirigami.Theme.highlightColor
}
QQC2.BusyIndicator {}
Kirigami.Heading {
Layout.fillWidth: true
Layout.minimumHeight: root.defaultHeight
verticalAlignment: Text.AlignVCenter
level: 2
text: {
switch (root.type) {
case LinkPreviewLoadComponent.Reply:
return i18n("Loading reply");
case LinkPreviewLoadComponent.LinkPreview:
return i18n("Loading URL preview");
}
}
}
}
QQC2.Button {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
visible: root.hovered && root.type === LinkPreviewLoadComponent.LinkPreview
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: root.remove(root.index)
QQC2.ToolTip {
text: closeButton.text
visible: closeButton.hovered
delay: Kirigami.Units.toolTipDelay
}
}
}

View File

@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtLocation
import QtPositioning
import org.kde.neochat
/**
* @brief A component to show a live location from a message.
*/
ColumnLayout {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The display text of the message.
*/
required property string display
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
LiveLocationsModel {
id: liveLocationModel
eventId: root.eventId
room: Message.room
}
MapView {
id: mapView
Layout.fillWidth: true
Layout.preferredWidth: root.Message.maxContentWidth
Layout.preferredHeight: root.Message.maxContentWidth / 16 * 9
map.center: QtPositioning.coordinate(liveLocationModel.boundingBox.y, liveLocationModel.boundingBox.x)
map.zoomLevel: 15
map.plugin: OsmLocationPlugin.plugin
MapItemView {
model: liveLocationModel
delegate: LocationMapItem {}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: {
let map = fullScreenMap.createObject(parent, {
liveLocationModel: liveLocationModel
});
map.open();
}
}
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
onLongPressed: openMessageContext("")
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext("")
}
Connections {
target: mapView.map
function onCopyrightLinkActivated() {
Qt.openUrlExternally(link);
}
}
}
Component {
id: fullScreenMap
FullScreenMap {}
}
TextComponent {
display: root.display
visible: root.display !== ""
}
}

View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show that part of a message is loading.
*/
RowLayout {
id: root
required property string display
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
spacing: Kirigami.Units.smallSpacing
QQC2.BusyIndicator {}
Kirigami.Heading {
id: loadingText
Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter
level: 2
text: root.display.length > 0 ? root.display : i18n("Loading")
}
}

View File

@@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtLocation
import QtPositioning
import org.kde.neochat
import org.kde.kirigami as Kirigami
/**
* @brief A component to show a location from a message.
*/
ColumnLayout {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property var author
/**
* @brief The display text of the message.
*/
required property string display
/**
* @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
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
MapView {
id: mapView
Layout.fillWidth: true
Layout.preferredWidth: root.Message.maxContentWidth
Layout.preferredHeight: root.Message.maxContentWidth / 16 * 9
map.center: QtPositioning.coordinate(root.latitude, root.longitude)
map.zoomLevel: 15
map.plugin: OsmLocationPlugin.plugin
readonly property LocationMapItem locationMapItem: LocationMapItem {
latitude: root.latitude
longitude: root.longitude
asset: root.asset
author: root.author
isLive: true
heading: NaN
}
Component.onCompleted: map.addMapItem(locationMapItem)
Connections {
target: mapView.map
function onCopyrightLinkActivated(link: string) {
Qt.openUrlExternally(link);
}
}
RowLayout {
anchors {
top: parent.top
right: parent.right
margins: Kirigami.Units.smallSpacing
}
spacing: Kirigami.Units.mediumSpacing
Button {
text: i18nc("@action:button Open the location in an external program", "Open Externally")
icon.name: "open-link-symbolic"
display: AbstractButton.IconOnly
onClicked: Qt.openUrlExternally("geo:" + root.latitude + "," + root.longitude)
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
}
Button {
icon.name: "view-fullscreen"
text: i18nc("@action:button", "Open Fullscreen")
display: AbstractButton.IconOnly
onClicked: {
let map = fullScreenMap.createObject(parent, {
latitude: root.latitude,
longitude: root.longitude,
asset: root.asset,
author: root.author
});
map.open();
}
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
Component {
id: fullScreenMap
FullScreenMap {}
}
TextComponent {
eventId: root.eventId
author: root.author
display: root.display
visible: root.display !== ""
}
}

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import Qt.labs.qmlmodels
import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
BaseMessageComponentChooser {
id: root
DelegateChoice {
roleValue: MessageComponentType.ThreadBody
delegate: ThreadBodyComponent {
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onHoveredLinkChanged: hoveredLink => {
root.hoveredLinkChanged(hoveredLink);
}
}
}
}

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
/**
* @brief A component to show media based upon its mime type.
*/
RowLayout {
property alias mimeIconSource: icon.source
property alias label: nameLabel.text
property string subLabel: ""
property int size: 0
property int duration: 0
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
id: icon
fallback: "unknown"
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
spacing: 0
QQC2.Label {
id: nameLabel
Layout.fillWidth: true
Layout.alignment: caption.visible ? Qt.AlignLeft | Qt.AlignBottom : Qt.AlignLeft | Qt.AlignVCenter
elide: Text.ElideRight
}
QQC2.Label {
id: caption
Layout.fillWidth: true
text: (subLabel || size || duration || '') && [
subLabel,
size && Format.formatByteSize(size),
duration > 0 && Format.formatDuration(duration),
].filter(Boolean).join(" | ")
elide: Text.ElideRight
visible: text.length > 0
opacity: 0.7
}
}
}

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import org.kde.neochat
Rectangle {
id: root
/**
* @brief FileTransferInfo for any downloading files.
*/
required property var fileTransferInfo
/**
* @brief The attributes of the component.
*/
required property var componentAttributes
Layout.preferredWidth: mediaSizeHelper.currentSize.width
Layout.preferredHeight: mediaSizeHelper.currentSize.height
color: "white"
Image {
anchors.fill: root
source: root?.fileTransferInfo.localPath ?? ""
MediaSizeHelper {
id: mediaSizeHelper
contentMaxWidth: root.Message.maxContentWidth
mediaWidth: root.componentAttributes.size.width
mediaHeight: root.componentAttributes.size.height
}
}
}

View File

@@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
/**
* @brief A component to show a poll from a message.
*/
ColumnLayout {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @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.
*/
required property var pollHandler
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
Layout.minimumWidth: Message.maxContentWidth
spacing: 0
RowLayout {
Layout.fillWidth: true
Kirigami.Icon {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: implicitWidth
source: "amarok_playcount"
}
QQC2.Label {
id: questionLabel
Layout.fillWidth: true
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
text: root.pollHandler.question
wrapMode: Text.Wrap
visible: text.length > 0
}
}
Repeater {
model: root.pollHandler.answerModel
delegate: Delegates.RoundedItemDelegate {
id: answerDelegate
required property string id
required property string answerText
required property int count
required property bool localChoice
required property bool isWinner
Layout.fillWidth: true
Layout.leftMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing
Layout.rightMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing
highlighted: false
onClicked: {
if (root.pollHandler.hasEnded) {
return;
}
root.pollHandler.sendPollAnswer(root.eventId, answerDelegate.id);
}
text: answerDelegate.answerText
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
contentItem: ColumnLayout {
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
QQC2.CheckBox {
enabled: !root.pollHandler.hasEnded
checked: answerDelegate.localChoice
onClicked: answerDelegate.clicked()
}
QQC2.Label {
Layout.fillWidth: true
text: answerDelegate.answerText
}
Kirigami.Icon {
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: implicitWidth
visible: answerDelegate.isWinner
source: "favorite-favorited"
}
QQC2.Label {
visible: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded
horizontalAlignment: Text.AlignRight
text: i18np("%1 Vote", "%1 Votes", answerDelegate.count)
}
}
QQC2.ProgressBar {
id: voteProgress
Layout.fillWidth: true
to: root.pollHandler.totalCount
value: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded ? answerDelegate.count : 0
}
}
}
}
QQC2.Label {
visible: root.pollHandler.kind == PollKind.Disclosed || root.pollHandler.hasEnded
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.totalCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
font.pointSize: questionLabel.font.pointSize * 0.8
}
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
QQC2.Control {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property NeochatRoomMember author
/**
* @brief The display text of the message.
*/
required property string display
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: Message.maxContentWidth
topPadding: 0
bottomPadding: 0
contentItem: TextEdit {
id: quoteText
Layout.fillWidth: true
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
text: root.display
readOnly: true
textFormat: TextEdit.RichText
wrapMode: TextEdit.Wrap
color: Kirigami.Theme.textColor
font.italic: true
onSelectedTextChanged: root.selectedTextChanged(selectedText)
TapHandler {
enabled: !quoteText.hoveredLink
acceptedDevices: PointerDevice.TouchScreen
acceptedButtons: Qt.LeftButton
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
}
}

View File

@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Flow {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The reaction model to get the reactions from.
*/
required property ReactionModel reactionModel
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: Message.maxContentWidth
spacing: Kirigami.Units.smallSpacing
Repeater {
id: reactionRepeater
model: root.reactionModel
delegate: QQC2.AbstractButton {
id: reactionDelegate
required property string textContent
required property string reaction
required property string toolTip
required property bool hasLocalMember
width: Math.max(contentItem.implicitWidth + leftPadding + rightPadding, height)
height: Math.round(Kirigami.Units.gridUnit * 1.5)
contentItem: QQC2.Label {
id: reactionLabel
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: reactionDelegate.textContent
background: null
wrapMode: TextEdit.NoWrap
textFormat: Text.RichText
}
padding: Kirigami.Units.smallSpacing
background: Kirigami.ShadowedRectangle {
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
color: reactionDelegate.hasLocalMember ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
radius: height / 2
shadow {
size: Kirigami.Units.smallSpacing
color: !reactionDelegate.hasLocalMember ? 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)
}
}
onClicked: {
root.Message.room.toggleReaction(root.eventId, reactionDelegate.reaction)
}
hoverEnabled: true
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: reactionDelegate.toolTip
}
}
QQC2.AbstractButton {
id: reactButton
width: Math.round(Kirigami.Units.gridUnit * 1.5)
height: Math.round(Kirigami.Units.gridUnit * 1.5)
text: i18nc("@button", "React")
contentItem: Kirigami.Icon {
source: "list-add"
}
padding: Kirigami.Units.smallSpacing
background: Rectangle {
color: Kirigami.Theme.backgroundColor
radius: height / 2
border {
width: reactButton.hovered ? 1 : 0
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
}
}
onClicked: {
var dialog = emojiDialog.createObject(reactButton);
dialog.showStickers = false;
dialog.chosen.connect(emoji => {
root.Message.room.toggleReaction(root.eventId, emoji);
if (!Kirigami.Settings.isMobile) {
root.focusChatBar();
}
});
dialog.open();
}
hoverEnabled: true
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: reactButton.text
}
Component {
id: emojiDialog
EmojiDialog {
currentRoom: root.Message.room
showQuickReaction: true
}
}
}

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
RowLayout {
id: root
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property var author
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
Layout.fillWidth: true
Layout.maximumWidth: root.maxContentWidth
implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight)
spacing: Kirigami.Units.largeSpacing
KirigamiComponents.Avatar {
id: replyAvatar
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
source: root.author.avatarUrl
name: root.author.displayName
color: root.author.color
asynchronous: true
}
QQC2.Label {
id: replyName
Layout.fillWidth: true
color: root.author.color
text: root.author.disambiguatedName
elide: Text.ElideRight
textFormat: Text.PlainText
}
}

View File

@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
import org.kde.neochat.chatbar
/**
* @brief A component to show a reply button for threads in a message bubble.
*/
Delegates.RoundedItemDelegate {
id: root
/**
* @brief The thread root ID.
*/
required property string threadRoot
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
leftInset: 0
rightInset: 0
highlighted: true
icon.name: "mail-reply-custom"
text: i18nc("@action:button", "Reply")
onClicked: {
Message.room.threadCache.replyId = "";
Message.room.threadCache.threadId = root.threadRoot;
Message.room.mainCache.clearRelations();
Message.room.editCache.clearRelations();
}
}

View File

@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
/**
* @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.
*/
RowLayout {
id: root
/**
* @brief The matrix ID of the reply event.
*/
required property var replyEventId
/**
* @brief The reply author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property var replyAuthor
/**
* @brief The model to visualise the content of the message replied to.
*/
required property var replyContentModel
Layout.fillWidth: true
spacing: Kirigami.Units.largeSpacing
Rectangle {
id: verticalBorder
Layout.fillHeight: true
implicitWidth: Kirigami.Units.smallSpacing
color: root.replyAuthor.color
radius: Kirigami.Units.cornerRadius
}
ColumnLayout {
id: contentColumn
spacing: Kirigami.Units.smallSpacing
Message.maxContentWidth: _private.availableContentWidth
Repeater {
id: contentRepeater
model: root.replyContentModel
delegate: ReplyMessageComponentChooser {
onReplyClicked: root.Message.timeline.goToEvent(root.replyEventId)
}
}
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.Message.timeline.goToEvent(root.replyEventId)
}
QtObject {
id: _private
// The space available for the component after taking away the border
readonly property real availableContentWidth: root.Message.maxContentWidth - verticalBorder.implicitWidth - root.spacing
}
}

View File

@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
DelegateChooser {
id: root
/**
* @brief The reply has been clicked.
*/
signal replyClicked()
role: "componentType"
DelegateChoice {
roleValue: MessageComponentType.Author
delegate: ReplyAuthorComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: root.replyClicked()
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: ImageComponent {
contentMaxHeight: Kirigami.Units.gridUnit * 5
}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: MimeComponent {
required property var mediaInfo
mimeIconSource: mediaInfo.mimeIcon
size: mediaInfo.size
duration: mediaInfo.duration
label: mediaInfo.filename
}
}
DelegateChoice {
roleValue: MessageComponentType.Code
delegate: CodeComponent {
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: root.replyClicked()
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Quote
delegate: QuoteComponent {
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: root.replyClicked()
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: MimeComponent {
required property string display
required property var mediaInfo
mimeIconSource: mediaInfo.mimeIcon
size: mediaInfo.size
duration: mediaInfo.duration
label: mediaInfo.filename
}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: MimeComponent {
required property string display
required property var mediaInfo
mimeIconSource: mediaInfo.mimeIcon
size: mediaInfo.size
label: mediaInfo.filename
}
}
DelegateChoice {
roleValue: MessageComponentType.Poll
delegate: PollComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Location
delegate: MimeComponent {
mimeIconSource: "mark-location"
label: display
}
}
DelegateChoice {
roleValue: MessageComponentType.LiveLocation
delegate: MimeComponent {
mimeIconSource: "mark-location"
label: display
}
}
DelegateChoice {
roleValue: MessageComponentType.Encrypted
delegate: EncryptedComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}
}
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
/**
* @brief A component for visualising a single state event
*/
RowLayout {
id: root
/**
* @brief All model roles as a map with the property names as the keys.
*/
required property var modelData
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
property var author: modelData.author
/**
* @brief The displayname for the event's sender; for name change events, the old displayname.
*/
property string authorDisplayName: modelData.authorDisplayName
/**
* @brief The display text for the state event.
*/
property string text: modelData.text
KirigamiComponents.Avatar {
id: stateAvatar
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
source: root.author?.avatarUrl ?? ""
name: root.author?.displayName ?? ""
color: root.author?.color ?? Kirigami.Theme.highlightColor
asynchronous: true
MouseArea {
anchors.fill: parent
enabled: root.author
cursorShape: Qt.PointingHandCursor
onClicked: RoomManager.resolveResource("https://matrix.to/#/" + root.author?.id ?? "")
}
}
QQC2.Label {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: `<style>a {text-decoration: none; color: ${Kirigami.Theme.textColor};}</style><a href="https://matrix.to/#/${root.author?.id ?? ""}">${root.authorDisplayName}</a> ${root.text}`
wrapMode: Text.WordWrap
textFormat: Text.RichText
onLinkActivated: link => RoomManager.resolveResource(link)
HoverHandler {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
}

View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show rich text from a message.
*/
TextEdit {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property NeochatRoomMember author
/**
* @brief The display text of the message.
*/
required property string display
/**
* @brief Whether this message is replying to another.
*/
property bool isReply: false
/**
* @brief Regex for detecting a message with a spoiler.
*/
readonly property var hasSpoiler: /data-mx-spoiler/g
/**
* @brief Whether a spoiler should be revealed.
*/
property bool spoilerRevealed: !hasSpoiler.test(display)
/**
* @brief The color of spoiler blocks, to be theme-agnostic.
*/
property color spoilerBlockColor: Kirigami.ColorUtils.tintWithAlpha("#232629", Kirigami.Theme.textColor, 0.15)
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: Message.maxContentWidth
ListView.onReused: Qt.binding(() => !hasSpoiler.test(display))
persistentSelection: true
text: "<style>
table {
width:100%;
border-width: 1px;
border-collapse: collapse;
border-style: solid;
}
code {
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
}
table th,
table td {
border: 1px solid black;
padding: 3px;
}
blockquote {
margin: 0;
}
blockquote table {
width: 100%;
border-width: 0;
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
}
blockquote td {
width: 100%;
padding: " + Kirigami.Units.largeSpacing + ";
}
pre {
white-space: pre-wrap
}
a{
color: " + Kirigami.Theme.linkColor + ";
text-decoration: none;
}
[data-mx-spoiler] a {
background: " + root.spoilerBlockColor + ";
}
[data-mx-spoiler] {
background: " + root.spoilerBlockColor + ";
}
" + (!spoilerRevealed ? "
[data-mx-spoiler] a {
color: transparent;
}
[data-mx-spoiler] {
color: transparent;
}
" : "
[data-mx-spoiler] a {
color: white;
}
[data-mx-spoiler] {
color: white;
}
") + "
</style>" + display
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font {
pointSize: !root.isReply && QmlUtils.isEmoji(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
family: QmlUtils.isEmoji(display) ? 'emoji' : Kirigami.Theme.defaultFont.family
}
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: link => {
spoilerRevealed = true;
RoomManager.resolveResource(link, "join");
}
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
applicationWindow().hoverLinkIndicator.text = hoveredLink;
} else {
applicationWindow().hoverLinkIndicator.text = "";
}
HoverHandler {
cursorShape: (root.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
}
TapHandler {
enabled: !root.hoveredLink && !spoilerRevealed
onTapped: spoilerRevealed = true
}
TapHandler {
enabled: !root.hoveredLink
acceptedButtons: Qt.LeftButton
acceptedDevices: PointerDevice.TouchScreen
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
gesturePolicy: TapHandler.WithinBounds
onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
}

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to visualize a ThreadModel.
*
* @sa ThreadModel
*/
ColumnLayout {
id: root
/**
* @brief The Matrix ID of the root message in the thread, if any.
*/
required property string threadRoot
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: Message.maxContentWidth
spacing: Kirigami.Units.smallSpacing
Repeater {
id: threadRepeater
model: root.Message.contentModel.modelForThread(root.threadRoot);
delegate: BaseMessageComponentChooser {
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onHoveredLinkChanged: hoveredLink => {
root.hoveredLinkChanged(hoveredLink);
}
onRemoveLinkPreview: index => threadRepeater.model.closeLinkPreview(index)
onFetchMoreEvents: threadRepeater.model.fetchMoreEvents(5)
}
}
}

View File

@@ -0,0 +1,214 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
/**
* @brief A component for a train itinerary reservation component.
*/
ItineraryReservationComponent {
id: root
/**
* @brief The name of the reservation.
*/
required property string name
/**
* @brief The departure time of the train.
*/
required property string departureTime
/**
* @brief The departure station of the train.
*/
required property string departureLocation
/**
* @brief The address of the departure station of the train.
*/
required property string departureAddress
/**
* @brief The departure platform of the train.
*/
required property string departurePlatform
/**
* @brief The arrival time of the train.
*/
required property string arrivalTime
/**
* @brief The arrival station of the train.
*/
required property string arrivalLocation
/**
* @brief The address of the arrival station of the train.
*/
required property string arrivalAddress
/**
* @brief The arrival platform of the train.
*/
required property string arrivalPlatform
headerItem: RowLayout {
TransportIcon {
source: "qrc:/qt/qml/org/kde/neochat/timeline/images/train.svg"
isMask: true
size: Kirigami.Units.iconSizes.smallMedium
}
QQC2.Label {
Layout.fillWidth: true
text: root.name
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
Accessible.ignored: true
}
QQC2.Label {
text: root.departureTime
}
}
contentItem: ColumnLayout {
spacing: 0
RowLayout {
Layout.fillWidth: true
ColumnLayout{
id: lineSectionColumn
spacing: 0
JourneySectionStopDelegateLineSegment {
Layout.fillHeight: true
isDeparture: true
}
JourneySectionStopDelegateLineSegment {
visible: departureCountryLayout.visible
Layout.fillHeight: true
}
}
ColumnLayout{
Layout.bottomMargin: Kirigami.Units.largeSpacing
spacing:0
Layout.fillHeight: true
Layout.fillWidth: true
RowLayout {
Layout.fillHeight: true
Layout.fillWidth: true
QQC2.Label {
id: depTime
text: root.departureTime
}
QQC2.Label {
Layout.fillWidth: true
font.bold: true
text: root.departureLocation
elide: Text.ElideRight
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
text: {
let platform = root.departurePlatform;
if (platform) {
return i18n("Pl. %1", platform);
} else {
return "";
}
}
}
}
RowLayout{
id: departureCountryLayout
visible: departureCountryLabel.text.length > 0
Item{
Layout.minimumWidth: depTime.width
}
QQC2.Label {
id: departureCountryLabel
Layout.fillWidth: true
text: root.departureAddress
}
}
}
}
RowLayout{
Layout.fillWidth: true
JourneySectionStopDelegateLineSegment {
Layout.fillHeight: true
}
Kirigami.Separator {
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
ColumnLayout {
spacing: 0
JourneySectionStopDelegateLineSegment {
visible: arrivalCountryLayout.visible
Layout.fillHeight: true
}
JourneySectionStopDelegateLineSegment {
Layout.fillHeight: true
isArrival: true
}
}
ColumnLayout{
Layout.topMargin: Kirigami.Units.largeSpacing
spacing:0
Layout.fillHeight: true
Layout.fillWidth: true
RowLayout {
Layout.fillHeight: true
Layout.fillWidth: true
QQC2.Label {
text: root.arrivalTime
}
QQC2.Label {
Layout.fillWidth: true
font.bold: true
text: root.arrivalLocation
elide: Text.ElideRight
}
QQC2.Label {
Layout.alignment: Qt.AlignRight
text: {
let platform = root.arrivalPlatform;
if (platform) {
return i18n("Pl. %1", platform);
} else {
return "";
}
}
}
}
RowLayout {
id: arrivalCountryLayout
visible: arrivalCountryLabel.text.length > 0
Item{
Layout.minimumWidth: depTime.width
}
QQC2.Label {
id: arrivalCountryLabel
Layout.fillWidth: true
text: root.arrivalAddress
}
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
/*
SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
/** Displays a transport line or mode logo/icon.
* Mainly to hide ugly implementation details of Icon not
* handling non-square SVG assets in the way we need it here.
*/
Item {
id: root
// properties match those of Icon
property string source
property alias isMask: __icon.isMask
property alias color: __icon.color
// icon size (height for non-square ones)
property int size: Kirigami.Units.iconSizes.small
property bool __isIcon: !source.startsWith("file:")
implicitWidth: __isIcon ? root.size : Math.round(root.size * __image.implicitWidth / __image.implicitHeight)
implicitHeight: root.size
Layout.preferredWidth: root.implicitWidth
Layout.preferredHeight: root.implicitHeight
Kirigami.Icon {
id: __icon
source: root.__isIcon ? root.source : ""
visible: source !== ""
anchors.fill: parent
}
Image {
id: __image
source: root.__isIcon ? "" : root.source
visible: source !== ""
anchors.fill: parent
fillMode: Image.PreserveAspectFit
}
}

View File

@@ -0,0 +1,460 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtCore as Core
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtMultimedia
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to show a video from a message.
*/
Video {
id: root
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The display text of the message.
*/
required property string display
/**
* @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 FileTransferInfo for any downloading files.
*/
required property var fileTransferInfo
/**
* @brief Whether the media has been downloaded.
*/
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
onDownloadedChanged: {
if (downloaded) {
root.source = root.fileTransferInfo.localPath;
}
if (downloaded && playOnFinished) {
playSavedFile();
playOnFinished = false;
}
}
/**
* @brief Whether the video should be played when downloaded.
*/
property bool playOnFinished: false
Layout.preferredWidth: mediaSizeHelper.currentSize.width
Layout.preferredHeight: mediaSizeHelper.currentSize.height
fillMode: VideoOutput.PreserveAspectFit
Component.onDestruction: root.stop()
Component.onCompleted: {
if (NeoChatConfig.hideImages && !Controller.isImageShown(root.eventId)) {
root.state = "hidden";
}
}
states: [
State {
name: "notDownloaded"
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
PropertyChanges {
target: videoLabel
visible: true
}
PropertyChanges {
target: mediaThumbnail
visible: true
}
},
State {
name: "downloading"
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
PropertyChanges {
target: downloadBar
visible: true
}
},
State {
name: "paused"
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.PausedState && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
PropertyChanges {
target: videoControls
stateVisible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: {
MediaManager.startPlayback();
root.play();
}
}
},
State {
name: "playing"
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.PlayingState && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
PropertyChanges {
target: videoControls
stateVisible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-pause"
onClicked: root.pause()
}
},
State {
name: "stopped"
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.StoppedState && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
PropertyChanges {
target: videoControls
stateVisible: true
}
PropertyChanges {
target: mediaThumbnail
visible: true
}
PropertyChanges {
target: videoLabel
visible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: {
MediaManager.startPlayback();
root.play();
}
}
},
State {
name: "hidden"
PropertyChanges {
target: mediaThumbnail
visible: false
}
PropertyChanges {
target: videoControls
visible: false
}
PropertyChanges {
target: hidden
visible: true
}
}
]
Connections {
target: MediaManager
function onPlaybackStarted() {
if (root.playbackState === MediaPlayer.PlayingState) {
root.pause();
}
}
}
QQC2.Button {
anchors.right: parent.right
anchors.top: parent.top
visible: root.state !== "hidden"
icon.name: "view-hidden"
text: i18nc("@action:button", "Hide Image")
display: QQC2.Button.IconOnly
z: 10
onClicked: {
root.state = "hidden"
Controller.markImageHidden(root.eventId)
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Image {
id: mediaThumbnail
anchors.fill: parent
visible: false
source: visible ? root.mediaInfo.tempInfo.source : ""
fillMode: Image.PreserveAspectFit
}
QQC2.Label {
id: videoLabel
anchors.centerIn: parent
visible: false
color: "white"
text: i18n("Video")
font.pixelSize: 16
padding: 8
background: Rectangle {
radius: Kirigami.Units.smallSpacing
color: "black"
opacity: 0.3
}
}
Rectangle {
id: downloadBar
anchors.fill: parent
visible: false
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
QQC2.ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: root.fileTransferInfo.total
value: root.fileTransferInfo.progress
}
}
Rectangle {
id: hidden
anchors.fill: parent
visible: false
color: "#BB000000"
QQC2.Button {
anchors.centerIn: parent
text: i18nc("@action:button", "Show Video")
onClicked: {
root.state = "notDownloaded";
Controller.markImageShown(root.eventId);
}
}
}
QQC2.Control {
id: videoControls
property bool stateVisible: false
anchors.bottom: root.bottom
anchors.left: root.left
anchors.right: root.right
visible: stateVisible && (videoHoverHandler.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || videoControlTimer.running)
contentItem: RowLayout {
id: controlRow
QQC2.ToolButton {
id: playButton
}
QQC2.Slider {
Layout.fillWidth: true
from: 0
to: root.duration
value: root.position
onMoved: root.seek(value)
}
QQC2.Label {
text: Format.formatDuration(root.position) + "/" + Format.formatDuration(root.duration)
}
QQC2.ToolButton {
id: volumeButton
property var unmuteVolume: root.volume
icon.name: root.volume <= 0 ? "player-volume-muted" : "player-volume"
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.timeout: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: i18nc("@action:button", "Volume")
onClicked: {
if (root.volume > 0) {
root.volume = 0;
} else {
if (unmuteVolume === 0) {
root.volume = 1;
} else {
root.volume = unmuteVolume;
}
}
}
onHoveredChanged: {
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
videoControlTimer.restart();
volumePopupTimer.restart();
}
}
QQC2.Popup {
id: volumePopup
y: -height
width: volumeButton.width
visible: videoControls.stateVisible && (volumeButton.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || volumePopupTimer.running)
focus: true
padding: Kirigami.Units.smallSpacing
closePolicy: QQC2.Popup.NoAutoClose
QQC2.Slider {
id: volumeSlider
anchors.centerIn: parent
implicitHeight: Kirigami.Units.gridUnit * 7
orientation: Qt.Vertical
padding: 0
from: 0
to: 1
value: root.volume
onMoved: {
root.volume = value;
volumeButton.unmuteVolume = value;
}
onHoveredChanged: {
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
videoControlTimer.restart();
volumePopupTimer.restart();
}
}
}
Timer {
id: volumePopupTimer
interval: 500
}
HoverHandler {
id: volumePopupHoverHandler
onHoveredChanged: {
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
videoControlTimer.restart();
volumePopupTimer.restart();
}
}
}
background: Kirigami.ShadowedRectangle {
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.8
property color borderColor: Kirigami.Theme.textColor
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
border.width: 1
shadow.xOffset: 0
shadow.yOffset: 4
shadow.color: Qt.rgba(0, 0, 0, 0.3)
shadow.size: 8
}
}
}
QQC2.ToolButton {
id: maximizeButton
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18n("Maximize")
icon.name: "view-fullscreen"
onTriggered: {
root.Message.timeline.interactive = false;
root.pause();
// We need to make sure the index is that of the MediaMessageFilterModel.
if (root.Message.timeline.model instanceof MessageFilterModel) {
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
} else {
RoomManager.maximizeMedia(root.Message.index);
}
}
}
}
}
background: Kirigami.ShadowedRectangle {
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.8
property color borderColor: Kirigami.Theme.textColor
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
border.width: 1
shadow.xOffset: 0
shadow.yOffset: 4
shadow.color: Qt.rgba(0, 0, 0, 0.3)
shadow.size: 8
}
}
Timer {
id: videoControlTimer
interval: 1000
}
HoverHandler {
id: videoHoverHandler
onHoveredChanged: {
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
videoControlTimer.restart();
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
onTapped: if (root.fileTransferInfo.completed) {
if (root.playbackState == MediaPlayer.PlayingState) {
root.pause();
} else {
MediaManager.startPlayback();
root.play();
}
} else {
root.downloadAndPlay();
}
}
MediaSizeHelper {
id: mediaSizeHelper
contentMaxWidth: root.Message.maxContentWidth
mediaWidth: root.mediaInfo.width
mediaHeight: root.mediaInfo.height
}
function downloadAndPlay() {
if (root.downloaded) {
playSavedFile();
} else {
playOnFinished = true;
Message.room.downloadFile(root.eventId, Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + Message.room.fileNameToDownload(root.eventId));
}
}
function playSavedFile() {
root.stop();
MediaManager.startPlayback();
root.play();
}
}

View File

@@ -0,0 +1,8 @@
/*
SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF6 "${KDE_INSTALL_FULL_LIBEXECDIR_KF}"

View File

@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "contentprovider.h"
ContentProvider::ContentProvider(QObject *parent)
: QObject(parent)
{
}
ContentProvider &ContentProvider::self()
{
static ContentProvider instance;
return instance;
}
MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply)
{
if (!room || evtOrTxnId.isEmpty()) {
return nullptr;
}
if (!m_eventContentModels.contains(evtOrTxnId)) {
auto model = new MessageContentModel(room, evtOrTxnId, isReply);
QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
m_eventContentModels.insert(evtOrTxnId, model);
}
return m_eventContentModels.object(evtOrTxnId);
}
MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply)
{
if (!room) {
return nullptr;
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
if (roomMessageEvent == nullptr) {
// If for some reason a model is there remove.
if (m_eventContentModels.contains(event->id())) {
m_eventContentModels.remove(event->id());
}
if (m_eventContentModels.contains(event->transactionId())) {
m_eventContentModels.remove(event->transactionId());
}
return nullptr;
}
if (event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
return nullptr;
}
auto eventId = event->id();
const auto txnId = event->transactionId();
if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) {
auto model = new MessageContentModel(room, eventId.isEmpty() ? txnId : eventId, isReply, eventId.isEmpty());
QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
m_eventContentModels.insert(eventId.isEmpty() ? txnId : eventId, model);
}
if (!eventId.isEmpty() && m_eventContentModels.contains(eventId)) {
return m_eventContentModels.object(eventId);
}
if (!txnId.isEmpty() && m_eventContentModels.contains(txnId)) {
if (eventId.isEmpty()) {
return m_eventContentModels.object(txnId);
}
// If we now have an event ID use that as the map key instead of transaction ID.
auto txnModel = m_eventContentModels.take(txnId);
m_eventContentModels.insert(eventId, txnModel);
return m_eventContentModels.object(eventId);
}
return nullptr;
}
ThreadModel *ContentProvider::modelForThread(NeoChatRoom *room, const QString &threadRootId)
{
if (!room || threadRootId.isEmpty()) {
return nullptr;
}
if (!m_threadModels.contains(threadRootId)) {
auto model = new ThreadModel(threadRootId, room);
QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
m_threadModels.insert(threadRootId, model);
}
return m_threadModels.object(threadRootId);
}
static PollHandler *emptyPollHandler = new PollHandler;
PollHandler *ContentProvider::handlerForPoll(NeoChatRoom *room, const QString &eventId)
{
if (!room || eventId.isEmpty()) {
return nullptr;
}
const auto event = room->getEvent(eventId);
if (event.first == nullptr || event.second) {
return emptyPollHandler;
}
if (!m_pollHandlers.contains(eventId)) {
auto pollHandler = new PollHandler(room, eventId);
QQmlEngine::setObjectOwnership(pollHandler, QQmlEngine::CppOwnership);
m_pollHandlers.insert(eventId, pollHandler);
}
return m_pollHandlers.object(eventId);
}
void ContentProvider::setThreadsEnabled(bool enableThreads)
{
MessageContentModel::setThreadsEnabled(enableThreads);
for (const auto &key : m_eventContentModels.keys()) {
m_eventContentModels.object(key)->threadsEnabledChanged();
}
}
#include "moc_contentprovider.cpp"

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QCache>
#include <QObject>
#include <QQmlEngine>
#include "models/messagecontentmodel.h"
#include "models/threadmodel.h"
#include "neochatroom.h"
#include "pollhandler.h"
/**
* @class ContentProvider
*
* Store and retrieve models for message content.
*/
class ContentProvider : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
/**
* Get the global instance of ContentProvider.
*/
static ContentProvider &self();
static ContentProvider *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&self(), QQmlEngine::CppOwnership);
return &self();
}
/**
* @brief Returns the content model for the given event ID.
*
* A model is created if one doesn't exist. Will return nullptr if evtOrTxnId
* is empty.
*
* @warning If a non-empty ID is given it is assumed to be a valid Quotient::RoomMessageEvent
* event ID. The caller must ensure that the ID is a real event. A model will be
* returned unconditionally.
*
* @warning Do NOT use for pending events as this function has no way to differentiate.
*/
Q_INVOKABLE MessageContentModel *contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply = false);
/**
* @brief Returns the content model for the given event.
*
* A model is created if one doesn't exist. Will return nullptr if event is:
* - nullptr
* - not a Quotient::RoomMessageEvent (e.g a state event)
*
* @note This method is preferred to the version using just an event ID as it
* can perform some basic checks. If a copy of the event is not available,
* you may have to use the version that takes an event ID.
*
* @note This version must be used for pending events as it can differentiate.
*/
MessageContentModel *contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false);
/**
* @brief Returns the thread model for the given thread root event ID.
*
* A model is created if one doesn't exist. Will return nullptr if threadRootId
* is empty.
*/
ThreadModel *modelForThread(NeoChatRoom *room, const QString &threadRootId);
/**
* @brief Get a PollHandler object for the given event Id.
*
* Will return an existing PollHandler if one already exists for the event ID.
* A new PollHandler will be created if one doesn't exist.
*
* @sa PollHandler
*/
Q_INVOKABLE PollHandler *handlerForPoll(NeoChatRoom *room, const QString &eventId);
void setThreadsEnabled(bool enableThreads);
private:
explicit ContentProvider(QObject *parent = nullptr);
QCache<QString, MessageContentModel> m_eventContentModels;
QCache<QString, ThreadModel> m_threadModels;
QCache<QString, PollHandler> m_pollHandlers;
};

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g
transform="matrix(0.00938989,0,0,-0.00938989,-1.1929841,18.425545)"
id="g4">
<path
sodipodi:nodetypes="sssccscsssssssssssccsccccsccccssssssssscsccssccsssssssscscccssssccsssssascccssssscssssssccscssssssssscsssccssscssssscccccccscccsccc"
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="M 2251.5,232 C 2165.8333,146.66667 2063,104 1943,104 c -114,0 -212.8333,38.5 -296.5,115.5 -83.6667,77 -130.1667,170.83333 -139.5,281.5 h -122 c -3.3333,-13.33333 -5.8333,-25.83333 -7.5,-37.5 -1.6667,-11.66667 -4.1667,-23.5 -7.5,-35.5 20.6667,0 37.6667,-4 51,-12 13.3333,-8 20,-22.66667 20,-44 0,-18.66667 -7.6667,-32.5 -23,-41.5 -15.3333,-9 -39,-13.5 -71,-13.5 -10.6667,0 -20.8333,0.33333 -30.5,1 -9.6667,0.66667 -18.1667,1 -25.5,1 h -21 c -22.6667,0 -38.1667,3.83333 -46.5,11.5 -8.3333,7.66667 -12.5,19.83333 -12.5,36.5 0,18 9.3333,33.16667 28,45.5 18.6667,12.33333 38,18.5 58,18.5 h 6 l 5,-1 c 3.3333,13.33333 5.5,25.33333 6.5,36 1,10.66667 3.1667,22.33333 6.5,35 h -14 l -6,1 h -9 c -2,1.33333 -5.5,2.83333 -10.5,4.5 -5,1.66667 -11.1667,7.5 -18.5,17.5 L 962,972 c -4,-15.33333 -8.66667,-31 -14,-47 l -16,-47 c 49.33333,-40.66667 88.5,-90.16667 117.5,-148.5 C 1078.5,671.16667 1093,607.66667 1093,539 1093,420.33333 1050.3333,318.16667 965,232.5 879.66667,146.83333 776.66667,104 656,104 535.33333,104 432.33333,146.83333 347,232.5 261.66667,318.16667 219,420.33333 219,539 c 0,120 43,222.83333 129,308.5 86,85.66667 188.66667,128.5 308,128.5 33.33333,0 65.33333,-3.5 96,-10.5 30.66667,-7 60,-17.16667 88,-30.5 15.33333,44 28,88.6667 38,134 10,45.3333 19.33333,90.3333 28,135 l 4,22 c 14,82 34.83333,131.8333 62.5,149.5 27.6667,17.6667 48.8333,31.3333 63.5,41 14.6667,9.6667 61.3333,14.5 140,14.5 l 61,-1 c 16,0 30.8333,-7.1667 44.5,-21.5 13.6667,-14.3333 20.5,-30.8333 20.5,-49.5 0,-20.6667 -10,-38.8333 -30,-54.5 -20,-15.6667 -52,-23.5 -96,-23.5 h -51 c -9.3333,0 -18.8333,-0.5 -28.5,-1.5 -9.6667,-1 -18.1667,-1.5 -25.5,-1.5 -6.6667,0 -13,0.8333 -19,2.5 -6,1.6667 -11.3333,4.5 -16,8.5 -5.3333,-6.6667 -9.5,-16.1667 -12.5,-28.5 -3,-12.3333 -6.8333,-29.8333 -11.5,-52.5 l -15,-81 h 443 c -34.6667,4 -62.5,12 -83.5,24 -21,12 -31.5,27 -31.5,45 0,16.6667 6,32 18,46 12,14 27.3333,21.8333 46,23.5 18.6667,1.6667 42,5.1667 70,10.5 l 75,13 c 24.6667,4 49,7.5 73,10.5 24,3 47,4.5 69,4.5 22.6667,0 42.5,-8.8333 59.5,-26.5 17,-17.6667 25.5,-38.1667 25.5,-61.5 0,-27.3333 -12.6667,-51.5 -38,-72.5 -25.3333,-21 -53.2102,-26.1605 -82,-31.5 -5.8994,-1.0941 -12.0021,-0.1577 -18,0 -6.3399,0.1667 -12.3333,0.3333 -19,1 v -21 l 126,-170 c 32,17.33333 65.8333,30.66667 101.5,40 35.6667,9.33333 72.8333,14 111.5,14 118.6667,0 221.1667,-42.66667 307.5,-128 86.3333,-85.33333 129.5,-188.33333 129.5,-309 0,-119.33333 -42.8333,-221.66667 -128.5,-307 z M 885,764 C 850.33333,690.66667 811.5,628.83333 768.5,578.5 725.5,528.16667 700,500 692,494 c -8,-6 -19,-9 -33,-9 -14.66667,0 -27.66667,5.16667 -39,15.5 -11.33333,10.33333 -17,23.16667 -17,38.5 0,9.33333 2.16667,18 6.5,26 4.33333,8 11.16667,14.66667 20.5,20 l 7,5 c 36,36 66.83333,73.66667 92.5,113 25.66667,39.33333 48.16667,80.66667 67.5,124 -21.33333,10.66667 -43.83333,18.83333 -67.5,24.5 -23.66667,5.66667 -48.16667,8.5 -73.5,8.5 -86.66667,0 -161.66667,-31.33333 -225,-94 -63.33333,-62.66667 -95,-138.33333 -95,-227 0,-88 31.33333,-163.33333 94,-226 62.66667,-62.66667 138,-94 226,-94 88.66667,0 164.33333,31.66667 227,95 62.66667,63.33333 94,138.33333 94,225 0,44 -8.33333,85 -25,123 -16.66667,38 -39,72 -67,102 z m 1283,1 c -63.3333,63.33333 -138.3333,95 -225,95 -25.3333,0 -50,-2.83333 -74,-8.5 -24,-5.66667 -46.3333,-13.83333 -67,-24.5 l 179,-242 c 6.6667,-9.33333 10,-20 10,-32 0,-14.66667 -5.1667,-27 -15.5,-37 -10.3333,-10 -22.5,-15 -36.5,-15 h -314 c 9.3333,-80.66667 44.3333,-147.83333 105,-201.5 60.6667,-53.66667 131.6667,-80.5 213,-80.5 88,0 163.3333,31.66667 226,95 62.6667,63.33333 94,138.33333 94,225 0,87.33333 -31.6667,162.66667 -95,226 z m -1113,257 244,-362 170,362 z m 507,-50 -171,-365 h 120 c 7.3333,49.33333 22.5,95.5 45.5,138.5 23,43 52.5,81.16667 88.5,114.5 z m 153,-207 c -22,-21.33333 -40.3333,-45.33333 -55,-72 -14.6667,-26.66667 -25,-55.33333 -31,-86 h 205 z"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 18.679188,11.502469 0.02034,0.569593 v 5.197534 q 0,0.23394 -0.16274,0.401766 -0.162741,0.167827 -0.396681,0.167827 h -0.925588 v 1.403639 q 0,0.406852 -0.274626,0.686563 -0.274625,0.279711 -0.681477,0.279711 -0.39668,0 -0.676391,-0.284797 -0.279711,-0.284796 -0.279711,-0.681477 V 17.839189 H 6.7075677 v 1.403639 q 0,0.406852 -0.2847964,0.686563 -0.2847964,0.279711 -0.6713058,0.279711 -0.3966807,0 -0.6763915,-0.284797 Q 4.7953633,19.639509 4.7953633,19.242828 V 17.839189 H 3.869775 q -0.2339399,0 -0.3966807,-0.167827 -0.1627408,-0.167826 -0.1627408,-0.401766 v -4.627942 l 0.010171,-0.569592 q 0,-0.335653 0.020343,-0.569593 L 3.7375481,4.1079337 Q 3.7578907,3.8231373 3.9308028,3.4773131 4.1037149,3.1314889 4.9326758,2.7144656 5.7616368,2.2974423 7.2822461,1.982132 8.8028555,1.6668217 10.908315,1.6668217 q 1.810491,0 3.290415,0.2491969 1.479924,0.2491968 2.558082,0.7069053 1.078158,0.4577085 1.28667,0.813704 0.208511,0.3559955 0.239025,0.6713058 z M 7.2771605,3.0501185 q -0.2644538,0 -0.4526229,0.188169 -0.188169,0.1881691 -0.188169,0.4526229 0,0.2644538 0.188169,0.4475372 Q 7.0127067,4.321531 7.2771605,4.321531 H 14.54964 q 0.264454,0 0.447537,-0.1830834 0.183084,-0.1830834 0.183084,-0.4475372 0,-0.2644538 -0.183084,-0.4526229 -0.183083,-0.188169 -0.447537,-0.188169 z M 4.7445068,12.194117 H 17.275548 L 17.082294,5.1860915 H 4.9275902 Z m 1.8257483,2.130888 q -0.3203959,-0.320396 -0.7577618,-0.320396 -0.4475372,0 -0.7628475,0.325481 -0.3153103,0.325482 -0.3153103,0.752676 0,0.427195 0.3153103,0.752677 0.3153103,0.325481 0.7628475,0.325481 0.4373659,0 0.7577618,-0.320396 0.320396,-0.320396 0.320396,-0.757762 0,-0.437365 -0.320396,-0.757761 z m 10.4611819,0.0051 q -0.31531,-0.325481 -0.752676,-0.325481 -0.447537,0 -0.762847,0.325481 -0.315311,0.325482 -0.315311,0.752676 0,0.427195 0.315311,0.752677 0.31531,0.325481 0.762847,0.325481 0.437366,0 0.752676,-0.325481 0.31531,-0.325482 0.31531,-0.752677 0,-0.427194 -0.31531,-0.752676 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 14.879482,5.2406591 q -0.07353,0.1562559 -0.216,0.2527668 -0.142469,0.096511 -0.326299,0.096511 h -1.700431 l 1.645282,5.1104851 h 2.803413 q 0.23898,0 0.436597,0.174639 0.197618,0.174639 0.243576,0.413618 l 0.661789,4.255674 q 0,0.07353 0.0092,0.160852 0.0092,0.08732 0.0092,0.188426 0,0.18383 -0.02298,0.413618 -0.02298,0.229788 -0.06894,0.386044 l -0.523917,1.939411 q -0.06434,0.238979 -0.284937,0.404427 -0.220596,0.165447 -0.459576,0.165447 H 5.0629391 q -0.2481711,0 -0.4733633,-0.165447 Q 4.3643835,18.871683 4.2908514,18.641895 L 3.6842111,16.684102 q -0.045958,-0.147065 -0.068936,-0.349278 -0.022979,-0.202214 -0.022979,-0.404427 0,-0.220597 0.027575,-0.386044 L 4.3643835,11.288679 Q 4.4103411,11.0497 4.6125546,10.875061 4.814768,10.700422 5.0629391,10.700422 h 3.02401 L 9.6035499,5.5899369 H 7.6825223 q -0.1838304,0 -0.3217032,-0.096511 Q 7.2229463,5.396915 7.1494141,5.2406591 H 0.26496562 V 4.6432103 H 7.094265 V 4.5237206 q 0,-0.2389796 0.1746389,-0.4136184 0.1746388,-0.1746389 0.4136184,-0.1746389 h 1.0110672 q 0.248171,0 0.4182141,-0.1746389 Q 9.2818467,3.5861855 9.2818467,3.3380145 V 3.2001417 q 0,-0.2389795 0.1792347,-0.4136184 Q 9.640316,2.6118844 9.8792955,2.6118844 h 2.2703055 q 0.248171,0 0.418214,0.1746389 0.170043,0.1746389 0.170043,0.4136184 v 0.1378728 q 0,0.248171 0.179235,0.4228099 0.179235,0.1746389 0.418214,0.1746389 h 1.001876 q 0.248171,0 0.42281,0.1746389 0.174639,0.1746388 0.174639,0.4136184 v 0.1194897 h 6.829299 v 0.5974488 z m 0.03677,9.9636079 q 0,0.156256 0.101107,0.257362 0.101107,0.101107 0.257363,0.101107 h 1.957794 q 0.128681,0 0.216,-0.07813 0.08732,-0.07813 0.08732,-0.206809 l -0.42281,-2.739073 q -0.02758,-0.156256 -0.142469,-0.257363 -0.114894,-0.101106 -0.261958,-0.101106 h -1.433877 q -0.156256,0 -0.257363,0.105702 -0.101107,0.105703 -0.101107,0.252767 z M 10.56866,4.3766562 q 0.18383,0.1838304 0.450384,0.1838304 0.257363,0 0.441193,-0.1838304 0.18383,-0.1838304 0.18383,-0.4411929 0,-0.2665541 -0.18383,-0.4503845 -0.18383,-0.1838304 -0.441193,-0.1838304 -0.266554,0 -0.450384,0.1838304 -0.183831,0.1838304 -0.183831,0.4503845 0,0.2573625 0.183831,0.4411929 z m -0.42281,3.7685232 h 2.003751 L 11.313173,5.5899369 H 10.908746 Z M 7.112648,12.538726 q 0,-0.147064 -0.1011067,-0.252767 -0.1011067,-0.105702 -0.248171,-0.105702 H 5.4122168 q -0.1562558,0 -0.2757456,0.101106 -0.1194897,0.101107 -0.1470643,0.257363 l -0.477959,2.729881 q 0,0.128682 0.087319,0.211405 0.087319,0.08272 0.2160007,0.08272 h 1.9486023 q 0.1470643,0 0.248171,-0.101107 0.1011067,-0.101106 0.1011067,-0.257362 z m 3.456012,0 q 0,-0.147064 -0.105703,-0.252767 -0.105702,-0.105702 -0.252767,-0.105702 H 8.3718863 q -0.1470644,0 -0.2527668,0.105702 -0.1057025,0.105703 -0.1057025,0.252767 v 2.665541 q 0,0.156256 0.1057025,0.257362 0.1057024,0.101107 0.2527668,0.101107 H 10.21019 q 0.147065,0 0.252767,-0.101107 0.105703,-0.101106 0.105703,-0.257362 z m 3.437628,0 q 0,-0.147064 -0.101107,-0.252767 -0.101106,-0.105702 -0.257362,-0.105702 h -1.829113 q -0.147064,0 -0.248171,0.105702 -0.101107,0.105703 -0.101107,0.252767 v 2.665541 q 0,0.156256 0.101107,0.257362 0.101107,0.101107 0.248171,0.101107 h 1.829113 q 0.156256,0 0.257362,-0.101107 0.101107,-0.101106 0.101107,-0.257362 z M 12.416155,8.9816078 H 9.9068701 L 9.392145,10.700422 h 3.575501 z"
id="path4751" />
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 20.363699,14.382151 q -0.682766,0.758106 -1.690435,0.758106 h -0.131845 q 0,-0.875825 -0.640388,-1.520921 -0.640388,-0.645097 -1.544465,-0.645097 -0.89466,0 -1.535048,0.635679 -0.640388,0.635679 -0.640388,1.530339 H 7.5041442 q 0,-0.89466 -0.640388,-1.530339 -0.6403879,-0.635679 -1.5350476,-0.635679 -0.9040771,0 -1.5444651,0.645097 -0.6403879,0.645096 -0.6403879,1.520921 -0.7157278,0 -1.4738341,-0.645097 -0.75810633,-0.645096 -0.75810633,-1.841115 0,-0.809902 0.31548523,-1.497378 Q 1.5428857,10.469192 2.6541471,9.9983184 3.7654086,9.5274449 4.3163306,9.2967169 4.8672526,9.0659889 5.4511357,7.8888051 L 5.7807472,7.2389997 Q 6.4305526,5.9770587 7.4099695,5.3790494 8.3893864,4.78104 9.7172496,4.78104 h 3.5880564 q 1.33728,0 2.415581,0.5885919 1.0783,0.5885919 1.944707,1.954125 l 0.329612,0.5179609 q 0.404951,0.6403879 0.904077,1.1395139 0.499126,0.4991259 1.101844,1.0877173 0.602718,0.588592 0.824028,1.224272 0.221311,0.635679 0.221311,1.257232 0,1.073591 -0.682767,1.831698 z M 10.216376,6.683369 q 0,-0.4520386 -0.117719,-0.621553 -0.1177182,-0.1695145 -0.3719899,-0.1695145 -0.6498054,0 -1.3937856,0.3531551 Q 7.5889014,6.5986117 7.0521056,7.394388 6.5153098,8.1901642 6.5153098,8.7175425 q 0,0.2166018 0.1130097,0.3484464 0.1130096,0.1318446 0.3672813,0.1318446 h 2.5803868 q 0.2731066,0 0.4567474,-0.1412621 0.183641,-0.141262 0.183641,-0.480291 z m 5.725821,2.5144645 q 0.357864,0 0.517961,-0.1695145 0.160097,-0.1695145 0.160097,-0.4426211 0,-0.621553 -0.687475,-1.4597078 Q 15.245305,6.2878352 14.327101,6.0853596 13.408898,5.882884 12.382394,5.882884 q -0.527379,0 -0.739272,0.080049 -0.211893,0.080049 -0.287233,0.2071844 -0.07534,0.1271358 -0.07534,0.5132521 v 1.8929114 q 0,0.3484464 0.277815,0.4849998 0.277815,0.1365533 0.673349,0.1365533 z M 6.4635137,16.289188 q -0.4755822,0.480291 -1.1348051,0.480291 -0.6780578,0 -1.1536401,-0.475582 -0.4755822,-0.475582 -0.4755822,-1.15364 0,-0.659223 0.480291,-1.134805 0.4802909,-0.475582 1.1489313,-0.475582 0.6592229,0 1.1348051,0.475582 0.4755823,0.475582 0.4755823,1.134805 0,0.66864 -0.4755823,1.148931 z m 11.0466923,0.0047 q -0.475582,0.475582 -1.15364,0.475582 -0.678058,0 -1.148931,-0.480291 -0.470874,-0.480291 -0.470874,-1.148931 0,-0.649805 0.470874,-1.130096 0.470873,-0.480291 1.148931,-0.480291 0.66864,0 1.148931,0.475582 0.480291,0.475582 0.480291,1.134805 0,0.678058 -0.475582,1.15364 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 21.126606,12.816239 q 0,0.344378 -0.231379,0.721041 -0.231379,0.376663 -0.608042,0.753326 0,0.05381 0.01076,0.112999 0.01076,0.05919 0.01076,0.123761 0,1.194559 -0.274426,1.770316 -0.274426,0.575756 -0.780231,0.575756 -0.30133,0 -0.548851,-0.242141 -0.247522,-0.24214 -0.376663,-0.823277 -0.807135,0.548852 -1.447462,0.963181 -0.640328,0.414329 -1.210703,0.629565 -0.02152,1.086942 -0.355139,1.646556 -0.333616,0.559613 -0.968562,0.559613 -0.559614,0 -0.898611,-0.446615 -0.338996,-0.446614 -0.414329,-1.329082 H 5.7372316 q -0.075333,0.89323 -0.4143293,1.334463 -0.3389967,0.441234 -0.8986103,0.441234 -0.5811372,0 -0.9201339,-0.4789 -0.3389967,-0.4789 -0.3928057,-1.4367 Q 2.2396466,17.433052 1.6746521,16.674345 1.1096576,15.915638 1.1096576,14.979361 V 4.9170782 q 0,-1.0546564 0.7479451,-1.721888 Q 2.6055478,2.5279586 3.7893458,2.5279586 H 18.94196 q 1.183798,0 1.684222,0.591899 0.500424,0.591899 0.500424,1.775697 z M 3.0683052,9.5876994 q 0,0.376663 0.2798068,0.6672316 0.2798068,0.290569 0.6779934,0.290569 h 7.9960176 q 0.398186,0 0.677993,-0.290569 0.279807,-0.2905686 0.279807,-0.6672316 v -4.30472 q 0,-0.4089484 -0.279807,-0.6833743 Q 12.420309,4.3251792 12.022123,4.3251792 H 4.0261054 q -0.3981866,0 -0.6779934,0.2744259 Q 3.0683052,4.874031 3.0683052,5.2829794 Z M 17.403023,5.0569816 q 0,-0.3013304 -0.156046,-0.5165664 -0.156046,-0.215236 -0.392806,-0.215236 h -1.162274 q -0.322854,0 -0.570376,0.2744259 Q 14.874,4.874031 14.874,5.2829794 v 4.30472 q 0,0.2798068 0.129141,0.4358526 0.129142,0.156046 0.344378,0.156046 0.172189,0 0.344378,-0.107618 L 16.854171,9.4262724 Q 17.090931,9.286369 17.246977,8.9796577 17.403023,8.6729464 17.403023,8.371616 Z m 1.431319,2.6258792 q 0,0.215236 0.06457,0.3389967 0.06457,0.1237607 0.161427,0.1237607 0.02152,0 0.05381,-0.010762 l 0.05381,-0.021524 0.527328,-0.3013304 q 0.107618,-0.064571 0.182951,-0.2529023 0.07533,-0.1883315 0.07533,-0.4035675 V 4.8525074 q 0,-0.215236 -0.07533,-0.3712821 -0.07533,-0.1560461 -0.182951,-0.1560461 h -0.527328 q -0.129142,0 -0.231379,0.1829506 -0.102237,0.1829506 -0.102237,0.430472 z m -5.671468,6.8875522 q -0.312093,-0.312092 -0.764088,-0.312092 -0.451996,0 -0.764088,0.312092 -0.312092,0.312092 -0.312092,0.764088 0,0.441233 0.306711,0.758706 0.306711,0.317474 0.769469,0.317474 0.462757,0 0.769469,-0.317474 0.306711,-0.317473 0.306711,-0.758706 0,-0.451996 -0.312092,-0.764088 z m -8.7977719,1.522794 q 0.3067113,-0.317473 0.3067113,-0.758706 0,-0.441234 -0.3174731,-0.758707 -0.3174731,-0.317473 -0.7587069,-0.317473 -0.4519956,0 -0.7640878,0.312092 -0.3120922,0.312092 -0.3120922,0.764088 0,0.451995 0.3120922,0.764087 0.3120922,0.312093 0.7640878,0.312093 0.4627574,0 0.7694687,-0.317474 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="22" height="22" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color:#4d4d4d; }
</style>
<polygon
style="fill:currentColor;stroke-width:0"
points="13.843885,3.2142857 9.0837593,6.7294713 0,6.7294713 0,11.785714 1.1319308,11.785714 1.1319308,9.1029246 13.790798,9.1029246 13.790798,11.785714 15,11.785714 15,3.2142857 "
id="polygon3"
transform="matrix(1.3138289,0,0,1.3138289,1.1118802,1.1489425)" />
</svg>

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path d="m 13.891951,2.1595513 v 1.465731 h 2.931463 V 18.282592 H 14.26706 l -0.03966,1.465731 h 5.527474 V 2.1595513 Z M 6.5632967,12.419668 10.96049,19.748323 15.357683,12.419668 H 12.426221 V 9.4882061 h 2.931462 L 10.96049,2.1595513 6.5632967,9.4882061 h 2.931462 v 2.9314619 z m 0.8526688,7.328655 0.059488,-1.465731 H 5.0975654 V 3.6252823 H 8.0290275 V 2.1595513 H 2.1661038 V 19.748323 Z" class="ColorScheme-Text" style="fill:currentColor;"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: https://github.com/gravitystorm/openstreetmap-carto

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="M 15.421821,4.149711 C 14.791977,4.1493251 13.983404,4.4066009 13.50051,4.8906508 L 5.6613053,12.681742 C 5.4318152,12.91046 5.4044789,12.921922 4.952441,12.922308 H 3.4545242 c -1.5034503,-3.86e-4 -2.4044909,1.35083 -2.4056481,2.498665 0.00115,1.143981 0.9019199,2.592455 2.5403643,2.591685 0,0 2.0343344,0.0011 2.6622505,0 0.6290732,0.0011 1.3908996,-0.237424 2.0207445,-0.827542 L 16.066534,9.406854 c 0.166238,-0.1465713 0.260251,-0.1924526 0.545282,-0.1924526 h 1.927725 c 0.924904,0 2.234878,-1.0477952 2.235649,-2.5628173 C 20.77409,5.1543052 19.454577,4.1527968 18.462561,4.149711 Z m 0.182828,1.2926342 h 2.732819 c 0.664169,0.00155 1.113782,0.7440044 1.113012,1.1867861 0.0011,0.4447097 -0.318821,1.2730043 -1.132258,1.2733903 h -1.642256 c -0.697342,-3.86e-4 -1.006209,0.097205 -1.408107,0.4971665 L 7.4158246,16.25814 C 7.096467,16.576339 6.6936595,16.70155 6.1745102,16.700778 H 3.5860329 c -0.6664858,7.77e-4 -1.253373,-0.561595 -1.2541446,-1.29905 7.71e-4,-0.741311 0.5608982,-1.156374 1.1386735,-1.157918 h 1.7769721 c 0.5176063,0.0015 0.8596525,-0.145258 1.1547109,-0.44264 L 14.388997,5.8144197 C 14.719154,5.4865778 15.044619,5.443889 15.604649,5.4423452 Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 19.904344,13.991511 c 0.04329,0 0.286785,0.03246 0.730485,0.09739 0.443714,0.06493 0.665564,0.313843 0.665564,0.746717 0,0.238089 -0.0505,0.487004 -0.151509,0.746731 -0.100998,0.259734 -0.216443,0.519461 -0.346307,0.779195 l -0.400417,0.768369 c -0.144296,0.281372 -0.232669,0.503229 -0.265147,0.665557 -0.03246,0.162328 -0.05591,0.398617 -0.07033,0.708847 H 3.6279025 c 0.036078,-0.05772 0.068542,-0.122651 0.097399,-0.194792 0.028858,-0.07215 0.043283,-0.147909 0.043283,-0.22727 0,-0.245302 -0.091979,-0.532087 -0.2759591,-0.860362 -0.1839798,-0.328262 -0.3138363,-0.597016 -0.3895906,-0.80624 -0.075754,-0.209231 -0.4545329,-0.80264 -1.1363217,-1.78024 -0.6817958,-0.9776 -1.05515432,-1.583628 -1.12008959,-1.818111 -0.0649353,-0.234482 -0.0973994,-0.402216 -0.0973994,-0.503229 0,-0.209224 0.077554,-0.362539 0.23267596,-0.459938 0.15511493,-0.0974 0.34450073,-0.146096 0.56816433,-0.146096 0.093786,0 0.6853951,0.05591 1.7748273,0.167741 1.0894181,0.111831 1.8722197,0.200211 2.3483906,0.26514 0.079361,-0.31745 0.165934,-0.62948 0.2597341,-0.93611 0.093786,-0.306624 0.1803664,-0.61866 0.2597271,-0.93611 l 0.0974,-0.3895946 C 6.347859,9.604946 6.4380387,9.377676 6.5606966,9.1973096 6.6833474,9.0169432 6.9106103,8.9267635 7.2424854,8.9267635 h 0.248908 c 0.3535201,0 0.6998275,0.025251 1.0389222,0.075754 0.3390878,0.050503 0.6817888,0.093786 1.0281033,0.1298636 l 1.7315301,0.2056178 c 0.07937,-0.4256611 0.14069,-0.8567492 0.183973,-1.2932433 0.04329,-0.4364871 0.07936,-0.8820006 0.108225,-1.3365265 0.187586,-0.1515086 0.387791,-0.2579274 0.600629,-0.3192493 0.21283,-0.061329 0.434687,-0.091993 0.665556,-0.091993 0.440101,0 0.901839,0.075754 1.38523,0.22727 0.483391,0.1515086 0.836911,0.331875 1.060567,0.5410992 l -0.08657,2.2726433 c 0,0.1010058 0.0036,0.1984052 0.01081,0.2922052 V 9.9115697 L 16.59278,10.12802 c 0.4401,0.06492 0.900039,0.151502 1.379816,0.259728 0.479785,0.108218 0.761157,0.192992 0.844131,0.254321 0.08297,0.06132 0.173147,0.315636 0.270553,0.762956 0.09739,0.447313 0.146096,0.919877 0.146096,1.417694 v 0.292198 c 0,0.137083 0.01082,0.270553 0.03246,0.400416 l 0.04329,0.40041 z M 11.657901,6.0913689 c 0.01443,-0.1803734 0.07215,-0.4455065 0.173154,-0.7954273 0.101006,-0.3499067 0.295805,-0.569957 0.584397,-0.6601507 0.288591,-0.090173 0.566357,-0.1352695 0.833297,-0.1352695 0.548326,0 1.03171,0.1136314 1.450165,0.3408944 0.418448,0.2272699 0.627687,0.5356932 0.627687,0.9252978 0,0.1009987 -0.0036,0.2002048 -0.01083,0.2976042 -0.0072,0.097399 -0.01082,0.1965984 -0.01082,0.2976042 -0.13709,-0.1515086 -0.452726,-0.3012106 -0.946935,-0.4491198 -0.494217,-0.1478953 -0.918079,-0.22185 -1.271599,-0.22185 -0.367952,0 -0.665556,0.034271 -0.892826,0.1028055 -0.227256,0.068549 -0.40583,0.1677477 -0.535694,0.2976112 z m 3.592938,6.6231241 c 0.180367,0.173161 0.393204,0.259734 0.638506,0.259734 0.252514,0 0.465345,-0.08657 0.638505,-0.259734 0.173154,-0.173154 0.259735,-0.385984 0.259735,-0.638499 0,-0.252521 -0.08658,-0.465351 -0.259735,-0.638505 -0.17316,-0.173154 -0.385991,-0.259734 -0.638505,-0.259734 -0.245302,0 -0.458139,0.08658 -0.638506,0.259734 -0.180366,0.173154 -0.270553,0.385984 -0.270553,0.638505 0,0.252515 0.09019,0.465345 0.270553,0.638499 z M 8.7467595,12.021885 c 0.1731537,0.180367 0.3823779,0.270553 0.6276796,0.270553 0.2525144,0 0.4671516,-0.08839 0.6439119,-0.265147 0.176767,-0.17676 0.265147,-0.391397 0.265147,-0.643911 0,-0.245302 -0.09019,-0.454533 -0.270553,-0.627687 -0.1803667,-0.173147 -0.3932042,-0.259727 -0.6385059,-0.259727 -0.2453017,0 -0.4545259,0.08658 -0.6276796,0.259727 -0.1731537,0.173154 -0.259734,0.382385 -0.259734,0.627687 0,0.245301 0.08658,0.458139 0.259734,0.638505 z m 3.2466235,-0.919878 c -0.173147,0.180367 -0.259727,0.393204 -0.259727,0.638499 0,0.245302 0.09019,0.454526 0.27056,0.627687 0.180366,0.173146 0.393197,0.259727 0.638499,0.259727 0.245301,0 0.454525,-0.08658 0.627679,-0.259727 0.173154,-0.173161 0.259727,-0.382385 0.259727,-0.627687 0,-0.245295 -0.08477,-0.458132 -0.254314,-0.638499 -0.169547,-0.180373 -0.380578,-0.270553 -0.633092,-0.270553 -0.259728,0 -0.476171,0.09018 -0.649332,0.270553 z"
id="path4751" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none"
d="m 2.3243326,11.281529 v -0.48768 L 2.763251,10.732949 1.9829519,8.0750591 h 1.2192176 l 1.7800603,2.4506299 c 1.1867061,-0.10575 2.9423813,-0.19913 5.2670252,-0.28042 C 9.8184645,8.0019091 8.9081155,5.4090391 7.5182042,2.4666491 h 1.4996403 c 2.1134945,2.74341 3.8331395,4.98965 5.9741715,7.7786199 h 3.218737 c 0.568969,0 1.048529,0.10574 1.438679,0.317 0.2601,0.13817 0.390149,0.29667 0.390149,0.47549 0,0.17883 -0.130044,0.33732 -0.390149,0.4755 -0.39015,0.21133 -0.86971,0.31699 -1.438679,0.31699 h -3.218737 c -2.145078,2.80603 -3.840734,4.99587 -5.9741715,7.77861 H 7.5182042 c 1.3899113,-2.86108 2.3002603,-5.45396 2.7310508,-7.77861 -2.3246439,-0.0894 -4.0803191,-0.18287 -5.2670252,-0.28042 l -1.7800603,2.45061 H 1.9829519 l 0.7802991,-2.65791 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none"
d="m 16.15861,18.818425 c 0,0.264672 -0.118157,0.527767 -0.354472,0.789284 -0.236311,0.261523 -0.486803,0.392284 -0.751475,0.392284 -0.327683,0 -0.58605,-0.189053 -0.775102,-0.567158 -0.15124,-0.30878 -0.22686,-0.645917 -0.22686,-1.011411 V 13.439957 C 13.73562,12.65225 13.57808,11.684944 13.57808,10.538039 V 4.3372017 c 0,-0.5545465 0.110277,-1.0744333 0.330832,-1.5596604 0.283579,-0.6049597 0.671132,-0.9074396 1.16266,-0.9074396 0.724692,0 1.087038,0.5955067 1.087038,1.7865202 z M 10.146822,6.9839006 c 0,0.8948384 -0.5621719,2.2156104 -1.639757,2.6063117 l 0.1368084,2.9612067 c 0.00627,0.126037 -0.1449733,-0.743594 0,0 0.2279252,5.263533 0.2279252,4.53254 0.2279252,5.263533 0,0.724692 0.039954,1.19782 -0.067173,1.512902 -0.1953527,0.554546 -0.1386973,0.738301 -0.7184505,0.738301 -0.5734481,0 -0.4871106,-0.247591 -0.6761628,-0.789532 -0.1134277,-0.385224 -0.09777,-0.685562 -0.076624,-1.391031 0.1577864,-5.264035 0,0 0.2045452,-5.334173 0.2268599,-1.304447 -0.081925,0.579757 0,0 L 7.6279841,9.5902123 C 6.5503984,9.199511 6.034985,7.878739 6.034985,6.9839006 c 0,-0.037808 0.00315,-0.1071252 0.00944,-0.2079516 L 6.1733048,2.8909713 h 0.4253606 l 0.022376,3.724287 c -0.0063,0.1386343 0.051989,0.875934 0.1748671,1.0429299 0.122883,0.1669912 0.2536416,0.2504868 0.3922759,0.2504868 0.1512399,0 0.2946042,-0.086648 0.4300929,-0.2599445 0.1354855,-0.1732963 0.2032283,-0.920049 0.2032283,-1.071289 V 2.8909713 h 0.5387972 v 3.6864702 c 0,0.15124 0.067742,0.8979927 0.2032267,1.071289 0.135484,0.1732963 0.278846,0.2599445 0.430086,0.2599445 0.1386387,0 0.2693993,-0.083496 0.392282,-0.2504868 0.1228833,-0.1669959 0.1811723,-0.9042956 0.174867,-1.0429299 l 0.022377,-3.724287 h 0.4253607 l 0.128865,3.8849777 c 0.0063,0.1008267 0.0095,0.1701439 0.0095,0.2079516 z M 15.61982,11.955917 V 2.8342596 c 0,-0.3465927 -0.15124,-0.519889 -0.45372,-0.519889 -0.308785,0 -0.567155,0.2174069 -0.775109,0.6522206 -0.176447,0.3591938 -0.26467,0.7152373 -0.26467,1.0681306 v 6.7585362 c 0,1.22252 0.242614,1.83378 0.727842,1.83378 0.151239,0 0.316657,-0.07404 0.496254,-0.222127 0.179602,-0.14809 0.269403,-0.297755 0.269403,-0.448994 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 16.645728,12.656988 q 0,-2.292227 -1.640349,-3.9453573 -1.640348,-1.6531303 -3.949617,-1.6531303 -1.4912262,0 -2.7949835,0.7498736 Q 6.9570211,8.5582475 6.2114082,9.8620048 5.4657953,11.165762 5.4657953,12.656988 v 5.061646 H 8.9254391 L 7.5620327,19.883042 H 2.9094083 q -0.7754373,0 -1.3378425,-0.562405 Q 1.0091607,18.758232 1.0091607,17.982794 V 9.308121 Q 2.9946212,5.8740411 5.5680508,4.0206606 8.1414804,2.16728 11.055762,2.16728 q 2.871674,0 5.423801,1.8107741 2.552126,1.8107741 4.631321,5.3300669 v 8.674673 q 0,0.775438 -0.562405,1.337843 -0.562405,0.562405 -1.337843,0.562405 H 14.54949 l -1.354885,-2.164408 h 3.451123 z m -3.63007,-3.6215483 q 0.673182,0 1.146114,0.4814529 0.472931,0.4814529 0.472931,1.1461134 v 4.465156 q 0,0.630576 -0.417543,1.086465 -0.417543,0.455889 -1.031076,0.524059 v 0.809522 q 0,0.144862 -0.102255,0.247118 -0.102256,0.102255 -0.255639,0.102255 -0.144862,0 -0.247118,-0.102255 -0.102255,-0.102256 -0.102255,-0.247118 v -0.79248 H 9.6156636 v 0.79248 q 0,0.144862 -0.1065162,0.247118 -0.1065161,0.102255 -0.251378,0.102255 -0.1448619,0 -0.2471174,-0.102255 Q 8.9083965,17.69307 8.9083965,17.548208 V 16.738686 Q 8.2948636,16.670516 7.8773204,16.214627 7.4597772,15.758738 7.4597772,15.128162 v -4.465156 q 0,-0.6731818 0.4814529,-1.150374 Q 8.422683,9.0354397 9.0873436,9.0354397 Z m 0.903257,1.4230553 q 0,-0.357894 -0.255639,-0.6177934 Q 13.407638,9.5808023 13.049743,9.5808023 H 9.0532584 q -0.3664154,0 -0.6220541,0.2556387 -0.2556387,0.255639 -0.2556387,0.622054 v 1.099247 q 0,0.366415 0.2556387,0.622054 0.2556387,0.255638 0.6220541,0.255638 h 3.9964846 q 0.357895,0 0.613533,-0.259899 0.255639,-0.259899 0.255639,-0.617793 z m -0.579448,9.424547 H 8.7720559 L 9.65827,17.718634 h 2.794983 z M 9.1981204,14.71914 q -0.1704258,-0.170426 -0.4175432,-0.170426 -0.2471175,0 -0.4175433,0.170426 -0.1704258,0.170426 -0.1704258,0.417543 0,0.247118 0.1704258,0.417544 0.1704258,0.170425 0.4175433,0.170425 0.2471174,0 0.4175432,-0.170425 0.1704258,-0.170426 0.1704258,-0.417544 0,-0.247117 -0.1704258,-0.417543 z m 4.5333266,0 q -0.170426,-0.170426 -0.417544,-0.170426 -0.247117,0 -0.417543,0.170426 -0.170426,0.170426 -0.170426,0.417543 0,0.247118 0.170426,0.417544 0.170426,0.170425 0.417543,0.170425 0.247118,0 0.417544,-0.170425 0.170425,-0.170426 0.170425,-0.417544 0,-0.247117 -0.170425,-0.417543 z"
id="path4751" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 14.514282,4.8066299 q 1.183528,0 2.037358,0.8538307 0.853831,0.8538308 0.853831,2.0373586 v 7.9549978 q 0,1.09899 -0.739705,1.927459 -0.739705,0.82847 -1.838695,0.946823 v 1.428687 q 0,0.262067 -0.181756,0.443823 -0.181756,0.181756 -0.443823,0.181756 -0.262067,0 -0.44805,-0.181756 -0.185983,-0.181756 -0.185983,-0.443823 v -1.41178 H 8.4698362 v 1.41178 q 0,0.262067 -0.1817561,0.443823 -0.181756,0.181756 -0.4438229,0.181756 -0.2620669,0 -0.4438229,-0.181756 Q 7.2186782,20.217853 7.2186782,19.955786 V 18.527099 Q 6.1196881,18.408746 5.3757563,17.584503 4.6318246,16.760261 4.6318246,15.652817 V 7.6978192 q 0,-1.1835278 0.8538307,-2.0373586 Q 6.3394861,4.8066299 7.5230139,4.8066299 H 8.622004 V 2.6171035 Q 8.3007608,2.7354562 8.1063241,2.9510274 7.9118873,3.1665985 7.9118873,3.2173211 q -0.06763,0.1352603 -0.1902098,0.2071174 -0.1225797,0.071857 -0.2662937,0.071857 -0.2197981,0 -0.3592853,-0.1521678 -0.1394872,-0.1521679 -0.1394872,-0.3381508 0,-0.109899 0.050723,-0.2113443 L 7.1341405,2.5748346 Q 7.3539385,2.2028687 7.8738454,1.8689448 8.3937522,1.5350209 9.1207764,1.5350209 h 3.8041966 q 0.710117,0 1.23425,0.3339239 0.524134,0.3339239 0.752386,0.7058898 l 0.126806,0.219798 q 0.04227,0.109899 0.04227,0.2028905 0,0.1944367 -0.139487,0.3508315 -0.139487,0.1563947 -0.359285,0.1563947 -0.143714,0 -0.266294,-0.076084 -0.12258,-0.076084 -0.19021,-0.2113443 0,-0.042269 -0.19021,-0.2620669 -0.190209,-0.219798 -0.519906,-0.3381507 v 2.1895264 z m -4.9031869,0 H 12.426201 V 2.5325657 H 9.6110951 Z M 7.7216775,14.917339 q -0.3085626,-0.304336 -0.7312511,-0.304336 -0.439596,0 -0.7439318,0.304336 -0.3043357,0.304336 -0.3043357,0.743932 0,0.448049 0.3170164,0.748158 0.3170164,0.300109 0.7312511,0.300109 0.405781,0 0.7227973,-0.300109 0.3170164,-0.300109 0.3170164,-0.748158 0,-0.439596 -0.3085626,-0.743932 z m 8.0775775,0 q -0.304336,-0.304336 -0.743932,-0.304336 -0.44805,0 -0.748159,0.317016 -0.300108,0.317017 -0.300108,0.731252 0,0.431142 0.308562,0.739704 0.308563,0.308563 0.739705,0.308563 0.414235,0 0.731251,-0.300109 0.317016,-0.300109 0.317016,-0.748158 0,-0.439596 -0.304335,-0.743932 z m 0.338151,-7.5914857 q 0,-0.6340327 -0.460731,-1.0947632 -0.46073,-0.4607304 -1.094763,-0.4607304 H 7.4722913 q -0.6424865,0 -1.103217,0.4565035 -0.4607305,0.4565036 -0.4607305,1.0989901 v 3.6942977 q 0,0.642486 0.4607305,1.103217 0.4607305,0.46073 1.103217,0.46073 h 7.1096207 q 0.642486,0 1.09899,-0.46073 0.456504,-0.460731 0.456504,-1.103217 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
d="M 18.270753,3.2659119 14.646412,14.864869 c -0.01672,0.06553 -0.921877,1.686021 -2.174043,1.681772 l -7.3329699,0.01812 c -1.238273,-0.413166 -1.229256,-1.466354 -1.229256,-1.466354 0,0 -0.05503,-1.219046 1.286042,-1.57108 l 6.2575869,0.02125 c 0.289281,-0.142682 0.285418,-0.063 0.426871,-0.415318 L 15.188634,2.4779275 c 0.04419,-0.1814692 0.444638,-1.3733887 1.996173,-1.0635157 1.323434,0.432045 1.134602,1.7542909 1.085946,1.8515001 z"
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
d="m 12.511479,9.6326174 -4.2465189,-0.0022 c -0.436543,0.00592 -1.180203,0.2731972 -1.278698,1.1330156 -0.09676,0.844716 0.04306,0.825819 0.04306,0.825819 0.0022,0.01975 0.04841,0.126554 0.223252,0.212145 l 4.5915099,-3.04e-4 z"
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
d="m 12.965467,17.031896 -0.908629,-0.02576 1.001921,2.921594 c 0.05194,0.122275 -0.05837,0.16911 -0.12924,0.174385 l -7.6249769,-0.01411 c -0.07821,-0.01611 -0.111476,-0.103479 -0.06521,-0.192766 l 1.015412,-2.847699 -0.931814,-0.01364 -0.945272,2.697325 c -0.02203,0.614614 0.175621,0.814271 0.175621,0.814271 0,0 0.17226,0.261324 0.538924,0.3735 l 7.9892399,0.0018 c 0.28247,0.01106 0.615134,-0.35466 0.615134,-0.35466 0.04515,-0.01943 0.301887,-0.441164 0.245863,-0.719598 z"
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 5.0443854,17.825429 q -0.5100278,-0.505261 -0.5100278,-1.220253 0,-0.705459 0.5052612,-1.215487 0.5052612,-0.510028 1.2297867,-0.510028 0.7245255,0 1.22502,0.519561 0.5004946,0.519561 0.5004946,1.205954 0,0.695925 -0.5052612,1.21072 -0.5052612,0.514794 -1.2202534,0.514794 -0.7149923,0 -1.2250201,-0.505261 z m 9.6619286,0 q -0.505261,-0.505261 -0.505261,-1.220253 0,-0.705459 0.505261,-1.215487 0.505261,-0.510028 1.220253,-0.510028 0.714993,0 1.220254,0.505261 0.505261,0.505261 0.505261,1.220254 0,0.714992 -0.510028,1.220253 -0.510027,0.505261 -1.215487,0.505261 -0.714992,0 -1.220253,-0.505261 z m 6.282399,-5.071678 q 0,0.705459 -0.257398,1.677848 -0.257397,0.97239 -0.891357,1.572983 -0.633959,0.600594 -1.625415,0.600594 0,-0.934257 -0.67686,-1.611116 -0.676859,-0.67686 -1.611116,-0.67686 -0.943789,0 -1.615882,0.67686 -0.672093,0.676859 -0.672093,1.611116 H 8.5478475 q 0,-0.924724 -0.6673261,-1.60635 Q 7.2131953,14.3172 6.2694055,14.3172 q -0.9437898,0 -1.6206491,0.67686 -0.6768593,0.676859 -0.6768593,1.611116 H 3.7144999 q -1.2583864,0 -2.0639443,-0.69116 -0.80555798,-0.691159 -0.80555798,-1.940012 0,-0.667326 0.24309738,-1.415685 0.2430973,-0.748358 0.9199567,-1.244086 L 2.4561135,10.999636 Q 2.9899744,10.62784 3.2330717,10.341844 3.4761691,10.055847 3.7335663,9.6363845 L 4.6296899,8.1968668 Q 5.6592788,6.5380847 6.3933375,5.6753274 7.1273962,4.8125701 7.6421906,4.5885392 8.156985,4.3645083 9.7776341,4.1929101 11.398283,4.021312 13.609993,4.021312 q 2.068711,0 3.613094,0.2430974 1.544383,0.2430973 2.049644,0.643493 0.505261,0.4003957 0.867524,1.2488531 0.362263,0.8484575 0.60536,3.1030664 0.243098,2.2546091 0.243098,3.4939291 z M 8.7194456,9.4361867 q 0,-0.2001979 0.00953,-0.433762 0.00953,-0.2335641 0.00953,-0.4623617 0,-0.7531251 -0.066733,-1.015289 Q 8.6050468,7.2626102 8.4715816,7.1625113 8.3381164,7.0624124 7.8423884,7.0624124 q -1.2011869,0 -1.4967171,0.3527295 Q 6.0501412,7.7678714 5.482914,8.9309255 4.9156868,10.09398 4.9156868,10.322777 q 0,0.162065 0.1048656,0.26693 0.1048655,0.104866 0.2955301,0.104866 h 2.859969 q 0.3050634,0 0.4146955,-0.214498 0.1096321,-0.214497 0.1096321,-0.490961 z M 12.718636,7.1529781 Q 12.313473,7.005213 10.683291,7.005213 q -0.9342566,0 -0.9866893,0.9485564 -0.052433,0.9485564 -0.052433,1.5491499 0,0.8579907 0.1382318,1.0152887 0.1382319,0.157299 1.2250205,0.157299 0.305063,0 0.571994,-0.0095 h 0.409928 l 0.276464,0.0095 q 0.695926,0 0.776958,-0.471895 0.08103,-0.4718952 0.08103,-1.3680188 0,-1.5348501 -0.405162,-1.6826151 z m 6.763826,0.9294899 q -0.181131,-0.8961236 -0.91519,-0.9771561 -0.734059,-0.081032 -2.116377,-0.081032 -0.381329,0 -1.034355,0.00953 -0.653027,0.00953 -0.643493,0.00953 -0.638727,0 -0.719759,0.3002968 -0.08103,0.3002967 -0.08103,1.4919505 0,1.6778488 0.300297,1.8017808 0.300297,0.123932 1.19642,0.123932 0.371796,0 0.800792,-0.0095 l 1.468117,-0.03813 q 0.285997,0 0.400396,-0.0095 0.953323,0 1.23932,-0.176365 0.285997,-0.176364 0.285997,-0.5672267 0,-0.9819227 -0.181132,-1.8780463 z"
id="path4751" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="22" height="22" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color:#4d4d4d; }
</style>
<g transform="matrix(1.3102432,0,0,1.3102432,1.2193104,1.1894719)">
<path d="m 5.1443418,7.3044641 c 0,-0.528773 -0.0081,-0.8023148 0.1015991,-1.2374509 C 5.3614143,5.6373851 5.5519455,5.2463134 5.8175344,4.8993061 6.071576,4.5688229 6.3991096,4.3125374 6.7051142,4.1307717 7.1671034,3.949414 7.1363263,3.9549633 7.6766568,3.8968406 L 13.845266,3.8729476 V 2.8539579 H 15 V 12.272727 H 13.845266 V 9.6013219 H 1.2124711 V 12.272727 H 0 V 2.7272727 h 1.1374134 v 0.837224 c 0.2078522,0 0.4272517,0.016524 0.6581986,0.060589 0.2309469,0.044065 0.4618938,0.1101611 0.6928407,0.1982899 0.2309468,0.088129 0.4503464,0.203798 0.6466512,0.3414993 0.1963049,0.1321932 0.3579677,0.2919267 0.4849885,0.4681844 0.2598152,0.3580234 0.4445727,0.7601112 0.5600462,1.2117715 0.1212471,0.4461522 0.1789838,0.9308608 0.1789838,1.4596338 z m -4.0069284,0 h 2.2748268 c 0,-0.4516602 -0.040416,-0.842732 -0.1212471,-1.1732151 C 3.204388,5.8007659 3.0715935,5.497823 2.8983834,5.2114043 2.7020785,4.9139695 2.4364896,4.7046635 2.1016166,4.5889944 1.76097,4.4678172 1.443418,4.4072287 1.1374134,4.4072287 Z M 7.9306984,4.7560967 C 7.2845735,4.7638814 6.8853796,4.9041689 6.5505066,5.3833694 6.3657491,5.653264 6.2216915,6.0174407 6.1408601,6.3424158 6.0658023,6.6673908 6.091224,6.8528039 6.091224,7.3044641 h 7.754042 V 4.7322037 Z" style="fill:currentColor;stroke-width:0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="stroke-width:1px;fill:none;stroke:currentColor;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" class="ColorScheme-Text" d="m 3.3896829,19 v -4 h 4 V 11 H 11.389683 V 7 h 4 V 3 h 4"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" >
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 19.635166,16.020953 q -1.345816,2.314593 -3.649935,3.634225 -2.304119,1.319632 -4.943383,1.319632 -2.6497377,0 -4.9643304,-1.330106 -2.3145926,-1.330105 -3.6394613,-3.644698 -1.3248686,-2.314592 -1.3248686,-4.96433 0,-2.6392637 1.3301053,-4.9538563 1.3301052,-2.3145927 3.6446979,-3.644698 2.3145927,-1.3301053 4.9538571,-1.3301053 2.62879,0 4.938146,1.319632 2.309356,1.319632 3.655172,3.6289881 1.345815,2.309356 1.345815,4.9800395 0,2.670684 -1.345815,4.985277 z M 16.540314,7.7784894 q 0,-1.0368537 -0.733129,-1.7595094 Q 15.074057,5.2963244 14.058149,5.2963244 H 8.0360191 q -1.0368537,0 -1.764746,0.7331289 -0.7278922,0.7331289 -0.7278922,1.7490361 v 6.8390456 q 0,0.974014 0.6545794,1.686196 0.6545793,0.712183 1.5657538,0.795969 v 1.068273 q 0,0.219939 0.1623357,0.371801 0.1623357,0.151863 0.3927476,0.151863 0.2199387,0 0.3718011,-0.151863 0.1518624,-0.151862 0.1518624,-0.371801 v -1.047327 h 4.3883001 v 1.047327 q 0,0.219939 0.162336,0.371801 0.162335,0.151863 0.382274,0.151863 0.230412,0 0.382274,-0.151863 0.151863,-0.151862 0.151863,-0.371801 V 17.0997 q 0.911174,-0.08379 1.57099,-0.795969 0.659816,-0.712182 0.659816,-1.686196 z M 14.110516,6.1132395 q 0.54461,0 0.937357,0.3927476 0.392748,0.3927476 0.392748,0.9583042 v 1.6861965 q 0,0.5655565 -0.392748,0.9583042 -0.392747,0.392748 -0.937357,0.392748 H 7.9836528 q -0.5655566,0 -0.9478309,-0.397985 Q 6.6535475,9.7055711 6.6535475,9.1504878 V 7.4642913 q 0,-0.5550833 0.3822744,-0.9530676 Q 7.4180962,6.1132395 7.9836528,6.1132395 Z M 8.2088281,15.261641 q -0.2670684,0.277541 -0.6441061,0.277541 -0.3665645,0 -0.6336328,-0.267068 -0.2670684,-0.267068 -0.2670684,-0.633633 0,-0.377037 0.2775416,-0.644106 0.2775417,-0.267068 0.6231596,-0.267068 0.3770377,0 0.6441061,0.267068 0.2670684,0.267069 0.2670684,0.644106 0,0.345618 -0.2670684,0.62316 z m 6.9437779,0 q -0.267068,0.277541 -0.644106,0.277541 -0.356091,0 -0.628396,-0.267068 -0.272305,-0.267068 -0.272305,-0.633633 0,-0.377037 0.261832,-0.644106 0.261831,-0.267068 0.638869,-0.267068 0.377038,0 0.644106,0.267068 0.267068,0.267069 0.267068,0.644106 0,0.345618 -0.267068,0.62316 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 19.256782,10.803117 q 0.700505,0.155667 1.161021,0.609698 0.460517,0.45403 0.460517,1.024811 v 4.527333 q 0,0.298363 -0.214043,0.512406 -0.214043,0.214043 -0.512406,0.214043 h -0.921033 v 1.660454 q 0,0.596726 -0.415114,1.018326 -0.415113,0.4216 -1.011839,0.4216 -0.596726,0 -1.01184,-0.4216 -0.415113,-0.4216 -0.415113,-1.018326 V 17.691408 H 5.8304519 v 1.660454 q 0,0.596726 -0.4151136,1.018326 -0.4151136,0.4216 -1.0118394,0.4216 -0.6096981,0 -1.0248117,-0.428086 -0.4151136,-0.428086 -0.4151136,-1.01184 V 17.691408 H 1.9517342 q -0.2983629,0 -0.5124058,-0.214043 -0.214043,-0.214043 -0.214043,-0.512406 v -4.527333 q 0,-0.570781 0.4605167,-1.024811 0.4605166,-0.454031 1.1739931,-0.609698 L 4.2737759,5.6012243 Q 4.4294435,5.0174708 4.967794,4.6023572 5.5061444,4.1872436 6.1288148,4.1872436 h 1.9328727 l -0.012972,-0.090806 q 0,-0.038917 -0.012972,-0.090806 v -1.945845 q 0,-0.2983629 0.214043,-0.5124059 0.2140429,-0.2140429 0.5124058,-0.2140429 h 4.5792227 q 0.298363,0 0.512405,0.2140429 0.214043,0.214043 0.214043,0.5124059 v 1.945845 q 0,0.1167507 -0.02594,0.1816122 h 1.945845 q 0.62267,0 1.161021,0.4151136 0.53835,0.4151136 0.694018,0.9988671 z M 12.809549,3.5386286 q 0,0.025945 0.02595,0.025945 h 0.324307 q 0.02595,0 0.02595,-0.025945 V 2.2284263 q 0,-0.025945 -0.02595,-0.025945 h -0.324307 q -0.02595,0 -0.02595,0.025945 z m -1.478842,-0.012972 q 0,0.025945 0.01297,0.038917 h 0.389169 l 0.02595,-0.012972 0.220529,-0.3372798 0.233502,0.3372798 0.02594,0.012972 h 0.376197 q 0.02594,0 0.02594,-0.038917 l -0.42808,-0.635643 0.415113,-0.648615 V 2.215454 L 12.614967,2.202482 H 12.25174 L 12.2258,2.215454 11.979326,2.5657061 11.745825,2.215454 11.719875,2.202482 h -0.363224 q -0.03892,0 -0.03892,0.012972 l 0.01297,0.025945 0.428086,0.648615 z M 10.396702,2.2024817 q -0.02595,0 -0.02595,0.012972 L 9.8778095,3.5256563 q 0,0.038917 0.051889,0.038917 h 0.2983635 l 0.03892,-0.038917 0.06486,-0.2075568 h 0.428086 l 0.07783,0.2075568 q 0,0.038917 0.03892,0.038917 h 0.311335 q 0.02594,0 0.02594,-0.038917 L 10.746954,2.2413986 q 0,-0.038917 -0.03892,-0.038917 z m 0.28539,0.8431995 h -0.259446 l 0.129723,-0.389169 z M 8.9308316,2.4619277 q 0,0.025945 0.038917,0.025945 H 9.2810838 V 3.538629 q 0,0.025945 0.038917,0.025945 h 0.2853906 q 0.025945,0 0.025945,-0.025945 V 2.4878723 h 0.3243075 q 0.025945,0 0.025945,-0.025945 V 2.215454 q 0,-0.025945 -0.025945,-0.025945 H 8.9697485 q -0.038917,0 -0.038917,0.025945 z M 5.4412829,13.014894 q -0.4151136,-0.395655 -0.9729225,-0.395655 -0.5318643,0 -0.9469779,0.389169 -0.4151136,0.389169 -0.4151136,0.972922 0,0.596726 0.4086275,0.985895 0.4086274,0.389169 0.953464,0.389169 0.5578089,0 0.9729225,-0.402141 0.4151136,-0.402142 0.4151136,-0.972923 0,-0.570781 -0.4151136,-0.966436 z M 16.662322,10.686366 q 0.259446,0 0.428086,-0.162154 0.16864,-0.162154 0.16864,-0.408627 0,-0.103779 -0.01297,-0.1556679 L 16.441793,6.2109224 Q 16.376932,5.9125595 16.117486,5.6985165 15.85804,5.4844736 15.546705,5.4844736 H 6.5569007 q -0.3113352,0 -0.5707812,0.2140429 -0.259446,0.214043 -0.3243075,0.5124059 L 4.8575294,9.9599171 q 0,0.051889 -0.012972,0.1426949 0,0.259446 0.1686399,0.4216 0.1686399,0.162154 0.4280859,0.162154 z m 2.127458,2.328528 q -0.415114,-0.395655 -0.972923,-0.395655 -0.544836,0 -0.95995,0.402141 -0.415114,0.402141 -0.415114,0.95995 0,0.570781 0.415114,0.972923 0.415114,0.402141 0.95995,0.402141 0.557809,0 0.972923,-0.402141 0.415113,-0.402142 0.415113,-0.972923 0,-0.570781 -0.415113,-0.966436 z"
id="path4751" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 15.098099,1.8668609 q 1.399482,0 2.400501,1.0010189 1.001019,1.0010189 1.001019,2.4005016 v 9.3687596 q 0,1.302296 -0.879536,2.2693 -0.879536,0.967004 -2.152676,1.103064 v 1.671605 q 0,0.310996 -0.21867,0.529665 -0.218669,0.218669 -0.519946,0.218669 -0.310996,0 -0.529666,-0.218669 -0.218669,-0.218669 -0.218669,-0.529665 V 18.028943 H 7.9937802 v 1.652167 q 0,0.310996 -0.2186692,0.529665 -0.2186692,0.218669 -0.5296653,0.218669 -0.3109962,0 -0.5248061,-0.218669 Q 6.5068298,19.992106 6.5068298,19.68111 V 18.009505 Q 5.214252,17.873445 4.3444346,16.906441 3.4746172,15.939437 3.4746172,14.637141 V 5.2683814 q 0,-1.3994827 1.0010189,-2.4005016 Q 5.476655,1.8668609 6.8761377,1.8668609 Z M 7.1093848,13.757605 q -0.3693079,-0.35473 -0.864958,-0.35473 -0.5150874,0 -0.8746767,0.359589 -0.3595893,0.359589 -0.3595893,0.874677 0,0.505368 0.3595893,0.869817 0.3595893,0.364449 0.8746767,0.364449 0.4956501,0 0.864958,-0.35959 0.369308,-0.359589 0.369308,-0.874676 0,-0.524806 -0.369308,-0.879536 z m 9.4902422,0.0049 q -0.364449,-0.359589 -0.860099,-0.359589 -0.515087,0 -0.874676,0.359589 -0.35959,0.359589 -0.35959,0.874677 0,0.505368 0.35959,0.869817 0.359589,0.364449 0.874676,0.364449 0.505369,0 0.864958,-0.364449 0.35959,-0.364449 0.35959,-0.869817 0,-0.515088 -0.364449,-0.874677 z m 0.393605,-8.9314209 q 0,-0.7386159 -0.544244,-1.2828592 Q 15.904745,3.0039406 15.166129,3.0039406 H 6.8081073 q -0.7483345,0 -1.2925778,0.539384 -0.5442433,0.539384 -0.5442433,1.2877185 v 2.3227525 q 0,0.7483345 0.5442433,1.2925778 0.5442433,0.5442433 1.2925778,0.5442433 h 8.3580217 q 0.748335,0 1.287719,-0.5442433 0.539384,-0.5442433 0.539384,-1.2925778 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 20.794826,13.179229 q 0.0173,0.06921 0.02595,0.147079 0.0087,0.07787 0.0087,0.164383 0,0.164383 -0.02596,0.328765 -0.02595,0.164383 -0.06921,0.285507 l -0.5018,1.444837 q 0.147079,0.05191 0.272529,0.173035 0.12545,0.121124 0.12545,0.276855 v 0.917082 q 0,0.19899 -0.09517,0.337417 -0.09517,0.138428 -0.294158,0.138428 H 18.3983 q -0.173035,0.2509 -0.441238,0.402305 -0.268203,0.151405 -0.596969,0.151405 -0.328765,0 -0.601294,-0.151405 -0.27253,-0.151405 -0.436912,-0.402305 h -0.432586 q -0.173035,0.2509 -0.445564,0.402305 -0.272529,0.151405 -0.592643,0.151405 -0.328765,0 -0.601294,-0.151405 -0.272529,-0.151405 -0.445564,-0.402305 H 8.2152252 q -0.1730344,0.2509 -0.4412378,0.402305 -0.2682033,0.151405 -0.5969686,0.151405 -0.3287654,0 -0.5969687,-0.151405 -0.2682033,-0.151405 -0.4412377,-0.402305 h -0.432586 q -0.1730344,0.2509 -0.4455636,0.402305 -0.2725292,0.151405 -0.5926428,0.151405 -0.3287654,0 -0.6012946,-0.151405 Q 3.7941962,17.643517 3.6211618,17.392617 H 1.7264352 q -0.1989896,0 -0.3157878,-0.138428 -0.1167982,-0.138427 -0.1167982,-0.337417 v -0.90843 q 0,-0.19899 0.1470792,-0.333092 0.1470792,-0.134101 0.3374171,-0.151405 L 1.2765457,14.104963 q -0.043259,-0.121124 -0.064888,-0.276855 -0.021629,-0.155731 -0.021629,-0.311462 0,-0.09517 0.00865,-0.181686 0.00865,-0.08652 0.025955,-0.155731 L 2.0638522,9.3119102 Q 2.2455384,8.4986486 2.8944174,7.9016799 3.5432964,7.3047112 4.356558,7.3047112 h 7.84711 L 13.337043,6.1799876 11.892206,4.7264986 H 10.568493 V 3.9651473 q 0,-0.1989896 0.142753,-0.341743 Q 10.854,3.480651 11.052989,3.480651 h 3.028102 q 0.19899,0 0.341743,0.1427533 0.142754,0.1427534 0.142754,0.341743 v 0.7613513 h -1.237196 l 1.090116,1.0901168 q 0.14708,0.1643826 0.14708,0.3633722 0,0.2076413 -0.14708,0.3547205 l -0.770003,0.7700031 h 4.014398 q 0.813262,0 1.462141,0.5969687 0.648879,0.5969687 0.830565,1.4102303 z M 14.67806,9.225393 q 0,-0.1989895 -0.147079,-0.337417 Q 14.383902,8.7495484 14.17626,8.7495484 h -2.119671 q -0.19899,0 -0.350395,0.1384276 -0.151405,0.1384275 -0.151405,0.337417 v 3.322261 q 0,0.198989 0.151405,0.341742 0.151405,0.142754 0.350395,0.142754 h 2.119671 q 0.207642,0 0.354721,-0.142754 0.147079,-0.142753 0.147079,-0.341742 z m -4.222039,0 q 0,-0.1989895 -0.14708,-0.337417 Q 10.161862,8.7495484 9.9542209,8.7495484 H 7.8345495 q -0.1989896,0 -0.3503947,0.1384276 -0.1514051,0.1384275 -0.1514051,0.337417 v 3.322261 q 0,0.198989 0.1514051,0.341742 0.1514051,0.142754 0.3503947,0.142754 h 2.1196714 q 0.2076411,0 0.3547201,-0.142754 0.14708,-0.142753 0.14708,-0.341742 z m 8.279696,3.806757 q 0.181686,0 0.30281,-0.112473 0.121124,-0.112472 0.121124,-0.276855 0,-0.06056 -0.0087,-0.08652 L 18.554031,9.5714618 Q 18.467513,9.1561793 18.203636,8.9355604 17.939758,8.7149416 17.610993,8.7149416 h -1.332365 q -0.198989,0 -0.350394,0.1427533 -0.151406,0.1427534 -0.151406,0.341743 v 3.3482161 q 0,0.198989 0.151406,0.341742 0.151405,0.142754 0.350394,0.142754 z M 6.2339813,9.1994379 q 0,-0.1989896 -0.1470793,-0.341743 Q 5.9398228,8.7149416 5.7321815,8.7149416 H 4.4084684 q -0.3287654,0 -0.588317,0.2206188 Q 3.5605998,9.1561793 3.4740826,9.5714618 L 2.8684622,12.556305 q 0,0.02595 -0.00865,0.08652 0,0.164383 0.1211241,0.276855 0.121124,0.112473 0.3028102,0.112473 h 2.4484367 q 0.2076413,0 0.3547205,-0.142754 0.1470793,-0.142753 0.1470793,-0.341742 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 1.9348413,12.612438 q -0.2922305,-0.838985 -0.2922305,-1.725103 0,-2.149308 1.5507069,-3.700015 1.550707,-1.550707 3.7000151,-1.550707 H 16.178721 V 2.7331617 l 4.835943,3.9215447 -4.835943,3.9026906 V 7.4559835 H 6.8933328 q -1.3951649,0 -2.4132582,1.0086666 -1.0180933,1.0086665 -1.0180933,2.4226849 0,0.103695 0.00943,0.197963 l 0.00943,0.207389 z M 20.185107,9.275354 q 0.29223,0.857838 0.29223,1.715676 0,2.149308 -1.54128,3.700015 -1.54128,1.550707 -3.709442,1.550707 H 5.941227 v 2.903451 L 1.1147105,15.233085 5.941227,11.320967 v 3.110841 h 9.285388 q 1.395165,0 2.413258,-1.008666 1.018094,-1.008667 1.018094,-2.432112 0,-0.188536 -0.02828,-0.405352 z"
id="path4751" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 16.575268,17.736668 q 0.345137,0.08026 0.565864,0.349151 0.220728,0.268886 0.220728,0.630076 0,0.417375 -0.288953,0.706328 -0.288952,0.288952 -0.706327,0.288952 H 5.6592955 q -0.4173753,0 -0.7103408,-0.296979 -0.2929654,-0.296979 -0.2929654,-0.698301 0,-0.36119 0.2167142,-0.62205 0.2167141,-0.26086 0.553825,-0.341124 0.056185,-2.303591 0.7906053,-3.648022 0.7344202,-1.34443 1.9343745,-1.950427 1.1999542,-0.605997 1.3042981,-0.826724 0.1043438,-0.220728 0.1043438,-0.381257 0,-0.561851 -0.8548169,-0.927055 Q 7.8505164,9.6540329 7.1040565,8.9798111 6.3575967,8.3055893 5.9041023,7.0735292 5.4506078,5.8414691 5.418502,4.0355179 5.0894176,3.9552534 4.8727035,3.6943938 4.6559893,3.4335341 4.6559893,3.0723439 q 0,-0.4013225 0.2929654,-0.6983011 Q 5.2419202,2.0770641 5.6592955,2.0770641 H 16.36658 q 0.417375,0 0.706327,0.2889522 0.288953,0.2889522 0.288953,0.7063276 0,0.3531638 -0.220728,0.6260631 -0.220727,0.2728993 -0.565864,0.3371109 -0.03211,2.3356969 -0.7585,3.7001934 -0.726394,1.3644966 -1.954441,1.9905597 -1.228046,0.626063 -1.328377,0.850804 -0.100331,0.22474 -0.100331,0.369216 0,0.545799 0.862844,0.919029 0.862843,0.37323 1.59325,1.039425 0.730407,0.666195 1.183901,1.862137 0.453495,1.195941 0.501654,2.969786 z M 6.0525916,17.720615 h 9.8966124 q -0.0321,-1.420682 -0.345137,-2.436027 -0.313031,-1.015346 -0.882909,-1.665489 -0.569878,-0.650142 -1.74174,-1.207981 -1.171862,-0.557838 -1.171862,-1.464827 0,-0.9712001 1.372523,-1.5852235 1.372523,-0.6140234 2.046745,-1.8380571 0.674222,-1.2240336 0.72238,-3.4152545 H 6.0525916 q 0.032106,2.1671415 0.6862615,3.3831487 0.6541556,1.2160072 1.7698322,1.7337132 1.1156765,0.517706 1.3966023,0.8748832 0.2809254,0.357177 0.2809254,0.84679 0,0.955148 -1.3966019,1.577198 -1.3966024,0.62205 -2.0427316,1.866149 -0.6461292,1.2441 -0.6942879,3.330977 z m 8.9976504,-2.343723 q 0.128423,0.369216 0.208688,0.830737 0.08026,0.461521 0.120396,1.039426 h -8.74883 q 0.032106,-0.577905 0.1163835,-1.039426 0.084278,-0.461521 0.2127009,-0.830737 h 3.7804576 v -4.430601 q 0,-0.545798 -0.305005,-1.0313985 Q 10.130028,9.4292923 9.3755419,9.0440227 L 8.95014,8.8273086 Q 8.7334259,8.7149383 8.5046721,8.5744754 8.2759182,8.4340125 8.0431512,8.2413777 H 13.950618 Q 13.573375,8.5544092 12.413553,9.1764591 11.253731,9.798509 11.253731,10.946291 v 4.430601 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
d="m 16.09347,20.042218 q -0.585255,0.535733 -1.24254,0.535733 -0.630274,0 -0.985928,-0.616768 -0.355655,-0.616768 -0.814854,-1.346085 l -0.891387,-1.368594 q -0.06303,-0.09904 -0.355655,-0.684297 -0.292627,-0.585254 -0.742822,-0.585254 -0.108047,0 -0.180078,0.05402 l -0.162071,0.108047 q -0.765332,0.423184 -1.5846878,1.535167 -0.8193558,1.111983 -0.8193558,1.33708 0,0.09904 0.036016,0.189082 l 0.1350587,0.378165 q 0.027012,0.09904 0.027012,0.198086 0,0.333144 -0.2701173,0.57625 -0.2701173,0.243105 -0.5942581,0.243105 -0.5852541,0 -1.1525005,-0.612265 -0.5672463,-0.612266 -0.5672463,-1.170509 0,-0.747324 0.7338187,-1.481143 0.7338186,-0.733819 1.1479985,-1.328077 L 8.0259662,15.67082 q 0.2341017,-0.369161 0.3781643,-0.715811 0.1440625,-0.346651 0.1440625,-0.625772 0,-0.378164 -0.049521,-0.742822 -0.049522,-0.364659 -0.049522,-0.742823 0,-0.333145 0.036016,-0.495215 0.036016,-0.16207 0.090039,-0.25211 L 8.65624,11.970213 q 0.036016,-0.06303 0.067529,-0.130557 0.031514,-0.06753 0.031514,-0.139561 0,-0.07203 -0.040518,-0.11705 -0.040518,-0.04502 -0.094541,-0.04502 -0.2791212,0 -0.5222268,0.540235 -0.2431055,0.540234 -0.2431055,0.927402 0,0.08104 0.036016,0.175577 0.036016,0.09454 0.036016,0.544736 0,0.423184 -0.2656153,0.62127 -0.2656154,0.198086 -0.6347757,0.198086 -0.6302737,0 -0.9454105,-0.342149 -0.3151369,-0.342148 -0.3151369,-0.999434 0,-0.639277 0.405176,-1.539668 Q 6.5763367,10.763689 7.2876456,9.9443329 7.9989545,9.1249771 8.6292282,8.6252601 9.2595019,8.1255431 9.2595019,7.738375 q 0,-0.2611134 -0.3736623,-0.5942581 Q 8.5121774,6.8109723 8.2060444,6.131177 7.8999115,5.4513818 7.8999115,4.7760886 q 0,-1.2605474 0.9229008,-2.2149619 0.9229008,-0.9544144 2.2644837,-0.9544144 1.368594,0 2.205958,0.895889 0.837363,0.8958891 0.837363,2.1744443 0,0.3151368 -0.05853,0.6257717 -0.05853,0.3106349 -0.157569,0.6257718 -0.126054,-0.027012 -0.247607,-0.058525 -0.121553,-0.031514 -0.247608,-0.031514 -0.306133,0 -0.468203,0.040518 -0.16207,0.040518 -0.16207,0.1665723 0,0.1440626 0.157568,0.2521095 0.157568,0.1080469 0.364658,0.1800782 l 0.25211,0.081035 Q 13.44632,6.837984 13.243732,7.0675837 13.041144,7.2971834 12.879074,7.3512069 L 12.66298,7.441246 q -0.09904,0.04502 -0.153067,0.1215527 -0.05402,0.076533 -0.05402,0.2656154 0,0.3331447 0.270117,0.6347756 0.270118,0.301631 0.634776,0.8868852 0.364658,0.5852541 0.96792,1.3775981 0.603262,0.792344 0.931905,0.99043 0.328643,0.198086 0.558242,0.504219 0.2296,0.306133 0.2296,0.639278 0,0.36916 -0.288125,0.603262 -0.288125,0.234101 -0.648282,0.234101 -0.279121,0 -0.513222,-0.112548 -0.234102,-0.112549 -0.43669,-0.324141 -0.202588,-0.211592 -0.373662,-0.477207 -0.171075,-0.265616 -0.351153,-0.504219 -0.180078,-0.238604 -0.283623,-0.454698 -0.103545,-0.216094 -0.256611,-0.216094 -0.07203,0 -0.09454,0.08103 -0.02251,0.08104 -0.02251,0.180079 0,0.153066 0.02701,0.333144 0.02701,0.180079 0.02701,0.234102 0,0.171074 0.234102,1.251543 0.234102,1.08047 0.454697,1.530665 0.220596,0.450196 0.612266,1.355089 0.39167,0.904893 0.729317,1.256045 0.337647,0.351153 0.96792,0.351153 0.243106,0 0.544737,0.265615 0.301631,0.265615 0.301631,0.589756 0,0.468203 -0.585254,1.003936 z M 13.014132,5.0011863 q 0.171075,-0.1710742 0.171075,-0.4051759 0,-0.2431056 -0.171075,-0.4141799 -0.171074,-0.1710743 -0.414179,-0.1710743 -0.234102,0 -0.405176,0.1710743 -0.171075,0.1710743 -0.171075,0.4141799 0,0.2341017 0.171075,0.4051759 0.171074,0.1710743 0.405176,0.1710743 0.243105,0 0.414179,-0.1710743 z"
id="path4749" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,2 @@
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>

View File

@@ -0,0 +1,170 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "mediasizehelper.h"
int MediaSizeHelper::m_mediaMaxWidth = 0;
int MediaSizeHelper::m_mediaMaxHeight = 0;
MediaSizeHelper::MediaSizeHelper(QObject *parent)
: QObject(parent)
{
}
qreal MediaSizeHelper::contentMaxWidth() const
{
return m_contentMaxWidth;
}
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
{
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
m_contentMaxWidth = -1.0;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
return;
}
m_contentMaxWidth = contentMaxWidth;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::contentMaxHeight() const
{
return m_contentMaxHeight;
}
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
{
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
m_contentMaxHeight = -1.0;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
return;
}
m_contentMaxHeight = contentMaxHeight;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaWidth() const
{
return m_mediaWidth;
}
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
{
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
m_mediaWidth = -1.0;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
return;
}
m_mediaWidth = mediaWidth;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaHeight() const
{
return m_mediaHeight;
}
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
{
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
m_mediaHeight = -1.0;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
return;
}
m_mediaHeight = mediaHeight;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::resolvedMediaWidth() const
{
if (m_mediaWidth > 0.0) {
return m_mediaWidth;
}
return widthLimit();
}
qreal MediaSizeHelper::resolvedMediaHeight() const
{
if (m_mediaHeight > 0.0) {
return m_mediaHeight;
}
return widthLimit() / 16.0 * 9.0;
}
qreal MediaSizeHelper::aspectRatio() const
{
return resolvedMediaWidth() / resolvedMediaHeight();
}
bool MediaSizeHelper::limitWidth() const
{
// If actual data isn't available we'll be using a placeholder that is width
// limited so return true.
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
return true;
}
return m_mediaWidth >= m_mediaHeight;
}
qreal MediaSizeHelper::widthLimit() const
{
if (m_contentMaxWidth < 0.0) {
return m_mediaMaxWidth;
}
return std::min(m_contentMaxWidth, qreal(m_mediaMaxWidth));
}
qreal MediaSizeHelper::heightLimit() const
{
if (m_contentMaxHeight < 0.0) {
return m_mediaMaxHeight;
}
return std::min(m_contentMaxHeight, qreal(m_mediaMaxHeight));
}
QSize MediaSizeHelper::currentSize() const
{
if (limitWidth()) {
qreal width = std::min(widthLimit(), resolvedMediaWidth());
qreal height = width / aspectRatio();
if (height > heightLimit()) {
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
}
return QSize(qRound(width), qRound(height));
} else {
qreal height = std::min(heightLimit(), resolvedMediaHeight());
qreal width = height * aspectRatio();
if (width > widthLimit()) {
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
}
return QSize(qRound(width), qRound(height));
}
}
void MediaSizeHelper::setMaxSize(int width, int height)
{
MediaSizeHelper::m_mediaMaxWidth = width;
MediaSizeHelper::m_mediaMaxHeight = height;
}
#include "moc_mediasizehelper.cpp"

View File

@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QSize>
/**
* @class MediaSizeHelper
*
* A class to help calculate the current width of a media item within a chat delegate.
*
* The only realistic way to guarantee that a media item (e.g. an image or video)
* is the correct size in QML is to calculate the size manually.
*
* The rules for this component work as follows:
* - The output will always try to keep the media size if no limits are breached.
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
* calcualated from either the configured max width or the contentMaxWidth, whichever
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
* - The current size will never be larger than any of the limits in either direction.
* - If any limit is breached the image size will be reduced while maintaining aspect
* ration, i.e. no stretching or squashing. This can mean that the width or height
* is reduced even if that parameter doesn't breach the limit itself.
*/
class MediaSizeHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The maximum width (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
/**
* @brief The maximum height (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
/**
* @brief The base width (in px) of the media.
*/
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
/**
* @brief The base height (in px) of the media.
*/
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
/**
* @brief The size (in px) of the component based on the current input.
*
* Will always try to return a value even if some of the inputs are not set to
* account for being called before the parameters are intialised. For any parameters
* not set these will just be left out of the calcs.
*
* If no input values are provided a default placeholder value will be returned.
*/
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
public:
explicit MediaSizeHelper(QObject *parent = nullptr);
qreal contentMaxWidth() const;
void setContentMaxWidth(qreal contentMaxWidth);
qreal contentMaxHeight() const;
void setContentMaxHeight(qreal contentMaxHeight);
qreal mediaWidth() const;
void setMediaWidth(qreal mediaWidth);
qreal mediaHeight() const;
void setMediaHeight(qreal mediaHeight);
QSize currentSize() const;
static void setMaxSize(int width, int height);
Q_SIGNALS:
void contentMaxWidthChanged();
void contentMaxHeightChanged();
void mediaWidthChanged();
void mediaHeightChanged();
void currentSizeChanged();
private:
qreal m_contentMaxWidth = -1.0;
qreal m_contentMaxHeight = -1.0;
qreal m_mediaWidth = -1.0;
qreal m_mediaHeight = -1.0;
qreal resolvedMediaWidth() const;
qreal resolvedMediaHeight() const;
qreal aspectRatio() const;
bool limitWidth() const;
qreal widthLimit() const;
qreal heightLimit() const;
static int m_mediaMaxWidth;
static int m_mediaMaxHeight;
};

View File

@@ -0,0 +1,206 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "itinerarymodel.h"
#include <QJsonDocument>
#include <QProcess>
#include "config-neochat.h"
#ifndef Q_OS_ANDROID
#include <KIO/ApplicationLauncherJob>
#endif
using namespace Qt::StringLiterals;
ItineraryModel::ItineraryModel(QObject *parent)
: QAbstractListModel(parent)
{
}
QVariant ItineraryModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
auto row = index.row();
auto data = m_data[row];
if (role == NameRole) {
if (data["@type"_L1] == u"TrainReservation"_s) {
auto trainName = u"%1 %2"_s.arg(data["reservationFor"_L1]["trainName"_L1].toString(), data["reservationFor"_L1]["trainNumber"_L1].toString());
if (trainName.trimmed().isEmpty()) {
return u"%1 to %2"_s.arg(data["reservationFor"_L1]["departureStation"_L1]["name"_L1].toString(),
data["reservationFor"_L1]["arrivalStation"_L1]["name"_L1].toString());
;
}
return trainName;
}
if (data["@type"_L1] == u"LodgingReservation"_s) {
return data["reservationFor"_L1]["name"_L1];
}
if (data["@type"_L1] == u"FoodEstablishmentReservation"_s) {
return data["reservationFor"_L1]["name"_L1];
}
if (data["@type"_L1] == u"FlightReservation"_s) {
return u"%1 %2 %3 → %4"_s.arg(data["reservationFor"_L1]["airline"_L1]["iataCode"_L1].toString(),
data["reservationFor"_L1]["flightNumber"_L1].toString(),
data["reservationFor"_L1]["departureAirport"_L1]["iataCode"_L1].toString(),
data["reservationFor"_L1]["arrivalAirport"_L1]["iataCode"_L1].toString());
}
}
if (role == TypeRole) {
return data["@type"_L1];
}
if (role == DepartureLocationRole) {
if (data["@type"_L1] == u"TrainReservation"_s) {
return data["reservationFor"_L1]["departureStation"_L1]["name"_L1];
}
if (data["@type"_L1] == u"FlightReservation"_s) {
return data["reservationFor"_L1]["departureAirport"_L1]["iataCode"_L1];
}
}
if (role == DepartureAddressRole) {
if (data["@type"_L1] == u"TrainReservation"_s) {
return data["reservationFor"_L1]["departureStation"_L1]["address"_L1]["addressCountry"_L1].toString();
}
if (data["@type"_L1] == u"FlightReservation"_s) {
return data["reservationFor"_L1]["departureAirport"_L1]["address"_L1]["addressCountry"_L1].toString();
}
}
if (role == ArrivalLocationRole) {
if (data["@type"_L1] == u"TrainReservation"_s) {
return data["reservationFor"_L1]["arrivalStation"_L1]["name"_L1];
}
if (data["@type"_L1] == u"FlightReservation"_s) {
return data["reservationFor"_L1]["arrivalAirport"_L1]["iataCode"_L1];
}
}
if (role == ArrivalAddressRole) {
if (data["@type"_L1] == u"TrainReservation"_s) {
return data["reservationFor"_L1]["arrivalStation"_L1]["address"_L1]["addressCountry"_L1].toString();
}
if (data["@type"_L1] == u"FlightReservation"_s) {
return data["reservationFor"_L1]["arrivalAirport"_L1]["address"_L1]["addressCountry"_L1].toString();
}
}
if (role == DepartureTimeRole) {
const auto &time = data["reservationFor"_L1]["departureTime"_L1];
auto dateTime = (time.isString() ? time : time["@value"_L1]).toVariant().toDateTime();
if (const auto &timeZone = time["timezone"_L1].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == ArrivalTimeRole) {
const auto &time = data["reservationFor"_L1]["arrivalTime"_L1];
auto dateTime = (time.isString() ? time : time["@value"_L1]).toVariant().toDateTime();
if (const auto &timeZone = time["timezone"_L1].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == AddressRole) {
const auto &addressData = data["reservationFor"_L1]["address"_L1];
return u"%1 - %2 %3 %4"_s.arg(addressData["streetAddress"_L1].toString(),
addressData["postalCode"_L1].toString(),
addressData["addressLocality"_L1].toString(),
addressData["addressCountry"_L1].toString());
}
if (role == StartTimeRole) {
QDateTime dateTime;
if (data["@type"_L1] == u"LodgingReservation"_s) {
dateTime = data["checkinTime"_L1]["@value"_L1].toVariant().toDateTime();
}
if (data["@type"_L1] == u"FoodEstablishmentReservation"_s) {
dateTime = data["startTime"_L1]["@value"_L1].toVariant().toDateTime();
}
if (data["@type"_L1] == u"FlightReservation"_s) {
dateTime = data["reservationFor"_L1]["boardingTime"_L1]["@value"_L1].toVariant().toDateTime();
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == EndTimeRole) {
auto dateTime = data["checkoutTime"_L1]["@value"_L1].toVariant().toDateTime();
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == DeparturePlatformRole) {
return data["reservationFor"_L1]["departurePlatform"_L1];
}
if (role == ArrivalPlatformRole) {
return data["reservationFor"_L1]["arrivalPlatform"_L1];
}
if (role == CoachRole) {
return data["reservedTicket"_L1]["ticketedSeat"_L1]["seatSection"_L1];
}
if (role == SeatRole) {
return data["reservedTicket"_L1]["ticketedSeat"_L1]["seatNumber"_L1];
}
return {};
}
int ItineraryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_data.size();
}
QHash<int, QByteArray> ItineraryModel::roleNames() const
{
return {
{NameRole, "name"},
{TypeRole, "type"},
{DepartureLocationRole, "departureLocation"},
{DepartureAddressRole, "departureAddress"},
{ArrivalLocationRole, "arrivalLocation"},
{ArrivalAddressRole, "arrivalAddress"},
{DepartureTimeRole, "departureTime"},
{ArrivalTimeRole, "arrivalTime"},
{AddressRole, "address"},
{StartTimeRole, "startTime"},
{EndTimeRole, "endTime"},
{DeparturePlatformRole, "departurePlatform"},
{ArrivalPlatformRole, "arrivalPlatform"},
{CoachRole, "coach"},
{SeatRole, "seat"},
};
}
QString ItineraryModel::path() const
{
return m_path;
}
void ItineraryModel::setPath(const QString &path)
{
m_path = path;
loadData();
}
void ItineraryModel::loadData()
{
auto process = new QProcess(this);
process->start(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF6) + u"/kitinerary-extractor"_s, {m_path.mid(7)});
connect(process, &QProcess::finished, this, [this, process]() {
auto data = process->readAllStandardOutput();
beginResetModel();
m_data = QJsonDocument::fromJson(data).array();
endResetModel();
Q_EMIT loaded();
});
connect(process, &QProcess::errorOccurred, this, [this]() {
Q_EMIT loadErrorOccurred();
});
}
void ItineraryModel::sendToItinerary()
{
#ifndef Q_OS_ANDROID
auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.itinerary"_s));
job->setUrls({QUrl::fromLocalFile(m_path.mid(7))});
job->start();
#endif
}
#include "moc_itinerarymodel.cpp"

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QJsonArray>
#include <QPointer>
#include <QQmlEngine>
#include <QString>
class ItineraryModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
enum Roles {
NameRole = Qt::DisplayRole,
TypeRole,
DepartureLocationRole,
ArrivalLocationRole,
DepartureTimeRole,
DepartureAddressRole,
ArrivalTimeRole,
ArrivalAddressRole,
AddressRole,
StartTimeRole,
EndTimeRole,
DeparturePlatformRole,
ArrivalPlatformRole,
CoachRole,
SeatRole,
};
Q_ENUM(Roles)
explicit ItineraryModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = {}) const override;
QHash<int, QByteArray> roleNames() const override;
QString path() const;
void setPath(const QString &path);
Q_INVOKABLE void sendToItinerary();
Q_SIGNALS:
void loaded();
void loadErrorOccurred();
private:
QJsonArray m_data;
QString m_path;
void loadData();
};

View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "linemodel.h"
LineModel::LineModel(QObject *parent)
: QAbstractListModel(parent)
{
}
QQuickTextDocument *LineModel::document() const
{
return m_document;
}
void LineModel::setDocument(QQuickTextDocument *document)
{
if (document == m_document) {
return;
}
m_document = document;
Q_EMIT documentChanged();
resetModel();
}
QVariant LineModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
const auto &row = index.row();
if (row < 0 || row > rowCount()) {
return {};
}
if (role == LineHeightRole) {
auto textDoc = m_document->textDocument();
return int(textDoc->documentLayout()->blockBoundingRect(textDoc->findBlockByNumber(row)).height());
}
return {};
}
int LineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
if (m_document == nullptr) {
return 0;
}
return m_document->textDocument()->blockCount();
}
QHash<int, QByteArray> LineModel::roleNames() const
{
return {{LineHeightRole, "docLineHeight"}};
}
void LineModel::resetModel()
{
beginResetModel();
endResetModel();
}
#include "moc_linemodel.cpp"

View File

@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QAbstractTextDocumentLayout>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextBlock>
#include <qtmetamacros.h>
/**
* @class LineModel
*
* A model to provide line info for a QQuickTextDocument.
*
* @sa QQuickTextDocument
*/
class LineModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The QQuickTextDocument that is being handled.
*/
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
LineHeightRole = Qt::UserRole + 1, /**< The delegate type of the message. */
};
Q_ENUM(Roles)
explicit LineModel(QObject *parent = nullptr);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Reset the model.
*
* This needs to be called when the QQuickTextDocument container changes width
* or height as this may change line heights due to wrapping.
*
* @sa QQuickTextDocument
*/
Q_INVOKABLE void resetModel();
Q_SIGNALS:
void documentChanged();
private:
QPointer<QQuickTextDocument> m_document = nullptr;
};

View File

@@ -0,0 +1,790 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagecontentmodel.h"
#include "contentprovider.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include <QImageReader>
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/qt_connection_util.h>
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
#include <Quotient/thread.h>
#endif
#include <KLocalizedString>
#include <Kirigami/Platform/PlatformTheme>
#ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Repository>
#endif
#include "chatbarcache.h"
#include "contentprovider.h"
#include "filetype.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "texthandler.h"
using namespace Quotient;
bool MessageContentModel::m_threadsEnabled = false;
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent)
, m_room(room)
, m_eventId(eventId)
, m_currentState(isPending ? Pending : Unknown)
, m_isReply(isReply)
{
initializeModel();
}
void MessageContentModel::initializeModel()
{
Q_ASSERT(m_room != nullptr);
Q_ASSERT(!m_eventId.isEmpty());
connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() {
if (m_room != nullptr && m_currentState == Unknown) {
initializeEvent();
updateReplyModel();
resetModel();
}
});
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr) {
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
m_eventId = serverEvent->id();
}
}
});
connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() {
if (m_room != nullptr && m_currentState == Pending) {
initializeEvent();
updateReplyModel();
resetModel();
}
});
connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) {
if (m_room != nullptr) {
for (int i = fromIndex; i <= toIndex; i++) {
if (m_room->findInTimeline(i)->event()->id() == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
}
}
}
});
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_room != nullptr) {
if (m_eventId == newEvent->id()) {
beginResetModel();
initializeEvent();
resetContent();
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_room != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (oldEventId == m_eventId || newEventId == m_eventId) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
resetContent(newEventId == m_eventId);
endResetModel();
}
});
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
if (oldThreadId == m_eventId || newThreadId == m_eventId) {
beginResetModel();
resetContent(false, newThreadId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
if (m_room != nullptr) {
if (senderId().isEmpty() || senderId() == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
if (m_room != nullptr) {
if (senderId().isEmpty() || senderId() == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(this, &MessageContentModel::threadsEnabledChanged, this, [this]() {
updateReplyModel();
resetModel();
});
connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
updateReactionModel();
}
});
initializeEvent();
if (m_currentState == Available || m_currentState == Pending) {
updateReplyModel();
}
resetModel();
updateReactionModel();
}
void MessageContentModel::initializeEvent()
{
if (m_currentState == UnAvailable) {
return;
}
const auto eventResult = m_room->getEvent(m_eventId);
if (eventResult.first == nullptr) {
if (m_currentState != Pending) {
getEvent();
}
return;
}
if (eventResult.second) {
m_currentState = Pending;
} else {
m_currentState = Available;
}
Q_EMIT eventUpdated();
}
void MessageContentModel::getEvent()
{
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
return true;
}
}
return false;
});
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_currentState = UnAvailable;
resetModel();
return true;
}
}
return false;
});
m_room->downloadEventFromServer(m_eventId);
}
QString MessageContentModel::senderId() const
{
const auto eventResult = m_room->getEvent(m_eventId);
if (eventResult.first == nullptr) {
return {};
}
auto senderId = eventResult.first->senderId();
if (senderId.isEmpty()) {
senderId = m_room->localMember().id();
}
return senderId;
}
NeochatRoomMember *MessageContentModel::senderObject() const
{
const auto eventResult = m_room->getEvent(m_eventId);
if (eventResult.first == nullptr) {
return nullptr;
}
if (eventResult.first->senderId().isEmpty()) {
return m_room->qmlSafeMember(m_room->localMember().id());
}
return m_room->qmlSafeMember(eventResult.first->senderId());
}
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
QVariant MessageContentModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= rowCount()) {
qDebug() << "MessageContentModel, something's wrong: index.row() >= rowCount()";
return {};
}
const auto component = m_components[index.row()];
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
if (role == DisplayRole) {
if (m_isReply) {
return i18n("Loading reply");
} else {
return i18n("Loading");
}
}
if (role == ComponentTypeRole) {
return component.type;
}
return {};
}
if (role == DisplayRole) {
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(senderId())) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QString disabledTextColor;
if (theme != nullptr) {
disabledTextColor = theme->disabledTextColor().name();
} else {
disabledTextColor = u"#000000"_s;
}
return QString(u"<span style=\"color:%1\">"_s.arg(disabledTextColor)
+ i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user")
+ u"</span>"_s);
}
if (component.type == MessageComponentType::Loading) {
if (m_isReply) {
return i18n("Loading reply");
} else {
return i18n("Loading");
}
}
if (!component.content.isEmpty()) {
return component.content;
}
return EventHandler::richBody(m_room, event.first);
}
if (role == ComponentTypeRole) {
return component.type;
}
if (role == ComponentAttributesRole) {
return component.attributes;
}
if (role == EventIdRole) {
return event.first->displayId();
}
if (role == TimeRole) {
return EventHandler::time(m_room, event.first, m_currentState == Pending);
}
if (role == TimeStringRole) {
return EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending);
}
if (role == AuthorRole) {
return QVariant::fromValue<NeochatRoomMember *>(senderObject());
}
if (role == MediaInfoRole) {
return EventHandler::mediaInfo(m_room, event.first);
}
if (role == FileTransferInfoRole) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(event.first));
}
if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
}
if (role == LatitudeRole) {
return EventHandler::latitude(event.first);
}
if (role == LongitudeRole) {
return EventHandler::longitude(event.first);
}
if (role == AssetRole) {
return EventHandler::locationAssetType(event.first);
}
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(ContentProvider::self().handlerForPoll(m_room, m_eventId));
}
if (role == ReplyEventIdRole) {
if (const auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first)) {
return roomMessageEvent->replyEventId();
}
}
if (role == ReplyAuthorRole) {
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first));
}
if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
}
if (role == ReactionModelRole) {
return QVariant::fromValue<ReactionModel *>(m_reactionModel);
;
}
if (role == ThreadRootRole) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first);
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
#else
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
#endif
return roomMessageEvent->threadRootEventId();
}
return {};
}
if (role == LinkPreviewerRole) {
if (component.type == MessageComponentType::LinkPreview) {
return QVariant::fromValue<LinkPreviewer *>(
dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(component.attributes["link"_L1].toUrl()));
} else {
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == ChatBarCacheRole) {
if (m_room->threadCache()->threadId() == m_eventId) {
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
}
return {};
}
int MessageContentModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_components.size();
}
QHash<int, QByteArray> MessageContentModel::roleNames() const
{
return roleNamesStatic();
}
QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
{
QHash<int, QByteArray> roles;
roles[MessageContentModel::DisplayRole] = "display";
roles[MessageContentModel::ComponentTypeRole] = "componentType";
roles[MessageContentModel::ComponentAttributesRole] = "componentAttributes";
roles[MessageContentModel::EventIdRole] = "eventId";
roles[MessageContentModel::TimeRole] = "time";
roles[MessageContentModel::TimeStringRole] = "timeString";
roles[MessageContentModel::AuthorRole] = "author";
roles[MessageContentModel::MediaInfoRole] = "mediaInfo";
roles[MessageContentModel::FileTransferInfoRole] = "fileTransferInfo";
roles[MessageContentModel::ItineraryModelRole] = "itineraryModel";
roles[MessageContentModel::LatitudeRole] = "latitude";
roles[MessageContentModel::LongitudeRole] = "longitude";
roles[MessageContentModel::AssetRole] = "asset";
roles[MessageContentModel::PollHandlerRole] = "pollHandler";
roles[MessageContentModel::ReplyEventIdRole] = "replyEventId";
roles[MessageContentModel::ReplyAuthorRole] = "replyAuthor";
roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel";
roles[MessageContentModel::ReactionModelRole] = "reactionModel";
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
return roles;
}
void MessageContentModel::resetModel()
{
beginResetModel();
m_components.clear();
if (m_room->connection()->isIgnored(senderId()) || m_currentState == UnAvailable) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
}
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
endResetModel();
return;
}
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
m_components += messageContentComponents();
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing, bool isThreading)
{
const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0;
beginRemoveRows({}, startRow, rowCount() - 1);
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing, isThreading);
if (newComponents.size() == 0) {
return;
}
beginInsertRows({}, startRow, startRow + newComponents.size() - 1);
m_components += newComponents;
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
{
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
return {};
}
QList<MessageComponent> newComponents;
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
if (roomMessageEvent && roomMessageEvent->rawMsgtype() == u"m.key.verification.request"_s) {
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
return newComponents;
}
if (event.first->isRedacted()) {
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
return newComponents;
}
if (m_replyModel != nullptr) {
newComponents += MessageComponent{MessageComponentType::Reply, QString(), {}};
}
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} else {
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first)));
}
if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents);
}
if ((m_reactionModel && m_reactionModel->rowCount() > 0)) {
newComponents += MessageComponent{MessageComponentType::Reaction, QString(), {}};
}
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (m_threadsEnabled && roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
#else
if (m_threadsEnabled && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
#endif
newComponents += MessageComponent{MessageComponentType::Separator, {}, {}};
newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}};
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (isThreading && roomMessageEvent && !(roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
#else
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) {
#endif
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
}
return newComponents;
}
void MessageContentModel::updateReplyModel()
{
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr || m_isReply) {
return;
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
if (roomMessageEvent == nullptr) {
return;
}
if (!roomMessageEvent->isReply(m_threadsEnabled) || (roomMessageEvent->isThreaded() && m_threadsEnabled)) {
if (m_replyModel) {
delete m_replyModel;
}
return;
}
if (m_replyModel != nullptr) {
return;
}
m_replyModel = new MessageContentModel(m_room, roomMessageEvent->replyEventId(!m_threadsEnabled), true, false, this);
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
});
}
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
{
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
return {};
}
switch (type) {
case MessageComponentType::Text: {
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
if (body.trimmed().isEmpty()) {
return TextHandler().textComponents(i18n("<i>This event does not have any content.</i>"),
Qt::TextFormat::RichText,
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
} else {
return TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
}
}
case MessageComponentType::File: {
QList<MessageComponent> components;
components += MessageComponent{MessageComponentType::File, QString(), {}};
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
if (m_emptyItinerary) {
if (!m_isReply) {
auto fileTransferInfo = m_room->cachedFileTransferInfo(event.first);
#ifndef Q_OS_ANDROID
Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->has<EventContent::FileContent>());
const QMimeType mimeType = roomMessageEvent->get<EventContent::FileContent>()->mimeType;
if (mimeType.name() == u"text/plain"_s || mimeType.parentMimeTypes().contains(u"text/plain"_s)) {
QString originalName = roomMessageEvent->get<EventContent::FileContent>()->originalName;
if (originalName.isEmpty()) {
originalName = roomMessageEvent->plainBody();
}
KSyntaxHighlighting::Repository repository;
KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName);
if (!definitionForFile.isValid()) {
definitionForFile = repository.definitionForMimeType(mimeType.name());
}
QFile file(fileTransferInfo.localPath.path());
file.open(QIODevice::ReadOnly);
components += MessageComponent{MessageComponentType::Code,
QString::fromStdString(file.readAll().toStdString()),
{{u"class"_s, definitionForFile.name()}}};
}
#endif
if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) {
QImageReader reader(fileTransferInfo.localPath.path());
components += MessageComponent{MessageComponentType::Pdf, QString(), {{u"size"_s, reader.size()}}};
}
}
} else if (m_itineraryModel != nullptr) {
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
if (m_itineraryModel->rowCount() > 0) {
updateItineraryModel();
}
} else {
updateItineraryModel();
}
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components;
}
case MessageComponentType::Image:
case MessageComponentType::Audio:
case MessageComponentType::Video: {
if (!event.first->is<StickerEvent>()) {
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
const auto fileContent = roomMessageEvent->get<EventContent::FileContentBase>();
if (fileContent != nullptr) {
const auto fileInfo = fileContent->commonInfo();
const auto body = EventHandler::rawMessageBody(*roomMessageEvent);
// Do not attach the description to the image, if it's the same as the original filename.
if (fileInfo.originalName != body) {
QList<MessageComponent> components;
components += MessageComponent{type, QString(), {}};
components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components;
}
}
}
}
[[fallthrough]];
default:
return {MessageComponent{type, QString(), {}}};
}
}
MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
{
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
if (linkPreviewer == nullptr) {
return {};
}
if (linkPreviewer->loaded()) {
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_L1, link}}};
} else {
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
for (auto &component : m_components) {
if (component.attributes["link"_L1].toUrl() == link) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
component.type = MessageComponentType::LinkPreview;
endResetModel();
}
}
}
});
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
}
}
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents)
{
int i = 0;
while (i < inputComponents.size()) {
const auto component = inputComponents.at(i);
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
const auto links = LinkPreviewer::linkPreviews(component.content);
for (qsizetype j = 0; j < links.size(); ++j) {
const auto linkPreview = linkPreviewComponent(links[j]);
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
inputComponents.insert(i + j + 1, linkPreview);
}
};
}
}
i++;
}
return inputComponents;
}
void MessageContentModel::closeLinkPreview(int row)
{
if (row < 0 || row >= m_components.size()) {
qWarning() << "closeLinkPreview() called with row" << row << "which does not exist. m_components.size() =" << m_components.size();
return;
}
if (m_components[row].type == MessageComponentType::LinkPreview || m_components[row].type == MessageComponentType::LinkPreviewLoad) {
beginResetModel();
m_removedLinkPreviews += m_components[row].attributes["link"_L1].toUrl();
m_components.remove(row);
m_components.squeeze();
endResetModel();
resetContent();
}
}
void MessageContentModel::updateItineraryModel()
{
const auto event = m_room->getEvent(m_eventId);
if (m_room == nullptr || event.first == nullptr) {
return;
}
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
if (roomMessageEvent->has<EventContent::FileContent>()) {
auto filePath = m_room->cachedFileTransferInfo(event.first).localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel;
m_itineraryModel = nullptr;
} else if (!filePath.isEmpty()) {
if (m_itineraryModel == nullptr) {
m_itineraryModel = new ItineraryModel(this);
connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() {
if (m_itineraryModel->rowCount() == 0) {
m_emptyItinerary = true;
m_itineraryModel->deleteLater();
m_itineraryModel = nullptr;
resetContent();
}
});
connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() {
m_emptyItinerary = true;
m_itineraryModel->deleteLater();
m_itineraryModel = nullptr;
resetContent();
});
}
m_itineraryModel->setPath(filePath.toString());
}
}
}
}
void MessageContentModel::updateReactionModel()
{
if (m_reactionModel != nullptr && m_reactionModel->rowCount() > 0) {
return;
}
if (m_reactionModel == nullptr) {
m_reactionModel = new ReactionModel(this, m_eventId, m_room);
connect(m_reactionModel, &ReactionModel::reactionsUpdated, this, &MessageContentModel::updateReactionModel);
}
if (m_reactionModel->rowCount() <= 0) {
m_reactionModel->disconnect(this);
delete m_reactionModel;
m_reactionModel = nullptr;
return;
}
resetContent();
}
ThreadModel *MessageContentModel::modelForThread(const QString &threadRootId)
{
return ContentProvider::self().modelForThread(m_room, threadRootId);
}
void MessageContentModel::setThreadsEnabled(bool enableThreads)
{
m_threadsEnabled = enableThreads;
}
#include "moc_messagecontentmodel.cpp"

View File

@@ -0,0 +1,159 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/events/roomevent.h>
#include "enums/messagecomponenttype.h"
#include "itinerarymodel.h"
#include "messagecomponent.h"
#include "models/reactionmodel.h"
#include "neochatroommember.h"
class ThreadModel;
/**
* @class MessageContentModel
*
* A model to visualise the components of a single RoomMessageEvent.
*/
class MessageContentModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
enum MessageState {
Unknown, /**< The message state is unknown. */
Pending, /**< The message is a new pending message which the server has not yet acknowledged. */
Available, /**< The message is available and acknowledged by the server. */
UnAvailable, /**< The message can't be retrieved either because it doesn't exist or is blocked. */
};
Q_ENUM(MessageState)
/**
* @brief Defines the model roles.
*/
enum Roles {
DisplayRole = Qt::DisplayRole, /**< The display text for the message. */
ComponentTypeRole = Qt::UserRole, /**< The type of component to visualise the message. */
ComponentAttributesRole, /**< The attributes of the component. */
EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
AuthorRole, /**< The author of the event. */
MediaInfoRole, /**< The media info for the event. */
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
ItineraryModelRole, /**< The itinerary model for a file. */
LatitudeRole, /**< Latitude for a location event. */
LongitudeRole, /**< Longitude for a location event. */
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
PollHandlerRole, /**< The PollHandler for the event, if any. */
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
ReplyAuthorRole, /**< The author of the event that was replied to. */
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
ReactionModelRole, /**< Reaction model for this event. */
ThreadRootRole, /**< The thread root event ID for the event. */
LinkPreviewerRole, /**< The link preview details. */
ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
explicit MessageContentModel(NeoChatRoom *room,
const QString &eventId,
bool isReply = false,
bool isPending = false,
MessageContentModel *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
static QHash<int, QByteArray> roleNamesStatic();
/**
* @brief Close the link preview at the given index.
*
* If the given index is not a link preview component, nothing happens.
*/
Q_INVOKABLE void closeLinkPreview(int row);
/**
* @brief Returns the thread model for the given thread root event ID.
*
* A model is created is one doesn't exist. Will return nullptr if threadRootId
* is empty.
*/
Q_INVOKABLE ThreadModel *modelForThread(const QString &threadRootId);
static void setThreadsEnabled(bool enableThreads);
Q_SIGNALS:
void showAuthorChanged();
void eventUpdated();
void threadsEnabledChanged();
private:
QPointer<NeoChatRoom> m_room;
QString m_eventId;
QString senderId() const;
NeochatRoomMember *senderObject() const;
MessageState m_currentState = Unknown;
bool m_isReply;
void initializeModel();
void initializeEvent();
void getEvent();
QList<MessageComponent> m_components;
void resetModel();
void resetContent(bool isEditing = false, bool isThreading = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();
ReactionModel *m_reactionModel = nullptr;
ItineraryModel *m_itineraryModel = nullptr;
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
MessageComponent linkPreviewComponent(const QUrl &link);
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents);
QList<QUrl> m_removedLinkPreviews;
void updateItineraryModel();
bool m_emptyItinerary = false;
void updateReactionModel();
static bool m_threadsEnabled;
};

View File

@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "pollanswermodel.h"
#include "neochatroom.h"
#include "pollhandler.h"
PollAnswerModel::PollAnswerModel(PollHandler *parent)
: QAbstractListModel(parent)
{
Q_ASSERT(parent != nullptr);
connect(parent, &PollHandler::selectionsChanged, this, [this]() {
dataChanged(index(0), index(rowCount() - 1), {CountRole, LocalChoiceRole, IsWinnerRole});
});
connect(parent, &PollHandler::answersChanged, this, [this]() {
dataChanged(index(0), index(rowCount() - 1), {TextRole});
});
}
QVariant PollAnswerModel::data(const QModelIndex &index, int role) const
{
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
const auto row = index.row();
if (row < 0 || row >= rowCount()) {
return {};
}
const auto pollHandler = dynamic_cast<PollHandler *>(this->parent());
if (pollHandler == nullptr) {
qWarning() << "PollAnswerModel created with nullptr parent.";
return 0;
}
if (role == IdRole) {
return pollHandler->answerAtRow(row).id;
}
if (role == TextRole) {
return pollHandler->answerAtRow(row).text;
}
if (role == CountRole) {
return pollHandler->answerCountAtId(pollHandler->answerAtRow(row).id);
}
if (role == LocalChoiceRole) {
const auto room = pollHandler->room();
if (room == nullptr) {
return {};
}
return pollHandler->checkMemberSelectedId(room->localMember().id(), pollHandler->answerAtRow(row).id);
}
if (role == IsWinnerRole) {
return pollHandler->winningAnswerIds().contains(pollHandler->answerAtRow(row).id) && pollHandler->hasEnded();
}
return {};
}
int PollAnswerModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
const auto pollHandler = dynamic_cast<PollHandler *>(this->parent());
if (pollHandler == nullptr) {
qWarning() << "PollAnswerModel created with nullptr parent.";
return 0;
}
return pollHandler->numAnswers();
}
QHash<int, QByteArray> PollAnswerModel::roleNames() const
{
return {
{IdRole, "id"},
{TextRole, "answerText"},
{CountRole, "count"},
{LocalChoiceRole, "localChoice"},
{IsWinnerRole, "isWinner"},
};
}

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
class PollHandler;
/**
* @class PollAnswerModel
*
* This class defines the model for visualising a list of answer to a poll.
*/
class PollAnswerModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
IdRole, /**< The ID of the answer. */
TextRole, /**< The answer text. */
CountRole, /**< The number of people who gave this answer. */
LocalChoiceRole, /**< Whether this option was selected by the local user */
IsWinnerRole, /**< Whether this option was selected by the local user */
};
Q_ENUM(Roles)
explicit PollAnswerModel(PollHandler *parent);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = {}) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
};

Some files were not shown because too many files have changed in this diff Show More