Files
z-bar-qt/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml
T
2026-10-23 00:05:08 +02:00

390 lines
8.5 KiB
QML

import QtQuick
import ZShell.Components
import ZShell.Config
import ZShell.Llm
import qs.Effects
import qs.Components
import qs.Services
CustomRect {
id: root
property bool expanded: false
property bool highlighted: false
property bool slim: false
required property ChatSession modelData
signal clicked(content: ChatSession)
signal remove(content: ChatSession)
color: highlighted ? Qt.alpha(Colors.palette.m3primaryContainer, 0.3) : Qt.alpha(Colors.tPalette.m3surfaceContainer, 0.2)
implicitHeight: {
let h = 0;
if (slim)
return chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
h += infoContainer.implicitHeight;
if (expanded)
h += body.implicitHeight + body.topMargin + actionRow.implicitHeight + actionRow.anchors.topMargin;
else
h += preview.implicitHeight + preview.topMargin;
h += Tokens.padding.small;
const icon = chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
if (h < icon)
return icon;
return h;
}
radius: Tokens.rounding.large
clip: true
CustomRect {
id: borderHighlight
anchors.fill: parent
border.width: root.highlighted ? 2 : 0
border.color: Colors.palette.m3secondary
radius: root.radius - border.width + 1
Behavior on border.width {
Anim {}
}
}
Behavior on implicitHeight {
enabled: titleText.readOnly
Anim {}
}
MaterialIcon {
id: chatIcon
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: Tokens.padding.large
fontStyle: Tokens.font.icon.extraLarge
text: root.modelData.icon || "check"
color: Colors.palette.m3onSurface
}
Item {
id: infoContainer
anchors.left: chatIcon.right
anchors.leftMargin: Tokens.spacing.medium
anchors.top: parent.top
anchors.right: expandBtn.left
anchors.rightMargin: Tokens.spacing.medium
implicitHeight: {
let h = 0;
h += title.implicitHeight + title.anchors.topMargin;
h += timestamp.implicitHeight + timestamp.anchors.topMargin;
if (root.expanded)
h += created.implicitHeight + created.anchors.topMargin;
return h;
}
opacity: root.slim ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomRect {
id: title
anchors.left: parent.left
anchors.topMargin: Tokens.padding.extraSmall
anchors.top: parent.top
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
radius: Tokens.rounding.full
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
opacity: titleText.readOnly ? 0 : 1
Behavior on opacity {
Anim {}
}
}
Item {
id: titleWrapper
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
TextFieldBase {
id: titleText
anchors.left: parent.left
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
font: Tokens.font.body.medium
text: root.modelData.title || qsTr("New chat")
readOnly: true
color: Colors.palette.m3onSurface
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
onEditingFinished: root.modelData.title = text
onReadOnlyChanged: {
if (!readOnly) {
this.forceActiveFocus();
cursorPosition = text.length;
} else {
root.forceActiveFocus();
}
}
}
}
CustomRect {
id: fadeMask
anchors.fill: titleWrapper
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
}
}
}
CustomText {
id: created
anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.extraSmall
anchors.left: parent.left
anchors.right: parent.right
font: Tokens.font.label.small
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Created on: %1").arg(root.modelData.createdAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP"))
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
CustomText {
id: updated
anchors.top: created.bottom
anchors.topMargin: Tokens.spacing.small
anchors.left: parent.left
font: Tokens.font.label.small
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Updated on: ")
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
CustomText {
id: timestamp
anchors.left: parent.left
anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.label.small
text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
target: timestamp
anchors.left: updated.right
anchors.top: undefined
anchors.verticalCenter: updated.verticalCenter
}
}
transitions: Transition {
AnchorAnim {}
}
}
}
CustomText {
id: preview
readonly property int topMargin: Tokens.spacing.extraSmall
anchors.left: chatIcon.right
anchors.margins: Tokens.spacing.medium
anchors.right: parent.right
y: infoContainer.implicitHeight + topMargin
elide: Text.ElideRight
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.small
maximumLineCount: 1
opacity: !root.expanded && !root.slim ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
Behavior on y {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomText {
id: body
readonly property int topMargin: root.expanded ? Tokens.spacing.medium : Tokens.spacing.extraSmall
anchors.left: chatIcon.right
anchors.margins: Tokens.spacing.medium
anchors.right: parent.right
y: infoContainer.implicitHeight + topMargin
animate: true
color: Colors.palette.m3onSurfaceVariant
maximumLineCount: 5
opacity: root.expanded ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.content ?? qsTr("No messages yet")
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on y {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
StateLayer {
id: sLayer
property int startY
onClicked: {
root.clicked(root.modelData);
}
}
ButtonRow {
id: actionRow
anchors.top: body.bottom
anchors.left: chatIcon.right
anchors.right: parent.right
anchors.topMargin: Tokens.spacing.small
anchors.leftMargin: Tokens.spacing.medium
anchors.rightMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium
IconButton {
icon: "close"
fillWidth: true
shapeMorph: true
isRound: true
font: Tokens.font.icon.medium
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
onClicked: root.remove(root.modelData)
}
IconButton {
icon: titleText.readOnly ? "edit" : "check"
label.animate: true
isToggle: true
fillWidth: true
shapeMorph: true
font: Tokens.font.icon.medium
isRound: true
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
activeColor: Colors.palette.m3secondary
activeOnColor: Colors.palette.m3onSecondary
checked: !titleText.readOnly
onClicked: titleText.readOnly = !titleText.readOnly
}
}
CustomRect {
id: expandBtn
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Tokens.padding.small
implicitHeight: expandIcon.implicitHeight
implicitWidth: expandIcon.implicitHeight
radius: Tokens.rounding.full
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
opacity: root.slim ? 0 : 1
visible: opacity > 0
enabled: visible
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
StateLayer {
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
anchors.verticalCenterOffset: root.expanded ? -1 : 1
rotation: root.expanded ? 180 : 0
color: Colors.palette.m3onSurface
text: "expand_more"
Behavior on anchors.verticalCenterOffset {
Anim {}
}
Behavior on rotation {
Anim {}
}
}
}
}