chore: move sidebar directory to modules
This commit is contained in:
@@ -1,581 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ChatSession chatData
|
||||
// qmlformat off
|
||||
readonly property var keys: [
|
||||
Qt.Key_A,
|
||||
Qt.Key_B,
|
||||
Qt.Key_C,
|
||||
Qt.Key_D,
|
||||
Qt.Key_E,
|
||||
Qt.Key_F,
|
||||
Qt.Key_G,
|
||||
Qt.Key_H,
|
||||
Qt.Key_I,
|
||||
Qt.Key_J,
|
||||
Qt.Key_K,
|
||||
Qt.Key_L,
|
||||
Qt.Key_M,
|
||||
Qt.Key_N,
|
||||
Qt.Key_O,
|
||||
Qt.Key_P,
|
||||
Qt.Key_Q,
|
||||
Qt.Key_R,
|
||||
Qt.Key_S,
|
||||
Qt.Key_T,
|
||||
Qt.Key_U,
|
||||
Qt.Key_V,
|
||||
Qt.Key_W,
|
||||
Qt.Key_X,
|
||||
Qt.Key_Y,
|
||||
Qt.Key_Z,
|
||||
|
||||
Qt.Key_Agrave,
|
||||
Qt.Key_Aacute,
|
||||
Qt.Key_Acircumflex,
|
||||
Qt.Key_Atilde,
|
||||
Qt.Key_Adiaeresis,
|
||||
Qt.Key_Aring,
|
||||
Qt.Key_AE,
|
||||
|
||||
Qt.Key_Ccedilla,
|
||||
|
||||
Qt.Key_Egrave,
|
||||
Qt.Key_Eacute,
|
||||
Qt.Key_Ecircumflex,
|
||||
Qt.Key_Ediaeresis,
|
||||
|
||||
Qt.Key_Igrave,
|
||||
Qt.Key_Iacute,
|
||||
Qt.Key_Icircumflex,
|
||||
Qt.Key_Idiaeresis,
|
||||
|
||||
Qt.Key_ETH,
|
||||
|
||||
Qt.Key_Ntilde,
|
||||
|
||||
Qt.Key_Ograve,
|
||||
Qt.Key_Oacute,
|
||||
Qt.Key_Ocircumflex,
|
||||
Qt.Key_Otilde,
|
||||
Qt.Key_Odiaeresis,
|
||||
Qt.Key_Ooblique,
|
||||
|
||||
Qt.Key_Ugrave,
|
||||
Qt.Key_Uacute,
|
||||
Qt.Key_Ucircumflex,
|
||||
Qt.Key_Udiaeresis,
|
||||
|
||||
Qt.Key_Yacute,
|
||||
Qt.Key_ydiaeresis,
|
||||
|
||||
Qt.Key_THORN,
|
||||
Qt.Key_ssharp
|
||||
]
|
||||
// qmlformat on
|
||||
|
||||
signal requestClose
|
||||
|
||||
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
|
||||
|
||||
property var pendingAttachments: []
|
||||
|
||||
function attachFiles(): void {
|
||||
Files.pickFiles().then(files => {
|
||||
if (!files || files.length === 0)
|
||||
return;
|
||||
const added = chatData.addAttachments(files);
|
||||
root.pendingAttachments = root.pendingAttachments.concat(added);
|
||||
});
|
||||
}
|
||||
|
||||
function send(text: string): void {
|
||||
if (text.trim() === "" && root.pendingAttachments.length === 0)
|
||||
return;
|
||||
root.scrollToLatest();
|
||||
chatData.sendMessage(text, root.pendingAttachments);
|
||||
input.text = "";
|
||||
root.pendingAttachments = [];
|
||||
}
|
||||
|
||||
function scrollToLatest(): void {
|
||||
if (!listLoader.item || root.atNewestEnd)
|
||||
return;
|
||||
scrollAnim.stop();
|
||||
scrollAnim.start();
|
||||
}
|
||||
|
||||
function focusInput(): void {
|
||||
Qt.callLater(() => input.forceActiveFocus());
|
||||
}
|
||||
|
||||
onChatDataChanged: {
|
||||
if (chatData)
|
||||
focusInput();
|
||||
}
|
||||
Keys.onPressed: e => {
|
||||
if (root.keys.includes(e.key)) {
|
||||
input.insert(input.length, e.text);
|
||||
focusInput();
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: scrollAnim
|
||||
|
||||
target: listLoader.item
|
||||
property: "contentY"
|
||||
to: 0
|
||||
type: Anim.DefaultEffects
|
||||
|
||||
onFinished: if (target)
|
||||
target.contentY = 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.large
|
||||
|
||||
IconButton {
|
||||
icon: "arrow_back"
|
||||
inactiveColor: Colors.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
enabled: !ChatState.isWindow
|
||||
visible: enabled
|
||||
|
||||
onClicked: root.requestClose()
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: ChatState.isWindow ? false : true
|
||||
Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.title.medium
|
||||
text: qsTr(root.chatData?.title || ChatState.title)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: listLoader
|
||||
|
||||
anchors.bottom: input.top
|
||||
anchors.bottomMargin: Tokens.spacing.medium
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
active: root.chatData && root.chatData.loaded
|
||||
|
||||
sourceComponent: Flickable {
|
||||
id: flick
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: list.layoutHeight
|
||||
|
||||
transform: Rotation {
|
||||
origin.y: flick.height / 2
|
||||
origin.x: flick.width / 2
|
||||
angle: 180
|
||||
}
|
||||
|
||||
WheelInverter {
|
||||
target: flick
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: restoreAnim
|
||||
|
||||
target: flick
|
||||
type: Anim.DefaultEffects
|
||||
property: "contentY"
|
||||
}
|
||||
|
||||
ChatListView {
|
||||
id: list
|
||||
|
||||
width: flick.width
|
||||
height: flick.height
|
||||
yAnimDuration: Tokens.anim.durations.expressiveDefaultEffects
|
||||
|
||||
asynchronous: true
|
||||
cacheBuffer: 50
|
||||
estimatedHeight: 100
|
||||
readyDelay: 5
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
spacing: 0
|
||||
|
||||
model: root.chatData.messagesModel
|
||||
|
||||
useCustomViewport: true
|
||||
viewport: {
|
||||
tWatcher.transform;
|
||||
return Qt.rect(0, flick.contentY, width, flick.height);
|
||||
}
|
||||
onViewportAdjustNeeded: delta => {
|
||||
restoreAnim.stop();
|
||||
flick.contentY += delta;
|
||||
}
|
||||
onViewportRestoreRequested: target => {
|
||||
restoreAnim.to = target;
|
||||
restoreAnim.start();
|
||||
}
|
||||
|
||||
delegate: MessageDelegate {
|
||||
id: message
|
||||
|
||||
rootParent: flick
|
||||
chatList: list
|
||||
ChatListView.preferredHeight: implicitHeight
|
||||
ChatListView.trackViewport: true
|
||||
ChatListView.visibleHeight: ChatListView.removing ? 0 : implicitHeight
|
||||
opacity: ChatListView.removing || ChatListView.adding ? 0 : 1
|
||||
transform: Rotation {
|
||||
origin.y: message.height / 2
|
||||
origin.x: message.width / 2
|
||||
angle: 180
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: message.ChatListView.ready
|
||||
|
||||
Anim {
|
||||
id: yAnim
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
id: scrollBar
|
||||
|
||||
flickable: flick
|
||||
parent: flick.parent
|
||||
anchors.top: flick.top
|
||||
anchors.right: flick.right
|
||||
anchors.bottom: flick.bottom
|
||||
inverted: true
|
||||
|
||||
transform: Rotation {
|
||||
origin.y: flick.height / 2
|
||||
origin.x: scrollBar.width / 2
|
||||
angle: 180
|
||||
|
||||
axis {
|
||||
y: 0
|
||||
x: 1
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TransformWatcher {
|
||||
id: tWatcher
|
||||
|
||||
a: flick.contentItem
|
||||
b: list
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: if (root.chatData)
|
||||
root.chatData.messagesModel
|
||||
}
|
||||
|
||||
EmptyBackground {
|
||||
id: emptyState
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.small
|
||||
visible: root.chatData.messageCount < 1
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.bottom: input.top
|
||||
anchors.bottomMargin: Tokens.spacing.extraLarge
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitHeight: scrollToBottom.implicitHeight
|
||||
implicitWidth: scrollToBottom.implicitWidth
|
||||
scale: root.atNewestEnd || !listLoader.item || listLoader.item.contentHeight <= listLoader.item.height ? 0 : 1
|
||||
visible: scale > 0
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: 2
|
||||
radius: scrollToBottom.radius
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: scrollToBottom
|
||||
|
||||
anchors.centerIn: parent
|
||||
font: Tokens.font.icon.large
|
||||
icon: "arrow_downward"
|
||||
isRound: true
|
||||
padding: Tokens.padding.extraSmall
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: {
|
||||
root.scrollToLatest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: attachments
|
||||
|
||||
readonly property int columns: ChatState.isWindow ? 5 : 3
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: input.top
|
||||
anchors.bottomMargin: Tokens.spacing.medium
|
||||
|
||||
flow: GridView.LeftToRight
|
||||
verticalLayoutDirection: GridView.BottomToTop
|
||||
|
||||
cellWidth: width / columns
|
||||
cellHeight: cellWidth
|
||||
|
||||
implicitHeight: Math.ceil(count / columns) * cellHeight
|
||||
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "x"
|
||||
}
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
id: addTrans
|
||||
Anim {
|
||||
property: "y"
|
||||
from: addTrans.ViewTransition.destination.y + attachments.cellHeight / 4
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
id: removeTrans
|
||||
Anim {
|
||||
property: "y"
|
||||
to: removeTrans.ViewTransition.destination.y + attachments.cellHeight / 4
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
objectProp: "path"
|
||||
values: root.pendingAttachments
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: thumb
|
||||
|
||||
required property var modelData
|
||||
function fileIcon(mime): string {
|
||||
if (mime.startsWith("archive/") || mime === "application/zip")
|
||||
return "folder_zip";
|
||||
if (mime === "application/pdf")
|
||||
return "picture_as_pdf";
|
||||
if (mime.startsWith("audio/"))
|
||||
return "audio_file";
|
||||
if (mime.startsWith("video/"))
|
||||
return "video_file";
|
||||
if (mime.startsWith("text/"))
|
||||
return "description";
|
||||
return "insert_drive_file";
|
||||
}
|
||||
|
||||
width: attachments.cellWidth
|
||||
height: width
|
||||
opacity: modelData ? 1 : 0
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.spacing.extraSmall
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
|
||||
FadeImage {
|
||||
id: attImage
|
||||
|
||||
anchors.fill: parent
|
||||
fillMode: FadeImage.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
source: visible ? thumb.modelData?.path ?? "" : ""
|
||||
visible: thumb.modelData?.isImage ?? false
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: thumb.fileIcon(thumb.modelData?.mimeType ?? "")
|
||||
visible: !thumb.modelData?.isImage ?? false
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: attIndicatorLoader
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
opacity: 0
|
||||
active: opacity > 0 && (thumb.modelData?.isImage ?? false)
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
|
||||
implicitHeight: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
|
||||
|
||||
color: Colors.palette.m3primaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
LoadingIndicator {
|
||||
id: attLoadingIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
containsIcon: true
|
||||
implicitSize: thumb.implicitWidth * 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: attLoadDebounceTimer
|
||||
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
if (attImage.status !== Image.Ready)
|
||||
attIndicatorLoader.opacity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hover
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: undefined
|
||||
|
||||
IconButton {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
icon: "close"
|
||||
visible: opacity > 0
|
||||
enabled: thumb.opacity > 0
|
||||
opacity: hover.containsMouse ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.pendingAttachments = root.pendingAttachments.filter(a => a !== thumb.modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChatInput {
|
||||
id: input
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
bg.border.color: Colors.palette.m3outlineVariant
|
||||
bg.color: Colors.tPalette.m3surfaceContainerLowest
|
||||
fastGlow: LlmClient.busy
|
||||
glow.opacity: input.activeFocus && (!LlmClient.busy || ChatState.isWindow)
|
||||
font: Tokens.font.body.small
|
||||
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
|
||||
focus: true
|
||||
placeholderText: qsTr("Send a message")
|
||||
sendIcon.font: Tokens.font.icon.medium
|
||||
sendIcon.icon: LlmClient.busy ? "stop" : "arrow_upward"
|
||||
sendIcon.padding: Tokens.padding.extraSmall
|
||||
sendIcon.opacity: text || LlmClient.busy ? 1.0 : 0.5
|
||||
sendIcon.enabled: text || LlmClient.busy
|
||||
|
||||
Component.onCompleted: {
|
||||
root.focusInput();
|
||||
}
|
||||
Keys.onPressed: e => {
|
||||
if (e.key == Qt.Key_Return) {
|
||||
if (!(e.modifiers & Qt.ShiftModifier)) {
|
||||
root.send(text);
|
||||
e.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key == Qt.Key_V && e.modifiers & Qt.ControlModifier) {
|
||||
if (root.chatData.clipboardHasAttachment()) {
|
||||
const att = root.chatData.pasteAttachments();
|
||||
root.pendingAttachments = [...root.pendingAttachments, ...att];
|
||||
e.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
onButtonPressed: {
|
||||
if (LlmClient.busy)
|
||||
LlmClient.stop();
|
||||
else
|
||||
root.send(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
TextAreaBase {
|
||||
id: root
|
||||
|
||||
readonly property alias bg: bg
|
||||
readonly property alias sendIcon: sendIcon
|
||||
readonly property alias glow: glow
|
||||
|
||||
property bool fastGlow: false
|
||||
|
||||
signal buttonPressed
|
||||
|
||||
bottomPadding: Tokens.padding.large
|
||||
leftPadding: Tokens.padding.extraLarge
|
||||
rightPadding: sendIcon.width + sendIcon.anchors.rightMargin + Tokens.spacing.small
|
||||
topPadding: Tokens.padding.large
|
||||
wrapMode: TextAreaBase.Wrap
|
||||
|
||||
onFastGlowChanged: {
|
||||
if (fastGlow) {
|
||||
resetAnim.stop();
|
||||
boostAnim.restart();
|
||||
} else {
|
||||
boostAnim.stop();
|
||||
resetAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
background: CustomRect {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.extraLarge
|
||||
|
||||
GeminiGlow {
|
||||
id: glow
|
||||
glowSize: 32
|
||||
z: 5
|
||||
|
||||
opacity: root.activeFocus ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: boostAnim
|
||||
|
||||
NumberAnimation {
|
||||
target: glow
|
||||
property: "speed"
|
||||
to: 4.0
|
||||
duration: 250
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: glow
|
||||
property: "speed"
|
||||
to: 2.25
|
||||
duration: 1500
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Ease back down to normal once fastGlow is false
|
||||
NumberAnimation {
|
||||
id: resetAnim
|
||||
target: glow
|
||||
property: "speed"
|
||||
to: 1.0
|
||||
duration: 800
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
cursorShape: Qt.IBeamCursor
|
||||
enabled: !root.activeFocus
|
||||
manualPressOverride: tapHandler.pressed
|
||||
|
||||
onClicked: root.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
if (stateLayer.enabled)
|
||||
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: placeholder
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.leftPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.placeholderTextColor
|
||||
font: root.font
|
||||
opacity: root.text ? 0 : 1
|
||||
text: root.placeholderText
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: sendIcon
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled: root.text
|
||||
icon: "upward_arrow"
|
||||
opacity: root.text ? 1 : 0
|
||||
radius: Tokens.rounding.full
|
||||
radiusMorph: false
|
||||
stateLayer.hoverEnabled: enabled
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.buttonPressed()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias list: list
|
||||
property alias model: list.model
|
||||
property bool highlight: false
|
||||
property bool slim: false
|
||||
|
||||
signal requestExpand
|
||||
signal deleteChatRequest(content: ChatSession)
|
||||
signal loadChatRequest(content: ChatSession, index: int)
|
||||
signal newChatRequest
|
||||
|
||||
onSlimChanged: {
|
||||
if (slim)
|
||||
modelsContainer.open = false;
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: chatHeader
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.extraSmall
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: Colors.palette.m3outline
|
||||
font.family: "CaskaydiaCove NF"
|
||||
font.pointSize: 13
|
||||
font.weight: 500
|
||||
text: qsTr("%1 Chat%2").arg(list.count).arg(list.count > 1 ? "s" : "")
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
|
||||
anchors.bottom: modelsContainer.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: chatHeader.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
clip: true
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: list.width
|
||||
property: "x"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
to: list.width
|
||||
property: "x"
|
||||
}
|
||||
|
||||
Anim {
|
||||
to: 0
|
||||
property: "opacity"
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ChatDelegate {
|
||||
id: chat
|
||||
|
||||
required property int index
|
||||
|
||||
implicitWidth: ListView.view.width
|
||||
highlighted: root.highlight && ChatState.chatSession === modelData
|
||||
slim: root.slim
|
||||
|
||||
onHighlightedChanged: if (highlighted)
|
||||
list.currentIndex = index
|
||||
onClicked: content => root.loadChatRequest(content, index)
|
||||
onRemove: content => root.deleteChatRequest(content)
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
root.focus = true;
|
||||
ChatState.fabExpanded = false;
|
||||
modelsContainer.expanded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModelsContainer {
|
||||
id: modelsContainer
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: fabRoot.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.rightMargin: Tokens.spacing.small
|
||||
|
||||
rootParent: root
|
||||
fabRoot: fabRoot
|
||||
|
||||
opacity: root.slim ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: fabRoot
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.small
|
||||
padding: 8
|
||||
font.pointSize: Math.round(18 * 1.2)
|
||||
icon: root.slim ? "left_panel_open" : "add"
|
||||
isRound: ChatState.fabExpanded
|
||||
|
||||
label.transform: Rotation {
|
||||
origin.y: fabRoot.label.height / 2
|
||||
origin.x: fabRoot.label.width / 2
|
||||
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded && !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
|
||||
|
||||
Behavior on angle {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.slim && !ChatState.narrowSidebarExpanded) {
|
||||
root.requestExpand();
|
||||
return;
|
||||
}
|
||||
|
||||
ChatState.fabExpanded = !ChatState.fabExpanded;
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: fabRoot.stateLayer.containsMouse ? 4 : 3
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: fabMenu
|
||||
|
||||
anchors.bottom: fabRoot.top
|
||||
anchors.right: fabRoot.right
|
||||
anchors.bottomMargin: Tokens.padding.medium
|
||||
|
||||
Repeater {
|
||||
id: fabRep
|
||||
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
name: "new chat"
|
||||
icon: "add"
|
||||
}
|
||||
ListElement {
|
||||
name: "pop out in window"
|
||||
icon: "open_in_new"
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: fabMenuItem
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
Layout.alignment: Qt.AlignRight
|
||||
implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.medium * 2
|
||||
|
||||
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
|
||||
color: Colors.palette.m3primaryContainer
|
||||
visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
|
||||
clip: true
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 0
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: ChatState.fabExpanded
|
||||
|
||||
PropertyChanges {
|
||||
fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Tokens.padding.large * 2
|
||||
fabMenuItem.opacity: 1
|
||||
fabMenuItemInner.opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
to: "visible"
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: (fabRep.count - 1 - fabMenuItem.index) * 50
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "implicitWidth"
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Tokens.anim.durations.small
|
||||
easing: Tokens.anim.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: fabMenuItem.index * Tokens.anim.durations.small / 8
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "implicitWidth"
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Tokens.anim.durations.small
|
||||
easing: Tokens.anim.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
RowLayout {
|
||||
id: fabMenuItemInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.medium
|
||||
opacity: 0
|
||||
|
||||
MaterialIcon {
|
||||
text: fabMenuItem.modelData.icon
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
fontStyle: Tokens.font.icon.large
|
||||
fill: 1
|
||||
}
|
||||
|
||||
CustomText {
|
||||
animate: true
|
||||
text: fabMenuItem.modelData.name
|
||||
font.capitalization: Font.Capitalize
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
Layout.preferredWidth: implicitWidth
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: sLayer
|
||||
onClicked: {
|
||||
const name = fabMenuItem.modelData.name;
|
||||
|
||||
if (name === "new chat")
|
||||
root.newChatRequest();
|
||||
else if (name === "pop out in window")
|
||||
Detach.create();
|
||||
|
||||
ChatState.fabExpanded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Helpers
|
||||
import qs.Modules.Notifications.Sidebar
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
|
||||
Component.onCompleted: {
|
||||
if (ChatState.firstOpen) {
|
||||
ChatState.openDraft();
|
||||
stack.push(chatList);
|
||||
stack.push(chatContent);
|
||||
ChatState.firstOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ChatState.inChat) {
|
||||
stack.push(chatList);
|
||||
stack.push(chatContent);
|
||||
} else
|
||||
stack.push(chatList);
|
||||
}
|
||||
|
||||
function openChat(): void {
|
||||
if (stack.depth < 2)
|
||||
stack.push(chatContent);
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stack
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: chatList
|
||||
|
||||
ChatList {
|
||||
model: LlmClient.chats
|
||||
|
||||
onDeleteChatRequest: chat => {
|
||||
const wasCurrent = chat === ChatState.chatSession;
|
||||
LlmClient.chats.remove(chat);
|
||||
if (wasCurrent)
|
||||
ChatState.openDraft();
|
||||
}
|
||||
onLoadChatRequest: (chat, index) => {
|
||||
ChatState.chatSession = chat;
|
||||
openChat();
|
||||
ChatState.inChat = true;
|
||||
}
|
||||
onNewChatRequest: {
|
||||
ChatState.openDraft();
|
||||
openChat();
|
||||
ChatState.inChat = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: chatContent
|
||||
|
||||
ChatContent {
|
||||
chatData: ChatState.chatSession
|
||||
|
||||
onRequestClose: {
|
||||
stack.pop();
|
||||
ChatState.inChat = false;
|
||||
ChatState.openDraft();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Llm
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property ChatSession chatSession: LlmClient.chats.draft()
|
||||
readonly property string title: chatSession?.title || "Chat"
|
||||
property int currentIdx: -1
|
||||
property bool firstOpen: true
|
||||
property bool inChat: false
|
||||
property bool fabExpanded: false
|
||||
property bool isWindow: false
|
||||
property bool isWide: false
|
||||
property bool settledSlim: false
|
||||
property bool settledWide: false
|
||||
property bool narrowSidebarExpanded: false
|
||||
property bool windowIsWide: false
|
||||
property ShellScreen screen
|
||||
property bool sidebarVisible: ShellState.forActive().sidebar
|
||||
|
||||
function openDraft(): void {
|
||||
root.chatSession = LlmClient.chats.draft();
|
||||
root.currentIdx = -1;
|
||||
}
|
||||
|
||||
onSidebarVisibleChanged: if (!sidebarVisible)
|
||||
fabExpanded = false
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property TextEdit edit
|
||||
required property bool hovered
|
||||
required property bool isUser
|
||||
required property ChatMessage message
|
||||
required property bool running
|
||||
|
||||
implicitHeight: editButton.implicitHeight
|
||||
implicitWidth: generationTools.implicitWidth + editTools.implicitWidth + Tokens.spacing.small
|
||||
|
||||
ButtonRow {
|
||||
id: generationTools
|
||||
|
||||
enabled: visible
|
||||
visible: !root.isUser
|
||||
|
||||
IconButton {
|
||||
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== 0
|
||||
icon: "chevron_left"
|
||||
type: IconButton.Tonal
|
||||
isRound: true
|
||||
|
||||
onClicked: {
|
||||
if (!root.edit.readOnly)
|
||||
root.edit.readOnly = true;
|
||||
root.message.setActiveGeneration(root.message.activeGenerationIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: false
|
||||
icon: `${root.message.activeGenerationIndex + 1}`
|
||||
label.font: Tokens.font.body.small
|
||||
type: IconButton.Text
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== root.message.generationCount - 1
|
||||
icon: "chevron_right"
|
||||
type: IconButton.Tonal
|
||||
isRound: true
|
||||
|
||||
onClicked: {
|
||||
if (!root.edit.readOnly)
|
||||
root.edit.readOnly = true;
|
||||
root.message.setActiveGeneration(root.message.activeGenerationIndex + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: editTools
|
||||
|
||||
anchors.right: parent.right
|
||||
opacity: !root.running && root.hovered ? 1 : 0
|
||||
spacing: Tokens.spacing.small
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "refresh"
|
||||
inactiveColor: Colors.palette.m3secondary
|
||||
inactiveOnColor: Colors.palette.m3onSecondary
|
||||
scale: root.edit.readOnly ? 1 : 0
|
||||
visible: !root.isUser
|
||||
isRound: true
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.message.retry();
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "content_copy"
|
||||
inactiveColor: Colors.palette.m3secondary
|
||||
inactiveOnColor: Colors.palette.m3onSecondary
|
||||
scale: root.edit.readOnly ? 1 : 0
|
||||
isRound: true
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: Quickshell.clipboardText = root.message.content
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: editButton
|
||||
|
||||
icon: root.edit.readOnly ? "edit" : "check"
|
||||
inactiveColor: Colors.palette.m3tertiary
|
||||
inactiveOnColor: Colors.palette.m3onTertiary
|
||||
visible: root.isUser
|
||||
isRound: true
|
||||
|
||||
onClicked: {
|
||||
root.edit.readOnly = !root.edit.readOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Llm
|
||||
import ZShell.Blobs
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item rootParent
|
||||
required property ChatGeneration current
|
||||
|
||||
implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2
|
||||
|
||||
CustomRect {
|
||||
id: approvalBg
|
||||
|
||||
anchors.fill: parent
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3surface
|
||||
border.width: 1
|
||||
border.color: Colors.palette.m3outlineVariant
|
||||
|
||||
ColumnLayout {
|
||||
id: approvalLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
id: requestHeader
|
||||
text: qsTr("Tool call request")
|
||||
font: Tokens.font.headline.medium
|
||||
color: Colors.palette.m3onSurface
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolsBg
|
||||
|
||||
property bool open: false
|
||||
property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2
|
||||
property int openHeight: Math.min(toolsContent.item?.implicitHeight, maxHeight)
|
||||
property int openWidth: root.rootParent.width - Tokens.padding.medium * 2
|
||||
property real openHeightProg: openHeight / maxHeight
|
||||
property int collapsedY: 0
|
||||
|
||||
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
|
||||
z: open || dialogTransition.running ? 2 : 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
function reparentWrapper(): void {
|
||||
const newParent = open ? root.rootParent : toolsBg;
|
||||
const c = toolsWrapper.parent.mapToItem(newParent, toolsWrapper.x + toolsWrapper.width / 2, toolsWrapper.y + toolsWrapper.height / 2);
|
||||
toolsBg.collapsedY = toolsWrapper.y;
|
||||
toolsWrapper.parent = newParent;
|
||||
toolsWrapper.x = c.x - toolsWrapper.width / 2;
|
||||
toolsWrapper.y = c.y - toolsWrapper.height / 2;
|
||||
}
|
||||
|
||||
onOpenChanged: {
|
||||
if (open)
|
||||
root.rootParent.interactive = false;
|
||||
else
|
||||
root.rootParent.interactive = true;
|
||||
}
|
||||
|
||||
BlobGroup {
|
||||
id: blobGroup
|
||||
|
||||
color: toolsBg.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3primaryContainer
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backdrop
|
||||
|
||||
anchors.fill: parent
|
||||
enabled: false
|
||||
hoverEnabled: enabled
|
||||
preventStealing: true
|
||||
parent: toolsBg.open ? root.rootParent : toolsBg
|
||||
|
||||
onClicked: toolsBg.open = false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolsWrapper
|
||||
|
||||
width: toolsBg.width
|
||||
height: toolBox.implicitHeight
|
||||
rotation: parent === root.rootParent ? 180 : 0
|
||||
z: 1
|
||||
|
||||
states: State {
|
||||
name: "open"
|
||||
when: toolsBg.open
|
||||
|
||||
PropertyChanges {
|
||||
backdrop.enabled: true
|
||||
toolsBackground.bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||
toolsBackground.bottomRightRadius: Tokens.rounding.largeIncreased
|
||||
toolsBackground.topRightRadius: Tokens.rounding.largeIncreased
|
||||
toolsBackground.topLeftRadius: Tokens.rounding.largeIncreased
|
||||
toolsContent.opacity: 1
|
||||
toolsWrapper.height: toolsBg.openHeight
|
||||
toolsWrapper.width: toolsBg.openWidth
|
||||
toolsWrapper.x: (root.rootParent.width - toolsBg.openWidth) / 2
|
||||
toolsWrapper.y: (confirmationRow.implicitHeight + Tokens.spacing.small + Tokens.padding.medium) * (1 - toolsBg.openHeightProg) + Tokens.padding.medium
|
||||
elevation.opacity: 1
|
||||
toolBox.opacity: 0
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
id: dialogTransition
|
||||
|
||||
SequentialAnimation {
|
||||
ScriptAction {
|
||||
script: toolsBg.reparentWrapper()
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "enabled"
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "width,height"
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
id: elevation
|
||||
|
||||
anchors.fill: parent
|
||||
bottomLeftRadius: toolsBackground.bottomLeftRadius
|
||||
bottomRightRadius: toolsBackground.bottomRightRadius
|
||||
level: 4
|
||||
opacity: 0
|
||||
radius: toolsBackground.radius
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: toolsBackground.deformMatrix
|
||||
}
|
||||
}
|
||||
|
||||
BlobRect {
|
||||
id: toolsBackground
|
||||
|
||||
anchors.fill: parent
|
||||
bottomLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
||||
bottomRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
||||
topRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
||||
topLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
||||
deformScale: 0
|
||||
group: blobGroup
|
||||
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: toolBox
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: Math.min(implicitHeight, parent.height)
|
||||
implicitHeight: toolList.implicitHeight + Tokens.padding.medium * 2
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
StateLayer {
|
||||
onClicked: {
|
||||
toolsBg.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: toolList
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: root.current.pendingToolCalls?.slice(0, 4) ?? []
|
||||
|
||||
delegate: RowLayout {
|
||||
id: toolText
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
function toolTarget(seg: var): string {
|
||||
var a = null;
|
||||
try {
|
||||
a = JSON.parse(seg.arguments);
|
||||
} catch (e) {}
|
||||
if (a && typeof a === "object") {
|
||||
const v = a.url ?? a.path;
|
||||
if (v !== undefined && v !== null)
|
||||
return String(v);
|
||||
}
|
||||
return String(seg.arguments);
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: toolText.modelData.name === "webfetch" ? "web" : "text_snippet"
|
||||
fill: 1
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: qsTr("%1").arg(toolText.modelData.name)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: "arrow_right_alt"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: qsTr("%1").arg(toolText.toolTarget(toolText.modelData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: qsTr("And %1 more").arg(root.current.pendingToolCalls.slice(4).length)
|
||||
visible: repeater.count < root.current.pendingToolCalls.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: toolsContent
|
||||
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
opacity: 0
|
||||
|
||||
sourceComponent: MouseArea {
|
||||
onWheel: event => event.accepted = true
|
||||
preventStealing: true
|
||||
implicitHeight: innerLayout.contentHeight + innerLayout.anchors.margins * 2
|
||||
|
||||
VerticalFadeListView {
|
||||
id: innerLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
cacheBuffer: height * 10
|
||||
fadeAmount: 0.1
|
||||
fadeEnabled: interactive
|
||||
spacing: 0
|
||||
clip: true
|
||||
interactive: contentHeight > toolsBg.maxHeight
|
||||
|
||||
model: root.current.pendingToolCalls ?? []
|
||||
|
||||
delegate: Item {
|
||||
id: toolCall
|
||||
|
||||
required property var modelData
|
||||
property bool expanded: false
|
||||
|
||||
implicitWidth: CustomListView.view.width
|
||||
implicitHeight: collapsedRow.implicitHeight + collapsedRow.anchors.margins + (expanded ? expandedTool.implicitHeight + expandedTool.anchors.margins + expandedTool.anchors.topMargin : collapsedRow.anchors.margins)
|
||||
|
||||
function toolTarget(seg: var): string {
|
||||
var a = null;
|
||||
try {
|
||||
a = JSON.parse(seg.arguments);
|
||||
} catch (e) {}
|
||||
if (a && typeof a === "object") {
|
||||
const v = a.url ?? a.path;
|
||||
if (v !== undefined && v !== null)
|
||||
return String(v);
|
||||
}
|
||||
return String(seg.arguments);
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: collapsedRow
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
IconButton {
|
||||
id: toolExpandBtn
|
||||
|
||||
font: Tokens.font.icon.medium
|
||||
icon: "keyboard_arrow_down"
|
||||
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
rotation: toolCall.expanded ? 180 : 0
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: toolCall.expanded = !toolCall.expanded
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: preview
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: toolCall.modelData.name
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: expandedTool
|
||||
|
||||
anchors.top: collapsedRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
|
||||
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
|
||||
CustomText {
|
||||
id: fullToolCall
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
text: toolCall.toolTarget(toolCall.modelData)
|
||||
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: confirmationRow
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
IconTextButton {
|
||||
icon: "check_circle"
|
||||
text: qsTr("Allow")
|
||||
horizontalPadding: Tokens.padding.large
|
||||
verticalPadding: Tokens.padding.small
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
onClicked: root.current.approveTools()
|
||||
}
|
||||
|
||||
IconTextButton {
|
||||
icon: "cancel"
|
||||
text: qsTr("Deny")
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
inactiveColor: Colors.palette.m3error
|
||||
inactiveOnColor: Colors.palette.m3onError
|
||||
horizontalPadding: Tokens.padding.large
|
||||
verticalPadding: Tokens.padding.small
|
||||
onClicked: root.current.denyTools()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
TextEditBase {
|
||||
id: root
|
||||
|
||||
color: Colors.palette.m3onSurface
|
||||
readOnly: true
|
||||
textFormat: Text.MarkdownText
|
||||
font: Tokens.font.body.small
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: root.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
preventStealing: false
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import QtQuick
|
||||
import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int animOff
|
||||
property Item currentItem
|
||||
property bool noAnim: false
|
||||
property bool loading: false
|
||||
property int lastIdx: -1
|
||||
|
||||
readonly property Component conversationComp: ChatContent {}
|
||||
|
||||
signal requestClose
|
||||
|
||||
function loadConversation(chat): void {
|
||||
if (currentItem) {
|
||||
currentItem.destroy();
|
||||
currentItem = null;
|
||||
}
|
||||
|
||||
if (!chat)
|
||||
return;
|
||||
|
||||
root.loading = true;
|
||||
|
||||
const incubator = root.conversationComp.incubateObject(container, {
|
||||
chatData: chat
|
||||
});
|
||||
|
||||
const attach = () => {
|
||||
incubator.object.anchors.top = container.top;
|
||||
incubator.object.anchors.bottom = container.bottom;
|
||||
incubator.object.anchors.margins = Tokens.padding.medium;
|
||||
incubator.object.anchors.horizontalCenter = container.horizontalCenter;
|
||||
incubator.object.requestClose.connect(root.requestClose);
|
||||
incubator.object.implicitWidth = Qt.binding(() => Math.max(Math.min(container.width - Tokens.padding.medium * 2, 800), Config.sidebar.sizes.width));
|
||||
currentItem = incubator.object;
|
||||
root.loading = false;
|
||||
if (!noAnim)
|
||||
enterAnim.start();
|
||||
};
|
||||
|
||||
if (incubator.status === Component.Ready)
|
||||
attach();
|
||||
else
|
||||
incubator.onStatusChanged = status => {
|
||||
if (status === Component.Ready)
|
||||
attach();
|
||||
};
|
||||
}
|
||||
|
||||
Item {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: opacity < 1
|
||||
objectName: "ConversationContainer"
|
||||
|
||||
Component.onCompleted: {
|
||||
if (ChatState.chatSession)
|
||||
root.loadConversation(ChatState.chatSession);
|
||||
}
|
||||
}
|
||||
|
||||
LoadingIndicator {
|
||||
anchors.centerIn: parent
|
||||
implicitSize: Tokens.font.icon.extraLarge.pointSize * 4
|
||||
opacity: root.loading ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onChatSessionChanged(): void {
|
||||
exitAnim.complete();
|
||||
enterAnim.complete();
|
||||
|
||||
if (root.noAnim) {
|
||||
root.loadConversation(ChatState.chatSession);
|
||||
return;
|
||||
}
|
||||
|
||||
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
|
||||
root.lastIdx = ChatState.currentIdx;
|
||||
exitAnim.start();
|
||||
}
|
||||
|
||||
target: ChatState
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: exitAnim
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: container
|
||||
to: 0
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: root.loadConversation(ChatState.chatSession)
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: enterAnim
|
||||
|
||||
PropertyAction {
|
||||
property: "topMargin"
|
||||
target: container.anchors
|
||||
value: root.animOff
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "bottomMargin"
|
||||
target: container.anchors
|
||||
value: -root.animOff
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
target: container
|
||||
to: 1
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "topMargin,bottomMargin"
|
||||
target: container.anchors
|
||||
to: 0
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property string language
|
||||
required property string code
|
||||
property bool copied: false
|
||||
property color codeBackgroundColor: Colors.palette.m3surfaceContainerHigh
|
||||
property color codeHeaderColor: Colors.palette.m3outline
|
||||
property string html: ""
|
||||
property bool colored: false
|
||||
property string requestedCode: ""
|
||||
property string requestedLanguage: ""
|
||||
property int highlightToken: 0
|
||||
|
||||
function refresh() {
|
||||
requestedCode = root.code;
|
||||
requestedLanguage = root.language;
|
||||
const token = ++root.highlightToken;
|
||||
CodeHighlighter.highlight(root.code, root.language, root, token);
|
||||
}
|
||||
|
||||
function onHighlightSpans(token, spans) {
|
||||
if (token !== root.highlightToken)
|
||||
return;
|
||||
if (root.code !== root.requestedCode || root.language !== root.requestedLanguage)
|
||||
return;
|
||||
html = spans.length ? root.highlightedHtml(root.code, spans) : "";
|
||||
colored = root.visible && html.length > 0;
|
||||
}
|
||||
|
||||
function roleColor(kind) {
|
||||
var s = CodeColors.active;
|
||||
switch (kind) {
|
||||
case "comment":
|
||||
return s.comment;
|
||||
case "string":
|
||||
return s.string;
|
||||
case "string.key":
|
||||
return s.stringKey;
|
||||
case "number":
|
||||
case "constant":
|
||||
return s.number;
|
||||
case "keyword":
|
||||
return s.keyword;
|
||||
case "type":
|
||||
return s.type;
|
||||
case "function":
|
||||
return s.functions;
|
||||
case "method":
|
||||
return s.method ?? s.functions;
|
||||
case "macro":
|
||||
return s.macro;
|
||||
case "preproc":
|
||||
return s.preproc ?? s.macro;
|
||||
case "operator":
|
||||
return s.operator ?? s.normal;
|
||||
case "property":
|
||||
return s.property ?? s.normal;
|
||||
case "label":
|
||||
return s.label;
|
||||
case "attribute":
|
||||
return s.attribute ?? s.label;
|
||||
default:
|
||||
return s.normal;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text): string {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
// Mirrors the whitespace rules of the rich text rendering below (leading
|
||||
// whitespace kept, mid-line runs collapsed, tabs as single spaces) so the
|
||||
// plain and colored layouts are identical.
|
||||
function plainForDisplay(code): string {
|
||||
return code.split("\n").map(line => {
|
||||
const i = line.search(/\S/);
|
||||
if (i < 0)
|
||||
return "";
|
||||
const lead = line.slice(0, i).replace(/\t/g, " ");
|
||||
return lead + line.slice(i).replace(/\s+/g, " ").replace(/\s+$/, "");
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function highlightedHtml(code, spans): string {
|
||||
let out;
|
||||
if (!spans.length) {
|
||||
out = escapeHtml(code);
|
||||
} else {
|
||||
out = "";
|
||||
let pos = 0;
|
||||
for (let i = 0; i < spans.length; i++) {
|
||||
const span = spans[i];
|
||||
const start = Math.min(span.start, code.length);
|
||||
const end = Math.min(span.start + span.length, code.length);
|
||||
if (end <= pos)
|
||||
continue;
|
||||
if (start > pos)
|
||||
out += escapeHtml(code.slice(pos, start));
|
||||
out += `<font color="${roleColor(span.kind)}">` + escapeHtml(code.slice(start, end)) + "</font>";
|
||||
pos = end;
|
||||
}
|
||||
if (pos < code.length)
|
||||
out += escapeHtml(code.slice(pos));
|
||||
}
|
||||
return out.replace(/(^|\n)[ \t]+/g, ws => ws.replace(/[ \t]/g, " ")).replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
implicitWidth: headerRow.implicitWidth + headerRow.anchors.leftMargin + headerRow.anchors.rightMargin
|
||||
implicitHeight: headerRow.anchors.topMargin + headerRow.implicitHeight + codeRect.implicitHeight + codeRect.anchors.margins + codeRect.anchors.topMargin
|
||||
color: root.codeBackgroundColor
|
||||
radius: Tokens.rounding.medium
|
||||
|
||||
onLanguageChanged: {
|
||||
html = "";
|
||||
colored = false;
|
||||
if (root.visible)
|
||||
refreshTimer.restart();
|
||||
}
|
||||
onCodeChanged: {
|
||||
colored = false;
|
||||
if (root.visible)
|
||||
refreshTimer.restart();
|
||||
}
|
||||
onVisibleChanged: if (root.visible)
|
||||
refreshTimer.restart()
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
|
||||
interval: 200
|
||||
repeat: false
|
||||
onTriggered: refresh()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Tokens.padding.small
|
||||
anchors.leftMargin: Tokens.padding.small
|
||||
anchors.rightMargin: Tokens.padding.small
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Item {
|
||||
id: iconItem
|
||||
|
||||
readonly property string iconPath: CodeIcons.path(root.language)
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: height
|
||||
|
||||
Shape {
|
||||
id: shape
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: iconItem.iconPath !== ""
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
|
||||
|
||||
ShapePath {
|
||||
strokeColor: "transparent"
|
||||
fillColor: Colors.palette.m3tertiary
|
||||
fillRule: ShapePath.WindingFill
|
||||
|
||||
PathSvg {
|
||||
path: iconItem.iconPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: fallbackShape
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: -1
|
||||
visible: iconItem.iconPath === ""
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
|
||||
|
||||
ShapePath {
|
||||
strokeColor: Colors.palette.m3tertiary
|
||||
strokeWidth: 16
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
|
||||
PathSvg {
|
||||
path: "M 40 64 L 112 128 L 40 192"
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
strokeColor: Colors.palette.m3tertiary
|
||||
strokeWidth: 16
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
|
||||
PathSvg {
|
||||
path: "M 120 192 L 216 192"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: langText
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: root.codeHeaderColor
|
||||
font: Tokens.font.label.small
|
||||
text: CodeIcons.name(root.language)
|
||||
visible: root.language.length > 0
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: root.copied ? "check" : "content_copy"
|
||||
inactiveColor: "transparent"
|
||||
inactiveOnColor: root.codeHeaderColor
|
||||
label.animate: true
|
||||
type: IconButton.Text
|
||||
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = root.code;
|
||||
root.copied = true;
|
||||
copyResetTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyResetTimer
|
||||
|
||||
interval: 1500
|
||||
|
||||
onTriggered: root.copied = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: codeRect
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: Tokens.padding.small
|
||||
radius: root.radius - anchors.margins
|
||||
anchors.margins: Tokens.padding.extraSmall
|
||||
implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2
|
||||
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
|
||||
color: CodeColors.active.bg
|
||||
|
||||
Flickable {
|
||||
id: codeFlick
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
|
||||
CustomScrollBar.horizontal: CustomScrollBar {
|
||||
flickable: codeFlick
|
||||
parent: codeFlick.parent
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
||||
TextAreaBase.flickable: TextAreaBase {
|
||||
id: codeText
|
||||
|
||||
color: CodeColors.active.normal
|
||||
layer.enabled: true
|
||||
clip: false
|
||||
font: Tokens.font.mono.small
|
||||
textFormat: root.colored ? Text.RichText : Text.PlainText
|
||||
text: root.colored ? root.html : root.plainForDisplay(root.code)
|
||||
readOnly: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property JsonObject schemes: JsonObject {
|
||||
readonly property Scheme oneDark: Scheme {
|
||||
comment: "#5c6370"
|
||||
string: "#98c379"
|
||||
stringKey: "#e06c75"
|
||||
number: "#d19a66"
|
||||
keyword: "#c678dd"
|
||||
type: "#e5c07b"
|
||||
functions: "#61afef"
|
||||
method: "#61afef"
|
||||
macro: "#98c379"
|
||||
preproc: "#abb2bf"
|
||||
operator: "#abb2bf"
|
||||
property: "#abb2bf"
|
||||
label: "#e06c75"
|
||||
attribute: "#d19a66"
|
||||
bg: "#282c34"
|
||||
normal: "#abb2bf"
|
||||
}
|
||||
readonly property Scheme nord: Scheme {
|
||||
comment: "#4c566a"
|
||||
string: "#a3be8c"
|
||||
stringKey: "#ebcb8b"
|
||||
number: "#b48ead"
|
||||
keyword: "#81a1c1"
|
||||
type: "#8fbcbb"
|
||||
functions: "#88c0d0"
|
||||
method: "#88c0d0"
|
||||
macro: "#5e81ac"
|
||||
preproc: "#5e81ac"
|
||||
operator: "#81a1c1"
|
||||
property: "#d8dee9"
|
||||
label: "#d08770"
|
||||
attribute: "#d8dee9"
|
||||
bg: "#2e3440"
|
||||
normal: "#d8dee9"
|
||||
}
|
||||
readonly property Scheme dracula: Scheme {
|
||||
comment: "#6272a4"
|
||||
string: "#f1fa8c"
|
||||
stringKey: "#f8f8f2"
|
||||
number: "#ffb86c"
|
||||
keyword: "#ff79c6"
|
||||
type: "#8be9fd"
|
||||
functions: "#50fa7b"
|
||||
method: "#50fa7b"
|
||||
macro: "#ff79c6"
|
||||
preproc: "#ff79c6"
|
||||
operator: "#f8f8f2"
|
||||
property: "#f8f8f2"
|
||||
label: "#6272a4"
|
||||
attribute: "#8be9fd"
|
||||
bg: "#282a36"
|
||||
normal: "#f8f8f2"
|
||||
}
|
||||
readonly property Scheme githubDark: Scheme {
|
||||
comment: "#8b949e"
|
||||
string: "#a5d6ff"
|
||||
stringKey: "#79c0ff"
|
||||
number: "#79c0ff"
|
||||
keyword: "#ff7b72"
|
||||
type: "#ffa657"
|
||||
functions: "#d2a8ff"
|
||||
method: "#d2a8ff"
|
||||
macro: "#ff7b72"
|
||||
preproc: "#79c0ff"
|
||||
operator: "#ff7b72"
|
||||
property: "#79c0ff"
|
||||
label: "#7ee787"
|
||||
attribute: "#7ee787"
|
||||
bg: "#0d1117"
|
||||
normal: "#c9d1d9"
|
||||
}
|
||||
readonly property Scheme solarizedDark: Scheme {
|
||||
comment: "#586e75"
|
||||
string: "#2aa198"
|
||||
stringKey: "#268bd2"
|
||||
number: "#2aa198"
|
||||
keyword: "#859900"
|
||||
type: "#b58900"
|
||||
functions: "#268bd2"
|
||||
method: "#268bd2"
|
||||
macro: "#cb4b16"
|
||||
preproc: "#cb4b16"
|
||||
operator: "#859900"
|
||||
property: "#268bd2"
|
||||
label: "#6c71c4"
|
||||
attribute: "#657b83"
|
||||
bg: "#002b36"
|
||||
normal: "#839496"
|
||||
}
|
||||
readonly property Scheme monokai: Scheme {
|
||||
comment: "#75715e"
|
||||
string: "#e6db74"
|
||||
stringKey: "#f8f8f2"
|
||||
number: "#ae81ff"
|
||||
keyword: "#f92672"
|
||||
type: "#a6e22e"
|
||||
functions: "#a6e22e"
|
||||
method: "#a6e22e"
|
||||
macro: "#a6e22e"
|
||||
preproc: "#f92672"
|
||||
operator: "#f92672"
|
||||
property: "#fda5ff"
|
||||
label: "#f92672"
|
||||
attribute: "#a6e22e"
|
||||
bg: "#272822"
|
||||
normal: "#f8f8f2"
|
||||
}
|
||||
readonly property Scheme gruvboxDark: Scheme {
|
||||
comment: "#928374"
|
||||
string: "#b8bb26"
|
||||
stringKey: "#ebdbb2"
|
||||
number: "#d3869b"
|
||||
keyword: "#fb4934"
|
||||
type: "#fabd2f"
|
||||
functions: "#b8bb26"
|
||||
method: "#b8bb26"
|
||||
macro: "#8ec07c"
|
||||
preproc: "#8ec07c"
|
||||
operator: "#ebdbb2"
|
||||
property: "#83a598"
|
||||
label: "#fb4934"
|
||||
attribute: "#8ec07c"
|
||||
bg: "#1d2021"
|
||||
normal: "#ebdbb2"
|
||||
}
|
||||
readonly property Scheme catppuccinMocha: Scheme {
|
||||
comment: "#9399b2"
|
||||
string: "#a6e3a1"
|
||||
stringKey: "#b4befe"
|
||||
number: "#fab387"
|
||||
keyword: "#cba6f7"
|
||||
type: "#f9e2af"
|
||||
functions: "#89b4fa"
|
||||
method: "#89b4fa"
|
||||
macro: "#cba6f7"
|
||||
preproc: "#f5c2e7"
|
||||
operator: "#89dceb"
|
||||
property: "#b4befe"
|
||||
label: "#74c7ec"
|
||||
attribute: "#f9e2af"
|
||||
bg: "#1e1e2e"
|
||||
normal: "#cdd6f4"
|
||||
}
|
||||
readonly property Scheme tokyoNight: Scheme {
|
||||
comment: "#565f89"
|
||||
string: "#9ece6a"
|
||||
stringKey: "#73daca"
|
||||
number: "#ff9e64"
|
||||
keyword: "#9d7cd8"
|
||||
type: "#2ac3de"
|
||||
functions: "#7aa2f7"
|
||||
method: "#7aa2f7"
|
||||
macro: "#7dcfff"
|
||||
preproc: "#7dcfff"
|
||||
operator: "#89ddff"
|
||||
property: "#73daca"
|
||||
label: "#7aa2f7"
|
||||
attribute: "#73daca"
|
||||
bg: "#1a1b26"
|
||||
normal: "#c0caf5"
|
||||
}
|
||||
readonly property Scheme ayuDark: Scheme {
|
||||
comment: "#5a6673"
|
||||
string: "#aad94c"
|
||||
stringKey: "#aad94c"
|
||||
number: "#d2a6ff"
|
||||
keyword: "#ff8f40"
|
||||
type: "#59c2ff"
|
||||
functions: "#ffb454"
|
||||
method: "#ffb454"
|
||||
macro: "#59c2ff"
|
||||
preproc: "#ff8f40"
|
||||
operator: "#f29668"
|
||||
property: "#f07178"
|
||||
label: "#59c2ff"
|
||||
attribute: "#ffb454"
|
||||
bg: "#0a0e14"
|
||||
normal: "#bfbdb6"
|
||||
}
|
||||
readonly property Scheme palenight: Scheme {
|
||||
comment: "#697098"
|
||||
string: "#c3e88d"
|
||||
stringKey: "#82b1ff"
|
||||
number: "#f78c6c"
|
||||
keyword: "#ff5370"
|
||||
type: "#ffcb6b"
|
||||
functions: "#82b1ff"
|
||||
method: "#82b1ff"
|
||||
macro: "#c792ea"
|
||||
preproc: "#ffcb6b"
|
||||
operator: "#89ddff"
|
||||
property: "#c3e88d"
|
||||
label: "#c792ea"
|
||||
attribute: "#ffcb6b"
|
||||
bg: "#292d3e"
|
||||
normal: "#bfc7d5"
|
||||
}
|
||||
}
|
||||
readonly property var active: schemes[Config.llm.appearance.scheme]
|
||||
|
||||
component Scheme: JsonObject {
|
||||
property color comment: "#697098"
|
||||
property color string: "#c3e88d"
|
||||
property color stringKey: "#82b1ff"
|
||||
property color number: "#f78c6c"
|
||||
property color keyword: "#ff5370"
|
||||
property color type: "#ffcb6b"
|
||||
property color functions: "#82b1ff"
|
||||
property color method: "#82b1ff"
|
||||
property color macro: "#c792ea"
|
||||
property color preproc: "#ffcb6b"
|
||||
property color operator: "#89ddff"
|
||||
property color property: "#c3e88d"
|
||||
property color label: "#c792ea"
|
||||
property color attribute: "#ffcb6b"
|
||||
property color bg: "#292d3e"
|
||||
property color normal: "#bfc7d5"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,165 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property bool isUser
|
||||
required property string text
|
||||
required property var blocks
|
||||
required property bool running
|
||||
required property bool hovered
|
||||
required property Repeater repeater
|
||||
required property int index
|
||||
required property ChatMessage message
|
||||
|
||||
readonly property real contentMaxWidth: width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge
|
||||
|
||||
signal edit(text: string)
|
||||
|
||||
implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0)
|
||||
|
||||
CustomRect {
|
||||
id: bubble
|
||||
|
||||
property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
|
||||
property real frozenWidth: 0.0
|
||||
property bool layoutFrozen: false
|
||||
property real layoutWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
|
||||
implicitWidth: layoutFrozen ? frozenWidth : layoutWidth
|
||||
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : markdown.implicitHeight + markdown.anchors.topMargin * 2
|
||||
anchors.right: root.isUser ? parent.right : undefined
|
||||
clip: true
|
||||
|
||||
transform: Scale {
|
||||
id: stretchScale
|
||||
origin.x: root.isUser ? bubble.width : 0
|
||||
xScale: bubble.layoutFrozen && bubble.frozenWidth > 0 ? bubble.liveTargetWidth / bubble.frozenWidth : 1.0
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "toSlim"
|
||||
when: ChatState.isWindow && !ChatState.isWide && !ChatState.settledSlim
|
||||
|
||||
PropertyChanges {
|
||||
bubble.layer.enabled: true
|
||||
bubble.layoutFrozen: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "toWide"
|
||||
when: ChatState.isWindow && ChatState.isWide && !ChatState.settledWide
|
||||
|
||||
PropertyChanges {
|
||||
bubble.layer.enabled: true
|
||||
bubble.layoutFrozen: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onLayoutFrozenChanged: {
|
||||
if (layoutFrozen)
|
||||
frozenWidth = width;
|
||||
}
|
||||
|
||||
TextEditBase {
|
||||
id: msgText
|
||||
|
||||
property string cachedText: root.text
|
||||
property bool cancelled: false
|
||||
|
||||
visible: root.isUser
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
animateCursor: false
|
||||
color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary
|
||||
font: Tokens.font.body.small
|
||||
readOnly: true
|
||||
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
|
||||
text: root.isUser ? root.text : ""
|
||||
textFormat: CustomText.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key == Qt.Key_Return) {
|
||||
if (!(event.modifiers & Qt.ShiftModifier)) {
|
||||
readOnly = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key == Qt.Key_Escape) {
|
||||
cancelled = true;
|
||||
readOnly = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onEditingFinished: {
|
||||
readOnly = true;
|
||||
}
|
||||
onReadOnlyChanged: {
|
||||
if (readOnly) {
|
||||
animateCursor = false;
|
||||
textFormat = CustomText.MarkdownText;
|
||||
|
||||
if (cancelled) {
|
||||
text = cachedText;
|
||||
cancelled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
root.edit(text);
|
||||
} else {
|
||||
textFormat = CustomText.PlainText;
|
||||
text = cachedText;
|
||||
forceActiveFocus();
|
||||
cursorPosition = text.length;
|
||||
animateCursor = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownBlocks {
|
||||
id: markdown
|
||||
|
||||
visible: !root.isUser
|
||||
anchors.topMargin: Tokens.padding.medium
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
blocks: root.blocks
|
||||
}
|
||||
}
|
||||
|
||||
Actions {
|
||||
id: actionsRow
|
||||
|
||||
readonly property bool shouldBeActive: !root.running && root.repeater.count === (root.index + 1)
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
|
||||
anchors.top: bubble.bottom
|
||||
opacity: shouldBeActive ? 1 : 0
|
||||
visible: opacity > 0
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
edit: msgText
|
||||
hovered: root.hovered
|
||||
isUser: root.isUser
|
||||
running: root.running
|
||||
message: root.message
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.tPalette.m3outlineVariant
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
text: "chat"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.tPalette.m3onSurfaceVariant
|
||||
text: qsTr("Send a message to get started")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ChatGeneration current
|
||||
|
||||
signal dismiss
|
||||
|
||||
implicitHeight: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
root.implicitHeight = Qt.binding(() => errorInner.implicitHeight + errorInner.anchors.margins * 2);
|
||||
enterAnim.start();
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: enterAnim
|
||||
|
||||
Anim {
|
||||
target: container
|
||||
property: "y"
|
||||
from: container.implicitHeight
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: container
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: exitAnim
|
||||
|
||||
Anim {
|
||||
target: root
|
||||
property: "implicitHeight"
|
||||
from: root.implicitHeight
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: container
|
||||
property: "y"
|
||||
to: container.implicitHeight
|
||||
from: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: container
|
||||
property: "opacity"
|
||||
to: 0
|
||||
from: 1
|
||||
}
|
||||
|
||||
onFinished: root.dismiss()
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: container
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3errorContainer
|
||||
implicitHeight: errorInner.implicitHeight + errorInner.anchors.margins * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: errorInner
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
id: header
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
font: Tokens.font.headline.small
|
||||
elide: CustomText.ElideRight
|
||||
color: Colors.palette.m3onErrorContainer
|
||||
|
||||
text: qsTr("An error occured")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: body
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: CustomText.WordWrap
|
||||
color: Colors.palette.m3onErrorContainer
|
||||
|
||||
text: root.current.errorMessage || LlmClient.serverError
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
isRound: true
|
||||
icon: "close"
|
||||
type: IconButton.Text
|
||||
font: Tokens.font.icon.medium
|
||||
|
||||
onClicked: exitAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property var blocks
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Repeater {
|
||||
id: blockRep
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.blocks
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "type"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MarkdownBlock.Type.Code
|
||||
|
||||
delegate: CodeBlockView {
|
||||
required property MarkdownBlock modelData
|
||||
|
||||
language: modelData.language
|
||||
code: modelData.code
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.spacing.small
|
||||
anchors.left: parent.left
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MarkdownBlock.Type.Math
|
||||
|
||||
delegate: MathBlockView {
|
||||
required property MarkdownBlock modelData
|
||||
|
||||
latex: modelData.latex
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MarkdownBlock.Type.Heading
|
||||
|
||||
delegate: TextEditBase {
|
||||
required property MarkdownBlock modelData
|
||||
|
||||
color: Colors.palette.m3onSurface
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
text: modelData.text
|
||||
textFormat: Text.MarkdownText
|
||||
readOnly: true
|
||||
font: {
|
||||
if (modelData.level <= 1)
|
||||
return Tokens.font.title.builders.large.weight(Font.Bold).build();
|
||||
if (modelData.level === 2)
|
||||
return Tokens.font.title.builders.medium.weight(Font.Bold).build();
|
||||
return Tokens.font.title.builders.small.weight(Font.Bold).build();
|
||||
}
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MarkdownBlock.Type.Text
|
||||
|
||||
delegate: BubbleEdit {
|
||||
required property MarkdownBlock modelData
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
text: modelData.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string latex
|
||||
property color mathColor: Colors.palette.m3onSurface
|
||||
property font font: Tokens.font.headline.medium
|
||||
|
||||
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
|
||||
|
||||
LlmMathText {
|
||||
id: math
|
||||
|
||||
latex: root.latex
|
||||
color: root.mathColor
|
||||
fontPointSize: root.font.pointSize
|
||||
devicePixelRatio: Screen.devicePixelRatio
|
||||
}
|
||||
|
||||
Image {
|
||||
id: equationImage
|
||||
|
||||
anchors.centerIn: parent
|
||||
source: math.imageId.length ? "image://llmmath/" + math.imageId : ""
|
||||
visible: math.ok
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
width: Math.min(math.width, root.width)
|
||||
height: math.width > 0 ? width * (math.height / math.width) : 0
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: fallbackText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.mathColor
|
||||
font: Tokens.font.mono.medium
|
||||
text: root.latex
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
visible: !math.ok
|
||||
}
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
required property int index
|
||||
required property Item rootParent
|
||||
required property ChatMessage modelData
|
||||
required property ChatListView chatList
|
||||
|
||||
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
||||
|
||||
readonly property var blocks: root.isUser ? [
|
||||
{
|
||||
kind: "content",
|
||||
id: 0,
|
||||
text: root.modelData.content,
|
||||
markdown: [],
|
||||
running: false
|
||||
}
|
||||
] : blockify(current.segments, current.error)
|
||||
|
||||
property ChatGeneration current: modelData.activeGeneration
|
||||
property bool reasoningExpanded: false
|
||||
property real savedOffset: NaN
|
||||
property bool anchored: false
|
||||
|
||||
function handleReasoningToggle(expanded: bool, block: Item): void {
|
||||
if (!root.rootParent)
|
||||
return;
|
||||
|
||||
if (expanded) {
|
||||
root.savedOffset = root.rootParent.contentY - root.y;
|
||||
root.anchored = true;
|
||||
chatList.anchorItem(block, ChatListView.TopEdge);
|
||||
settleTimer.start();
|
||||
} else {
|
||||
settleTimer.stop();
|
||||
root.anchored = false;
|
||||
chatList.clearAnchor();
|
||||
if (!isNaN(root.savedOffset)) {
|
||||
chatList.requestRestore(root.y + root.savedOffset);
|
||||
root.savedOffset = NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function blockify(segs, errored) {
|
||||
const blocks = [];
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
const seg = segs[i];
|
||||
if (seg.type === LlmSegment.Type.Content) {
|
||||
blocks.push({
|
||||
kind: "content",
|
||||
id: i,
|
||||
text: seg.text,
|
||||
markdown: seg.markdown,
|
||||
running: seg.running
|
||||
});
|
||||
} else {
|
||||
const last = blocks.length ? blocks[blocks.length - 1] : null;
|
||||
if (last && last.kind === "process") {
|
||||
last.segments.push(seg);
|
||||
} else {
|
||||
blocks.push({
|
||||
kind: "process",
|
||||
id: i,
|
||||
segments: [seg]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errored)
|
||||
blocks.push({
|
||||
kind: "error",
|
||||
id: segs.length
|
||||
});
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
preventStealing: false
|
||||
implicitHeight: layout.implicitHeight + Tokens.padding.medium + (attachments.visible ? attachments.implicitHeight : 0)
|
||||
onImplicitHeightChanged: if (root.anchored)
|
||||
settleTimer.restart()
|
||||
|
||||
Timer {
|
||||
id: settleTimer
|
||||
|
||||
interval: Tokens.anim.durations.expressiveDefaultEffects
|
||||
repeat: false
|
||||
|
||||
onTriggered: {
|
||||
if (root.anchored) {
|
||||
root.anchored = false;
|
||||
chatList.clearAnchor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on current {
|
||||
id: changeAnim
|
||||
|
||||
property var object: targetProperty
|
||||
readonly property bool next: {
|
||||
const cond = root.modelData.generations.indexOf(current) < root.modelData.generations.indexOf(changeAnim.targetValue);
|
||||
return cond;
|
||||
}
|
||||
|
||||
animation: SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: root
|
||||
property: "x"
|
||||
to: changeAnim.next ? root.width / 4 : -root.width / 4
|
||||
from: 0
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
}
|
||||
|
||||
PropertyAction {}
|
||||
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: root
|
||||
property: "x"
|
||||
from: changeAnim.next ? -root.width / 4 : root.width / 4
|
||||
type: Anim.FastEffects
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 0
|
||||
type: Anim.FastEffects
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: attachments
|
||||
|
||||
readonly property int columns: ChatState.isWindow ? 5 : 3
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
flow: GridView.LeftToRight
|
||||
verticalLayoutDirection: GridView.BottomToTop
|
||||
|
||||
cellWidth: width / columns
|
||||
cellHeight: cellWidth
|
||||
|
||||
implicitHeight: Math.ceil(count / columns) * cellHeight
|
||||
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "x"
|
||||
}
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
objectProp: "path"
|
||||
values: {
|
||||
const list = [...root.modelData.attachments];
|
||||
|
||||
if (list.length > 0 && list.length < attachments.columns) {
|
||||
while (list.length < attachments.columns)
|
||||
list.unshift(null);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: thumb
|
||||
|
||||
required property var modelData
|
||||
|
||||
function fileIcon(mime): string {
|
||||
if (mime.startsWith("archive/") || mime === "application/zip")
|
||||
return "folder_zip";
|
||||
if (mime === "application/pdf")
|
||||
return "picture_as_pdf";
|
||||
if (mime.startsWith("audio/"))
|
||||
return "audio_file";
|
||||
if (mime.startsWith("video/"))
|
||||
return "video_file";
|
||||
if (mime.startsWith("text/"))
|
||||
return "description";
|
||||
return "insert_drive_file";
|
||||
}
|
||||
|
||||
width: attachments.cellWidth
|
||||
height: width
|
||||
opacity: modelData ? 1 : 0
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.spacing.extraSmall
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
clip: true
|
||||
|
||||
FadeImage {
|
||||
id: attImage
|
||||
|
||||
anchors.fill: parent
|
||||
fillMode: FadeImage.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
source: visible ? thumb.modelData?.path ?? "" : ""
|
||||
visible: thumb.modelData?.isImage ?? false
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
text: thumb.fileIcon(thumb.modelData?.mimeType ?? "")
|
||||
visible: !thumb.modelData?.isImage ?? false
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: attIndicatorLoader
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
opacity: 0
|
||||
active: opacity > 0 && (thumb.modelData?.isImage ?? false)
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
|
||||
implicitHeight: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
|
||||
|
||||
color: Colors.palette.m3primaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
LoadingIndicator {
|
||||
id: attLoadingIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
containsIcon: true
|
||||
implicitSize: thumb.implicitWidth * 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: attLoadDebounceTimer
|
||||
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
if (attImage.status !== Image.Ready)
|
||||
attIndicatorLoader.opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: layout
|
||||
|
||||
anchors.top: attachments.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
Repeater {
|
||||
id: segmentRep
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.blocks
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "kind"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "error"
|
||||
|
||||
delegate: ErrorBubble {
|
||||
width: root.width
|
||||
current: root.current
|
||||
|
||||
onDismiss: root.current.dismissError()
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "process"
|
||||
|
||||
delegate: ProcessBlock {
|
||||
width: root.width
|
||||
blocks: root.blocks
|
||||
current: root.current
|
||||
rootParent: root.rootParent
|
||||
|
||||
onExpandedChanged: root.handleReasoningToggle(expanded, this)
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "content"
|
||||
|
||||
delegate: ContentBubble {
|
||||
required property var modelData
|
||||
|
||||
isUser: root.isUser
|
||||
text: modelData.text
|
||||
blocks: modelData.markdown
|
||||
running: modelData.running
|
||||
width: root.width
|
||||
repeater: segmentRep
|
||||
message: root.modelData
|
||||
hovered: root.containsMouse
|
||||
|
||||
onEdit: text => {
|
||||
root.modelData.edit(text);
|
||||
|
||||
if (root.isUser)
|
||||
root.modelData.generate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Llm
|
||||
import ZShell.Blobs
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item rootParent
|
||||
required property Item fabRoot
|
||||
|
||||
property bool open: false
|
||||
|
||||
property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2
|
||||
property int openHeight: Math.min(modelsContent.item?.implicitHeight, maxHeight)
|
||||
property int openWidth: root.rootParent.width - Tokens.padding.medium * 2
|
||||
property real openHeightProg: openHeight / maxHeight
|
||||
property int collapsedY: 0
|
||||
|
||||
property int horizontalContentMargin
|
||||
property string header: qsTr("Available models")
|
||||
property string acceptLabel: qsTr("Accept")
|
||||
|
||||
z: open || dialogTransition.running ? 2 : 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
implicitHeight: fabRoot.implicitHeight
|
||||
|
||||
function prettyModelName(): string {
|
||||
if (modelsContent.item?.modelsList.count < 1)
|
||||
return qsTr("No models found");
|
||||
|
||||
const pathList = LlmClient.model.split("\/");
|
||||
const name = pathList[pathList.length - 1];
|
||||
|
||||
if (!name)
|
||||
return qsTr("No model selected");
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
function reparentWrapper(): void {
|
||||
const newParent = open ? root.rootParent : root;
|
||||
const c = modelsWrapper.parent.mapToItem(newParent, modelsWrapper.x + modelsWrapper.width / 2, modelsWrapper.y + modelsWrapper.height / 2);
|
||||
root.collapsedY = modelsWrapper.y;
|
||||
modelsWrapper.parent = newParent;
|
||||
modelsWrapper.x = c.x - modelsWrapper.width / 2;
|
||||
modelsWrapper.y = c.y - modelsWrapper.height / 2;
|
||||
}
|
||||
|
||||
BlobGroup {
|
||||
id: blobGroup
|
||||
|
||||
color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backdrop
|
||||
|
||||
anchors.fill: parent
|
||||
enabled: false
|
||||
hoverEnabled: enabled
|
||||
preventStealing: true
|
||||
parent: root.open ? root.rootParent : root
|
||||
|
||||
onClicked: root.open = false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modelsWrapper
|
||||
|
||||
width: root.width
|
||||
height: modelBox.implicitHeight
|
||||
z: 1
|
||||
|
||||
states: State {
|
||||
name: "open"
|
||||
when: root.open
|
||||
|
||||
PropertyChanges {
|
||||
backdrop.enabled: true
|
||||
modelsBackground.radius: Tokens.rounding.largeIncreased
|
||||
modelsContent.opacity: 1
|
||||
modelsWrapper.height: root.openHeight
|
||||
modelsWrapper.width: root.openWidth
|
||||
modelsWrapper.x: (root.rootParent.width - root.openWidth) / 2
|
||||
modelsWrapper.y: root.rootParent.height - root.openHeight - Tokens.spacing.medium - Tokens.padding.medium - root.fabRoot.implicitHeight
|
||||
elevation.opacity: 1
|
||||
modelBox.opacity: 0
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
id: dialogTransition
|
||||
|
||||
SequentialAnimation {
|
||||
ScriptAction {
|
||||
script: root.reparentWrapper()
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "enabled"
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "opacity,radius"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "width,height"
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
id: elevation
|
||||
|
||||
anchors.fill: parent
|
||||
level: 4
|
||||
opacity: 0
|
||||
radius: modelsBackground.radius
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: modelsBackground.deformMatrix
|
||||
}
|
||||
}
|
||||
|
||||
BlobRect {
|
||||
id: modelsBackground
|
||||
|
||||
anchors.fill: parent
|
||||
radius: modelLayer.pressed ? Tokens.rounding.small : Tokens.rounding.extraLarge
|
||||
deformScale: 0
|
||||
group: blobGroup
|
||||
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: modelBox
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: root.fabRoot.implicitHeight
|
||||
radius: modelsBackground.radius
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
text: root.prettyModelName()
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "unfold_more"
|
||||
fontStyle: Tokens.font.icon.large
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: modelLayer
|
||||
|
||||
onClicked: {
|
||||
ChatState.fabExpanded = false;
|
||||
root.open = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: modelsContent
|
||||
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
opacity: 0
|
||||
|
||||
sourceComponent: MouseArea {
|
||||
readonly property alias modelsList: modelsList
|
||||
|
||||
onWheel: event => event.accepted = true
|
||||
preventStealing: true
|
||||
clip: true
|
||||
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: innerLayout
|
||||
anchors.bottomMargin: Tokens.padding.largeIncreased
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.extraLarge
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
font: Tokens.font.title.builders.large.weight(Font.Normal).build()
|
||||
text: root.header
|
||||
}
|
||||
|
||||
VerticalFadeListView {
|
||||
id: modelsList
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: root.horizontalContentMargin
|
||||
Layout.topMargin: Tokens.spacing.medium
|
||||
Layout.bottomMargin: Tokens.spacing.medium
|
||||
Layout.rightMargin: root.horizontalContentMargin
|
||||
Layout.preferredHeight: 400
|
||||
Layout.maximumHeight: 400
|
||||
model: LlmClient.availableModels
|
||||
currentIndex: LlmClient.availableModels.indexOf(LlmClient.model)
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: modelsList
|
||||
}
|
||||
|
||||
delegate: CustomRect {
|
||||
id: model
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property bool selected: ListView.view.currentIndex === model.index
|
||||
|
||||
implicitWidth: ListView.view.width
|
||||
implicitHeight: name.implicitHeight + Tokens.padding.small * 2
|
||||
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
|
||||
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.leftMargin: Tokens.padding.large
|
||||
anchors.rightMargin: Tokens.padding.large
|
||||
|
||||
CustomText {
|
||||
id: name
|
||||
|
||||
text: model.modelData
|
||||
elide: CustomText.ElideRight
|
||||
color: model.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: check
|
||||
|
||||
color: Colors.palette.m3onTertiaryContainer
|
||||
fontStyle: Tokens.font.icon.medium
|
||||
opacity: model.selected ? 1 : 0
|
||||
visible: opacity > 0
|
||||
text: "check"
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
onClicked: {
|
||||
model.ListView.view.currentIndex = model.index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
TextButton {
|
||||
horizontalPadding: Tokens.padding.largeIncreased
|
||||
isRound: true
|
||||
text: qsTr("Cancel")
|
||||
type: TextButton.Text
|
||||
verticalPadding: Tokens.padding.small
|
||||
stateLayer.preventStealing: true
|
||||
|
||||
onClicked: {
|
||||
root.open = false;
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
horizontalPadding: Tokens.padding.largeIncreased
|
||||
isRound: true
|
||||
text: root.acceptLabel
|
||||
type: TextButton.Text
|
||||
stateLayer.preventStealing: true
|
||||
verticalPadding: Tokens.padding.small
|
||||
|
||||
onClicked: {
|
||||
LlmClient.selectModel(modelsList.currentItem.modelData);
|
||||
root.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Blobs
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
required property var blocks
|
||||
readonly property var segments: modelData.segments
|
||||
required property Item rootParent
|
||||
required property ChatGeneration current
|
||||
readonly property bool isActive: index === blocks.length - 1
|
||||
property bool expanded: false
|
||||
readonly property var lastSegment: root.segments[root.segments.length - 1]
|
||||
readonly property real totalElapsedMs: root.segments.reduce((sum, s) => sum + (s.elapsedMs ?? 0), 0)
|
||||
readonly property bool pendingApproval: current.toolApprovalPending && segments.some(s => s.type === LlmSegment.Type.ToolCall && s.status === LlmToolCall.Status.Pending)
|
||||
|
||||
implicitHeight: {
|
||||
const expand = expandedRect.implicitHeight + layout.implicitHeight + expandedRect.anchors.topMargin;
|
||||
const collapsed = layout.implicitHeight;
|
||||
const approval = layout.implicitHeight + approvalPrompt.implicitHeight + approvalPrompt.anchors.topMargin;
|
||||
|
||||
if (expanded)
|
||||
return expand;
|
||||
else if (pendingApproval)
|
||||
return approval;
|
||||
else
|
||||
return collapsed;
|
||||
}
|
||||
|
||||
onCurrentChanged: expanded = false
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Item {
|
||||
implicitHeight: expandBtn.implicitHeight
|
||||
implicitWidth: expandBtn.implicitWidth
|
||||
|
||||
LoadingIndicator {
|
||||
id: spinnerReasoning
|
||||
|
||||
implicitSize: collapsedText.implicitHeight
|
||||
anchors.centerIn: parent
|
||||
opacity: root.isActive ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: expandBtn
|
||||
|
||||
font: Tokens.font.icon.medium
|
||||
icon: "keyboard_arrow_down"
|
||||
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
anchors.centerIn: parent
|
||||
opacity: root.isActive ? 0 : 1
|
||||
enabled: opacity > 0
|
||||
rotation: root.expanded ? 180 : 0
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: collapsedText
|
||||
|
||||
color: Colors.palette.m3outline
|
||||
font: Tokens.font.label.small
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Tokens.spacing.medium
|
||||
text: {
|
||||
if (root.isActive) {
|
||||
if (root.pendingApproval)
|
||||
return qsTr("Waiting for approval...");
|
||||
|
||||
if (root.lastSegment.type === LlmSegment.Type.ToolCall)
|
||||
return qsTr("Using %1...").arg(root.lastSegment.name);
|
||||
return qsTr("Thinking...");
|
||||
}
|
||||
return qsTr("Worked for %1s").arg((root.totalElapsedMs / 1000).toFixed(1));
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApprovalPrompt {
|
||||
id: approvalPrompt
|
||||
|
||||
anchors.top: layout.bottom
|
||||
anchors.topMargin: Tokens.spacing.small
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: root.pendingApproval
|
||||
|
||||
rootParent: root.rootParent
|
||||
current: root.current
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: expandedRect
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: layout.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: Tokens.spacing.small
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
||||
opacity: root.expanded ? 1 : 0
|
||||
radius: Tokens.rounding.medium
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: expandedContent
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
model: root.segments
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
delegate: Item {
|
||||
id: segment
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
implicitWidth: ListView.view.width
|
||||
implicitHeight: header.implicitHeight + content.implicitHeight + content.anchors.topMargin
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
text: segment.modelData.type === LlmSegment.Type.Reasoning ? "cognition" : "build"
|
||||
fill: 1
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: segment.modelData.type === LlmSegment.Type.Reasoning ? qsTr("Thought for %1s").arg((segment.modelData.elapsedMs / 1000).toFixed(1)) : qsTr("Used %1 for %2s").arg(segment.modelData.name).arg((segment.modelData.elapsedMs / 1000).toFixed(1))
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: content
|
||||
|
||||
function toolTarget(seg: var): string {
|
||||
var a = null;
|
||||
try {
|
||||
a = JSON.parse(seg.arguments);
|
||||
} catch (e) {}
|
||||
if (a && typeof a === "object") {
|
||||
const v = a.url ?? a.path;
|
||||
if (v !== undefined && v !== null)
|
||||
return String(v);
|
||||
}
|
||||
return String(seg.arguments);
|
||||
}
|
||||
|
||||
function prettyTool(seg: var): string {
|
||||
if (seg.name === "webfetch") {
|
||||
switch (seg.status) {
|
||||
case LlmToolCall.Status.Running:
|
||||
return qsTr("Fetching");
|
||||
case LlmToolCall.Status.Pending:
|
||||
return qsTr("Pending fetch");
|
||||
case LlmToolCall.Status.Success:
|
||||
return qsTr("Fetched");
|
||||
case LlmToolCall.Status.Error:
|
||||
return qsTr("Failed to fetch");
|
||||
default:
|
||||
return qsTr("Fetched website");
|
||||
}
|
||||
} else if (seg.name === "readfile") {
|
||||
switch (seg.status) {
|
||||
case LlmToolCall.Status.Running:
|
||||
return qsTr("Reading");
|
||||
case LlmToolCall.Status.Pending:
|
||||
return qsTr("Pending read");
|
||||
case LlmToolCall.Status.Success:
|
||||
return qsTr("Read");
|
||||
case LlmToolCall.Status.Error:
|
||||
return qsTr("Failed to read");
|
||||
default:
|
||||
return qsTr("Read file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.small
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Tokens.spacing.small
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
font: Tokens.font.body.small
|
||||
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
|
||||
text: segment.modelData.type === LlmSegment.Type.Reasoning ? segment.modelData.text : qsTr("%1 %2").arg(content.prettyTool(segment.modelData)).arg(content.toolTarget(segment.modelData))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function create(parent: Item, props: var): void {
|
||||
chatComp.createObject(parent ?? dummy, props);
|
||||
ShellState.forActive().sidebar = false;
|
||||
ChatState.isWindow = true;
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: dummy
|
||||
}
|
||||
|
||||
Component {
|
||||
id: chatComp
|
||||
|
||||
FloatingWindow {
|
||||
id: win
|
||||
|
||||
property var props
|
||||
|
||||
color: Colors.tPalette.m3surface
|
||||
implicitHeight: chat.implicitHeight
|
||||
implicitWidth: chat.implicitWidth
|
||||
minimumSize.height: Config.sidebar.sizes.width
|
||||
minimumSize.width: Config.sidebar.sizes.width
|
||||
surfaceFormat.opaque: false
|
||||
title: qsTr("ZShell - %1").arg(ChatState.title)
|
||||
|
||||
Component.onCompleted: ChatState.screen = screen
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
destroy();
|
||||
ChatState.isWindow = false;
|
||||
}
|
||||
}
|
||||
|
||||
SidebarView {
|
||||
id: chat
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import ZShell.Blobs
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int breakpoint: Math.round(Config.sidebar.sizes.width * 2) + Tokens.spacing.medium * 2 + Tokens.padding.medium * 3
|
||||
property color blobColor: Colors.tPalette.m3surfaceContainerLow
|
||||
readonly property int iconSize: Math.round(Tokens.font.icon.extraLarge.pointSize * (96 / 72)) + Tokens.padding.large * 2
|
||||
property alias conversationModel: sidebar.model
|
||||
property bool chatOpen: ChatState.chatSession !== null
|
||||
|
||||
readonly property bool isWide: root.width >= root.breakpoint
|
||||
|
||||
readonly property bool showList: isWide || !chatOpen
|
||||
readonly property bool showContent: !isWide && chatOpen
|
||||
|
||||
function openConversation(conv: ChatSession, index: int): void {
|
||||
}
|
||||
|
||||
BlobGroup {
|
||||
id: blobGroup
|
||||
|
||||
color: root.blobColor
|
||||
smoothing: Tokens.rounding.medium
|
||||
}
|
||||
|
||||
BlobInvertedRect {
|
||||
id: invertedRect
|
||||
anchors.fill: parent
|
||||
borderBottom: Tokens.padding.small
|
||||
borderLeft: sidebar.implicitWidth + sidebar.anchors.margins + Tokens.spacing.medium
|
||||
borderRight: Tokens.padding.small
|
||||
borderTop: Tokens.padding.small
|
||||
group: blobGroup
|
||||
opacity: root.blobColor.a
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
}
|
||||
|
||||
ChatList {
|
||||
id: sidebar
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.medium
|
||||
implicitWidth: root.isWide || ChatState.narrowSidebarExpanded ? Config.sidebar.sizes.width : root.iconSize
|
||||
highlight: true
|
||||
slim: !root.isWide && !ChatState.narrowSidebarExpanded
|
||||
z: 1
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
model: LlmClient.chats
|
||||
|
||||
onLoadChatRequest: (chat, index) => {
|
||||
ChatState.currentIdx = index;
|
||||
ChatState.chatSession = chat;
|
||||
}
|
||||
|
||||
onDeleteChatRequest: chat => {
|
||||
const wasCurrent = chat === ChatState.chatSession;
|
||||
LlmClient.chats.remove(chat);
|
||||
|
||||
if (wasCurrent)
|
||||
ChatState.openDraft();
|
||||
}
|
||||
onNewChatRequest: ChatState.openDraft()
|
||||
|
||||
onRequestExpand: {
|
||||
ChatState.narrowSidebarExpanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: invertedRect.borderLeft
|
||||
anchors.topMargin: invertedRect.borderTop
|
||||
anchors.bottomMargin: invertedRect.borderBottom
|
||||
anchors.rightMargin: invertedRect.borderRight
|
||||
radius: invertedRect.radius
|
||||
|
||||
children: [contentArea, dim]
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: dim
|
||||
anchors.fill: parent
|
||||
|
||||
color: Colors.palette.m3shadow
|
||||
opacity: ChatState.narrowSidebarExpanded && !root.isWide ? 0.3 : 0
|
||||
z: 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentArea
|
||||
|
||||
property int leftMargin: sidebar.implicitWidth
|
||||
|
||||
z: 0
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2
|
||||
|
||||
states: State {
|
||||
name: "wide"
|
||||
when: !root.isWide
|
||||
|
||||
PropertyChanges {
|
||||
contentArea.leftMargin: root.iconSize
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
to: "wide"
|
||||
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
property: "leftMargin"
|
||||
}
|
||||
}
|
||||
|
||||
ChatHost {
|
||||
id: convHost
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
onLoadingChanged: if (!loading)
|
||||
root.chatOpen = true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dimArea
|
||||
anchors.fill: parent
|
||||
preventStealing: true
|
||||
enabled: !root.isWide && ChatState.narrowSidebarExpanded
|
||||
cursorShape: undefined
|
||||
|
||||
onClicked: {
|
||||
const x = mouseX;
|
||||
const margin = root.width - contentArea.implicitWidth - contentArea.anchors.margins * 2;
|
||||
|
||||
if (x > invertedRect.borderLeft - margin) {
|
||||
ChatState.narrowSidebarExpanded = false;
|
||||
ChatState.fabExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: dimArea.enabled ? Qt.ArrowCursor : undefined
|
||||
enabled: dimArea.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: ChatState
|
||||
property: "windowIsWide"
|
||||
value: root.showList
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: ChatState
|
||||
property: "isWide"
|
||||
value: root.isWide
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: ChatState
|
||||
value: contentArea.leftMargin === root.iconSize
|
||||
property: "settledSlim"
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: ChatState
|
||||
value: contentArea.leftMargin === sidebar.implicitWidth
|
||||
property: "settledWide"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: conversationView
|
||||
|
||||
ChatContent {}
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ScreenState screenState
|
||||
|
||||
Connections {
|
||||
function onIsWindowChanged(): void {
|
||||
if (ChatState.isWindow)
|
||||
root.screenState.sidebarTab = 0;
|
||||
}
|
||||
|
||||
target: ChatState
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Tabs {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: ChatState.isWindow || !Config.llm.enabled ? 0 : implicitHeight
|
||||
screenState: root.screenState
|
||||
nonAnimWidth: layout.width
|
||||
visible: height > 0
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
|
||||
Item {
|
||||
id: pages
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: parent.top
|
||||
implicitWidth: parent.width
|
||||
opacity: root.screenState.sidebarTab === 0 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
x: root.screenState.sidebarTab === 0 ? 0 : -root.width
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
color: Colors.tPalette.m3surfaceContainerLow
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
NotifDock {
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: chatLoader
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: parent.top
|
||||
active: Config.llm.enabled
|
||||
width: parent.width
|
||||
property bool tre: true
|
||||
|
||||
sourceComponent: Item {
|
||||
id: chatPage
|
||||
|
||||
opacity: root.screenState.sidebarTab === 1 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
x: root.screenState.sidebarTab === 0 ? root.width : 0
|
||||
z: 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: Colors.tPalette.m3surfaceContainerLow
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
ChatPanel {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
GeminiGlow {
|
||||
innerGlow: 50
|
||||
z: 5
|
||||
opacity: LlmClient.busy ? 1.0 : 0.0
|
||||
glowSize: 0
|
||||
visible: opacity > 0 && !ChatState.isWindow
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: fadeMask
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: fadeMask
|
||||
|
||||
readonly property real fadeStartPos: {
|
||||
const h = Math.max(1, height);
|
||||
const nh = (h - h / 3) / h;
|
||||
return nh;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: fadeMask.fadeStartPos
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 1.0)
|
||||
position: 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Tokens.padding.small - layout.spacing
|
||||
color: Colors.tPalette.m3outlineVariant
|
||||
implicitHeight: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Daemons
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null
|
||||
required property bool expanded
|
||||
required property NotifServer.Notif modelData
|
||||
readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height
|
||||
required property ScreenState screenState
|
||||
|
||||
color: {
|
||||
const c = root.modelData?.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2);
|
||||
return expanded ? c : Qt.alpha(c, 0);
|
||||
}
|
||||
implicitHeight: nonAnimHeight
|
||||
radius: Tokens.rounding.large - Tokens.padding.small
|
||||
state: expanded ? "expanded" : ""
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
states: State {
|
||||
name: "expanded"
|
||||
|
||||
PropertyChanges {
|
||||
compactBody.anchors.margins: Tokens.padding.medium
|
||||
dummySummary.anchors.margins: Tokens.padding.medium
|
||||
expandedContent.anchors.margins: Tokens.padding.medium
|
||||
summary.anchors.margins: Tokens.padding.medium
|
||||
summary.maximumLineCount: Number.MAX_SAFE_INTEGER
|
||||
summary.width: root.width - Tokens.padding.medium * 2 - timeStr.implicitWidth - Tokens.spacing.small
|
||||
timeStr.anchors.margins: Tokens.padding.medium
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "margins,width,maximumLineCount"
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: summaryHeightMetrics
|
||||
|
||||
font: summary.font
|
||||
text: " " // Use this height to prevent weird characters from changing the line height
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: summary
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
text: root.modelData?.summary ?? ""
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: dummySummary
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text: root.modelData?.summary ?? ""
|
||||
visible: false
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: compactBody
|
||||
|
||||
anchors.left: dummySummary.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
shouldBeActive: !root.expanded
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
|
||||
}
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: timeStr
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
shouldBeActive: root.expanded
|
||||
|
||||
sourceComponent: CustomText {
|
||||
animate: true
|
||||
color: Colors.palette.m3outline
|
||||
text: root.modelData?.timeStr ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: expandedContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: summary.bottom
|
||||
anchors.topMargin: Tokens.spacing.extraSmall
|
||||
shouldBeActive: root.expanded
|
||||
|
||||
sourceComponent: ExpandedBody {
|
||||
}
|
||||
}
|
||||
|
||||
component ExpandedBody: ColumnLayout {
|
||||
readonly property alias body: bodyText
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
CustomText {
|
||||
id: bodyText
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3onSurface
|
||||
text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
|
||||
textFormat: Text.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
root.screenState.sidebar = false;
|
||||
}
|
||||
}
|
||||
|
||||
NotifActionList {
|
||||
notif: root.modelData
|
||||
}
|
||||
}
|
||||
component WrappedLoader: Loader {
|
||||
id: comp
|
||||
|
||||
required property bool shouldBeActive
|
||||
|
||||
active: false
|
||||
opacity: 0
|
||||
|
||||
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||
states: State {
|
||||
name: "active"
|
||||
when: comp.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
comp.active: true
|
||||
comp.opacity: 1
|
||||
}
|
||||
}
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Effects
|
||||
import qs.Services
|
||||
import qs.Daemons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property NotifServer.Notif notif
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: flickable.contentHeight
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
layer.effect: Mask {
|
||||
maskSource: gradientMask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gradientMask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
position: 0.1
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
position: 0.9
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
implicitWidth: parent.width / 2
|
||||
opacity: flickable.contentX > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitWidth: parent.width / 2
|
||||
opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomFlickable {
|
||||
id: flickable
|
||||
|
||||
anchors.fill: parent
|
||||
contentHeight: actionList.implicitHeight
|
||||
contentWidth: Math.max(width, actionList.implicitWidth)
|
||||
|
||||
RowLayout {
|
||||
id: actionList
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
isClose: true
|
||||
},
|
||||
...(root.notif?.actions ?? []),
|
||||
{
|
||||
isCopy: true
|
||||
}
|
||||
]
|
||||
|
||||
CustomRect {
|
||||
id: action
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0)
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 4)
|
||||
implicitHeight: actionInner.implicitHeight + Tokens.padding.small
|
||||
implicitWidth: actionInner.implicitWidth + Tokens.padding.medium * 2
|
||||
radius: actionStateLayer.pressed ? Tokens.rounding.medium / 2 : Tokens.rounding.large
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
|
||||
interval: 3000
|
||||
|
||||
onTriggered: actionInner.item.text = "content_copy"
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: actionStateLayer
|
||||
|
||||
onClicked: {
|
||||
if (action.modelData.isClose) {
|
||||
root.notif.close();
|
||||
} else if (action.modelData.isCopy) {
|
||||
Quickshell.clipboardText = root.notif.body;
|
||||
actionInner.item.text = "inventory";
|
||||
copyTimer.start();
|
||||
} else if (action.modelData.invoke) {
|
||||
action.modelData.invoke();
|
||||
} else if (!root.notif.resident) {
|
||||
root.notif.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: actionInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp
|
||||
}
|
||||
|
||||
Component {
|
||||
id: iconBtn
|
||||
|
||||
MaterialIcon {
|
||||
animate: action.modelData.isCopy ?? false
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: action.modelData.isCopy ? "content_copy" : "close"
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: iconComp
|
||||
|
||||
IconImage {
|
||||
asynchronous: true
|
||||
source: Quickshell.iconPath(action.modelData.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComp
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: action.modelData.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Modules
|
||||
import qs.Daemons
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
|
||||
required property ScreenState screenState
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
|
||||
Component.onCompleted: NotifServer.list.forEach(n => n.popup = false)
|
||||
|
||||
Item {
|
||||
id: title
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 4
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight)
|
||||
|
||||
CustomText {
|
||||
id: count
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3outline
|
||||
font: Tokens.font.label.large
|
||||
opacity: root.notifCount > 0 ? 1 : 0
|
||||
text: root.notifCount
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: titleText
|
||||
|
||||
anchors.left: count.right
|
||||
anchors.leftMargin: 7
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.label.large
|
||||
text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: clipRect
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: title.bottom
|
||||
anchors.topMargin: 10
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: root.notifCount > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: 20
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Notifications")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomFlickable {
|
||||
id: view
|
||||
|
||||
anchors.fill: parent
|
||||
contentHeight: notifList.implicitHeight
|
||||
contentWidth: width
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: view
|
||||
}
|
||||
|
||||
NotifDockList {
|
||||
id: notifList
|
||||
|
||||
container: view
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
const first = NotifServer.notClosed[0];
|
||||
if (!first) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = first.appName;
|
||||
let cleared = 0;
|
||||
for (const n of NotifServer.notClosed.filter(n => n.appName === appName)) {
|
||||
n.close();
|
||||
cleared++;
|
||||
if (cleared > 30) {
|
||||
interval = 5;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.right: parent.right
|
||||
opacity: root.notifCount > 0 ? 1 : 0
|
||||
scale: root.notifCount > 0 ? 1 : 0.5
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: IconButton {
|
||||
id: clearBtn
|
||||
|
||||
font: Tokens.font.icon.large
|
||||
icon: "clear_all"
|
||||
isRound: true
|
||||
|
||||
onClicked: clearTimer.start()
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: clearBtn.stateLayer.containsMouse ? 4 : 3
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property Flickable container
|
||||
required property ScreenState screenState
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
asynchronous: true
|
||||
cacheBuffer: 400
|
||||
implicitHeight: contentHeight
|
||||
readyDelay: 1
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
spacing: Tokens.spacing.small
|
||||
useCustomViewport: true
|
||||
viewport: Qt.rect(0, container.contentY, width, container.height)
|
||||
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
readonly property bool closed: notifInner.notifCount === 0
|
||||
required property int index
|
||||
required property string modelData
|
||||
property int startY
|
||||
|
||||
function closeAll(): void {
|
||||
clearTimer.start();
|
||||
}
|
||||
|
||||
LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight
|
||||
LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight
|
||||
LazyListView.visibleHeight: notifInner.implicitHeight
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
||||
drag.axis: Drag.XAxis
|
||||
drag.target: this
|
||||
enabled: !closed
|
||||
hoverEnabled: true
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
||||
preventStealing: true
|
||||
scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
notifInner.toggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
notifInner.toggleExpand(!notifInner.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
closeAll();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
closeAll();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
interval: 15
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
const notifs = NotifServer.notClosed.filter(n => n.appName === notif.modelData);
|
||||
if (notifs.length === 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const n of notifs.slice(0, 30))
|
||||
n.close();
|
||||
}
|
||||
}
|
||||
|
||||
NotifGroup {
|
||||
id: notifInner
|
||||
|
||||
container: root.container
|
||||
modelData: notif.modelData
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const map = new Map();
|
||||
for (const n of NotifServer.notClosed)
|
||||
map.set(n.appName, null);
|
||||
for (const n of NotifServer.list)
|
||||
map.set(n.appName, null);
|
||||
return [...map.keys()];
|
||||
}
|
||||
}
|
||||
|
||||
onViewportAdjustNeeded: d => {
|
||||
if (contentYAnim.running)
|
||||
contentYAnim.complete();
|
||||
contentYAnim.to = Math.max(0, container.contentY + d);
|
||||
contentYAnim.start();
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: contentYAnim
|
||||
|
||||
property: "contentY"
|
||||
target: root.container
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
import qs.Daemons
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property list<var> activeNotifs: notifs.filter(n => !n.closed)
|
||||
readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
|
||||
required property Flickable container
|
||||
readonly property bool expanded: screenState.expandedNotifs.includes(modelData)
|
||||
readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? ""
|
||||
required property string modelData
|
||||
readonly property int nonAnimHeight: {
|
||||
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0);
|
||||
const columnHeight = headerHeight + notifList.layoutHeight;
|
||||
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.small * 2);
|
||||
}
|
||||
readonly property int notifCount: activeNotifs.length
|
||||
readonly property list<var> notifs: NotifServer.list.filter(n => n.appName === modelData)
|
||||
required property ScreenState screenState
|
||||
readonly property int urgency: {
|
||||
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical))
|
||||
return NotificationUrgency.Critical;
|
||||
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal))
|
||||
return NotificationUrgency.Normal;
|
||||
return NotificationUrgency.Low;
|
||||
}
|
||||
|
||||
function toggleExpand(expand: bool): void {
|
||||
if (expand) {
|
||||
if (!expanded)
|
||||
screenState.expandedNotifs.push(modelData);
|
||||
} else if (expanded) {
|
||||
screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
|
||||
}
|
||||
}
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
clip: true
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainer, 2)
|
||||
implicitHeight: nonAnimHeight
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (notifCount === 0 && expanded)
|
||||
screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
|
||||
Component {
|
||||
id: imageComp
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
height: Config.notifs.sizes.image
|
||||
source: Qt.resolvedUrl(root.image)
|
||||
sourceSize: {
|
||||
const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
||||
return Qt.size(size, size);
|
||||
}
|
||||
width: Config.notifs.sizes.image
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appIconComp
|
||||
|
||||
ColoredIcon {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
implicitSize: Math.round(Config.notifs.sizes.image * 0.6)
|
||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: materialIconComp
|
||||
|
||||
MaterialIcon {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency)
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) : Colors.palette.m3secondaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0
|
||||
asynchronous: true
|
||||
sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.appIcon && root.image
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.palette.m3surfaceContainerHigh : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: Config.notifs.sizes.badge
|
||||
implicitWidth: Config.notifs.sizes.badge
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
ColoredIcon {
|
||||
anchors.centerIn: parent
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)
|
||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.appIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0
|
||||
|
||||
Behavior on spacing {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
text: root.modelData
|
||||
}
|
||||
|
||||
CustomText {
|
||||
animate: true
|
||||
color: Colors.palette.m3outline
|
||||
text: root.activeNotifs[0]?.timeStr ?? ""
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : Colors.layer(Colors.palette.m3surfaceContainerHigh, 3)
|
||||
implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall
|
||||
implicitWidth: expandBtn.implicitWidth + Tokens.padding.large
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
||||
|
||||
onClicked: root.toggleExpand(!root.expanded)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: expandBtn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
id: groupCount
|
||||
|
||||
Layout.leftMargin: Tokens.padding.extraSmall / 2
|
||||
animate: true
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant
|
||||
text: root.notifCount
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.rightMargin: -Tokens.padding.extraSmall / 2
|
||||
Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.extraSmall) : 0
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
|
||||
Behavior on Layout.topMargin {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on rotation {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotifGroupList {
|
||||
id: notifList
|
||||
|
||||
container: root.container
|
||||
expanded: root.expanded
|
||||
notifs: root.notifs
|
||||
screenState: root.screenState
|
||||
|
||||
onRequestToggleExpand: expand => root.toggleExpand(expand)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property Flickable container
|
||||
required property bool expanded
|
||||
required property list<var> notifs
|
||||
required property ScreenState screenState
|
||||
|
||||
signal requestToggleExpand(expand: bool)
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
cacheBuffer: 400
|
||||
implicitHeight: contentHeight
|
||||
readyDelay: 1
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
spacing: Math.round(Tokens.spacing.extraSmall)
|
||||
useCustomViewport: true
|
||||
viewport: {
|
||||
tWatcher.transform; // mapToItem is not reactive so use this to trigger updates
|
||||
return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height);
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property NotifServer.Notif modelData
|
||||
property int startY
|
||||
|
||||
LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight
|
||||
LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
||||
drag.axis: Drag.XAxis
|
||||
drag.target: this
|
||||
enabled: !(modelData?.closed ?? true)
|
||||
hoverEnabled: true
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
||||
preventStealing: !root.expanded
|
||||
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: modelData?.lock(this)
|
||||
Component.onDestruction: modelData?.unlock(this)
|
||||
onPositionChanged: event => {
|
||||
if (pressed && !root.expanded) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
root.requestToggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
root.requestToggleExpand(!root.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
modelData?.close();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
modelData?.close();
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.modelData?.closed ?? false
|
||||
|
||||
onFinished: notif.modelData?.unlock(notif)
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: notif
|
||||
to: 0
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "x"
|
||||
target: notif
|
||||
to: notif.x >= 0 ? notif.width : -notif.width
|
||||
}
|
||||
}
|
||||
|
||||
Notif {
|
||||
id: notifInner
|
||||
|
||||
anchors.fill: parent
|
||||
expanded: root.expanded
|
||||
modelData: notif.modelData
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
if (root.expanded)
|
||||
return root.notifs as Array;
|
||||
|
||||
let count = 0;
|
||||
let i = 0;
|
||||
const previewNum = Config.notifs.groupPreviewNum;
|
||||
while (i < root.notifs.length && count < previewNum) {
|
||||
if (!(root.notifs[i]?.closed ?? true))
|
||||
count++;
|
||||
i++;
|
||||
}
|
||||
|
||||
return root.notifs.slice(0, i) as Array;
|
||||
}
|
||||
}
|
||||
|
||||
TransformWatcher {
|
||||
id: tWatcher
|
||||
|
||||
a: root.container.contentItem
|
||||
b: root
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Templates
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property alias count: bar.count
|
||||
required property ScreenState screenState
|
||||
required property real nonAnimWidth
|
||||
|
||||
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
|
||||
|
||||
TabBar {
|
||||
id: bar
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
background: null
|
||||
currentIndex: root.screenState.sidebarTab
|
||||
implicitHeight: contentHeight
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: bar.contentModel
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: root.state.currentTab = currentIndex
|
||||
|
||||
Tab {
|
||||
iconName: "notifications"
|
||||
text: qsTr("Notifications")
|
||||
}
|
||||
|
||||
Tab {
|
||||
iconName: "chat"
|
||||
text: qsTr("Chat")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: indicator
|
||||
|
||||
anchors.top: bar.bottom
|
||||
clip: true
|
||||
implicitHeight: 3
|
||||
implicitWidth: bar.currentItem.implicitWidth
|
||||
x: {
|
||||
const tab = bar.currentItem;
|
||||
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
|
||||
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: Colors.palette.m3primary
|
||||
implicitHeight: parent.implicitHeight * 2
|
||||
radius: Tokens.rounding.full
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: separator
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: indicator.bottom
|
||||
color: Colors.palette.m3outlineVariant
|
||||
implicitHeight: 1
|
||||
}
|
||||
|
||||
component Tab: TabButton {
|
||||
id: tab
|
||||
|
||||
readonly property bool current: TabBar.tabBar.currentItem === this
|
||||
required property string iconName
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
background: null
|
||||
implicitHeight: implicitContentHeight
|
||||
implicitWidth: implicitContentWidth
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: icon.height + label.height
|
||||
implicitWidth: Math.max(icon.width, label.width)
|
||||
|
||||
StateLayer {
|
||||
color: tab.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
onClicked: root.screenState.sidebarTab = tab.TabBar.index
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.bottom: label.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
fill: tab.current ? 1 : 0
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: tab.iconName
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
text: tab.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Services
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
readonly property bool flatten: wrapper.height < rounding * 2
|
||||
readonly property real rounding: Config.bar.rounding
|
||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
||||
required property var sidebar
|
||||
required property Wrapper wrapper
|
||||
|
||||
fillColor: Colors.palette.m3surface
|
||||
strokeWidth: -1
|
||||
|
||||
Behavior on fillColor {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: -(root.wrapper.width + root.rounding)
|
||||
relativeY: 0
|
||||
}
|
||||
|
||||
PathArc {
|
||||
direction: PathArc.Counterclockwise
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
relativeX: root.rounding
|
||||
relativeY: -root.roundingY
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -(root.wrapper.height - root.roundingY * 2)
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.sidebar.utilsRoundingX
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
relativeX: root.sidebar.utilsRoundingX
|
||||
relativeY: -root.roundingY
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width
|
||||
relativeY: 0
|
||||
}
|
||||
|
||||
PathArc {
|
||||
direction: PathArc.Counterclockwise
|
||||
radiusX: root.rounding
|
||||
radiusY: root.rounding
|
||||
relativeX: root.rounding
|
||||
relativeY: -root.rounding
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property ScreenState screenState
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.medium
|
||||
z: 1
|
||||
|
||||
CustomRect {
|
||||
color: Recorder.running ? Colors.palette.m3secondary : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: {
|
||||
const h = icon.implicitHeight + Tokens.padding.small * 2;
|
||||
return h - (h % 2);
|
||||
}
|
||||
implicitWidth: implicitHeight
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -0.5
|
||||
anchors.verticalCenterOffset: 1.5
|
||||
color: Recorder.running ? Colors.palette.m3onSecondary : Colors.palette.m3onSecondaryContainer
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: "screen_record"
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.body.medium
|
||||
text: qsTr("Screen Recorder")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.body.small
|
||||
text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off")
|
||||
}
|
||||
}
|
||||
|
||||
SplitButton {
|
||||
active: menuItems.find(m => root.screenState.recordingMode === m.icon + m.text) ?? menuItems[0]
|
||||
enabled: !Recorder.running
|
||||
menuOnTop: true
|
||||
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
activeText: qsTr("Fullscreen")
|
||||
icon: "fullscreen"
|
||||
text: qsTr("Record fullscreen")
|
||||
|
||||
onClicked: Recorder.start()
|
||||
},
|
||||
MenuItem {
|
||||
activeText: qsTr("Region")
|
||||
icon: "screenshot_region"
|
||||
text: qsTr("Record region")
|
||||
|
||||
onClicked: Recorder.start(["-r"])
|
||||
},
|
||||
MenuItem {
|
||||
activeText: qsTr("Fullscreen")
|
||||
icon: "select_to_speak"
|
||||
text: qsTr("Record fullscreen with sound")
|
||||
|
||||
onClicked: Recorder.start(["-s"])
|
||||
},
|
||||
MenuItem {
|
||||
activeText: qsTr("Region")
|
||||
icon: "volume_up"
|
||||
text: qsTr("Record region with sound")
|
||||
|
||||
onClicked: Recorder.start(["-s", "-r"])
|
||||
}
|
||||
]
|
||||
|
||||
menu.onItemSelected: item => root.screenState.recordingMode = item.icon + item.text
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: listOrControls
|
||||
|
||||
property bool running: Recorder.running
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
asynchronous: true
|
||||
sourceComponent: running ? recordingControls : recordingList
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
id: locHeightAnim
|
||||
|
||||
enabled: false
|
||||
|
||||
Anim {}
|
||||
}
|
||||
Behavior on running {
|
||||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.small
|
||||
easing: Tokens.anim.standardAccel
|
||||
property: "scale"
|
||||
target: listOrControls
|
||||
to: 0.7
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.small
|
||||
easing: Tokens.anim.standardAccel
|
||||
property: "opacity"
|
||||
target: listOrControls
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "enabled"
|
||||
target: locHeightAnim
|
||||
value: true
|
||||
}
|
||||
|
||||
PropertyAction {}
|
||||
|
||||
PropertyAction {
|
||||
property: "enabled"
|
||||
target: locHeightAnim
|
||||
value: false
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.small
|
||||
easing: Tokens.anim.standardDecel
|
||||
property: "scale"
|
||||
target: listOrControls
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.small
|
||||
easing: Tokens.anim.standardDecel
|
||||
property: "opacity"
|
||||
target: listOrControls
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: recordingList
|
||||
|
||||
RecordingList {
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: recordingControls
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
CustomRect {
|
||||
color: Recorder.paused ? Colors.palette.m3tertiary : Colors.palette.m3error
|
||||
implicitHeight: recText.implicitHeight + Tokens.padding.small * 2
|
||||
implicitWidth: recText.implicitWidth + Tokens.padding.small * 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
SequentialAnimation on opacity {
|
||||
alwaysRunToEnd: true
|
||||
loops: Animation.Infinite
|
||||
running: !Recorder.paused
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.large
|
||||
easing: Tokens.anim.emphasizedAccel
|
||||
from: 1
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.extraLarge
|
||||
easing: Tokens.anim.emphasizedDecel
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: recText
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
color: Recorder.paused ? Colors.palette.m3onTertiary : Colors.palette.m3onError
|
||||
font: Tokens.font.mono.small
|
||||
text: Recorder.paused ? "PAUSED" : "REC"
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
font: Tokens.font.body.medium
|
||||
text: {
|
||||
const elapsed = Recorder.elapsed;
|
||||
|
||||
const hours = Math.floor(elapsed / 3600);
|
||||
const mins = Math.floor((elapsed % 3600) / 60);
|
||||
const secs = Math.floor(elapsed % 60).toString().padStart(2, "0");
|
||||
|
||||
let time;
|
||||
if (hours > 0)
|
||||
time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
|
||||
else
|
||||
time = `${mins}:${secs}`;
|
||||
|
||||
return qsTr("Recording for %1").arg(time);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
IconButton {
|
||||
checked: Recorder.paused
|
||||
font: Tokens.font.icon.medium
|
||||
icon: Recorder.paused ? "play_arrow" : "pause"
|
||||
isRound: true
|
||||
isToggle: true
|
||||
label.animate: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: {
|
||||
Recorder.togglePause();
|
||||
internalChecked = Recorder.paused;
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
font: Tokens.font.icon.medium
|
||||
icon: "stop"
|
||||
isRound: true
|
||||
inactiveColor: Colors.palette.m3error
|
||||
inactiveOnColor: Colors.palette.m3onError
|
||||
|
||||
onClicked: Recorder.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import ZShell.Models
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Paths
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property ScreenState screenState
|
||||
|
||||
spacing: 0
|
||||
|
||||
WrapperMouseArea {
|
||||
Layout.fillWidth: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: "list"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
font: Tokens.font.body.medium
|
||||
text: qsTr("Recordings")
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: root.screenState.recordingListExpanded ? "unfold_less" : "unfold_more"
|
||||
label.animate: true
|
||||
type: IconButton.Text
|
||||
|
||||
onClicked: root.screenState.recordingListExpanded = !root.screenState.recordingListExpanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: -Tokens.spacing.small
|
||||
clip: true
|
||||
implicitHeight: (Tokens.font.body.large.pointSize + Tokens.padding.extraSmall) * (root.screenState.recordingListExpanded ? 10 : 3)
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: list
|
||||
}
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
from: 0.5
|
||||
property: "scale"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: RowLayout {
|
||||
id: recording
|
||||
|
||||
property string baseName
|
||||
required property FileSystemEntry modelData
|
||||
|
||||
anchors.left: list.contentItem.left
|
||||
anchors.right: list.contentItem.right
|
||||
anchors.rightMargin: Tokens.spacing.small
|
||||
spacing: Tokens.spacing.small / 2
|
||||
|
||||
Component.onCompleted: baseName = modelData.baseName
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Tokens.spacing.small / 2
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
const time = recording.baseName;
|
||||
const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/);
|
||||
if (!matches)
|
||||
return time;
|
||||
const date = new Date(...matches.slice(1));
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "play_arrow"
|
||||
type: IconButton.Text
|
||||
|
||||
onClicked: {
|
||||
root.screenState.sidebar = false;
|
||||
if (Config.launcher.uwsm)
|
||||
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
|
||||
else
|
||||
Quickshell.execDetached([...Config.general.apps.playback, recording.modelData.path]);
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "folder"
|
||||
type: IconButton.Text
|
||||
|
||||
onClicked: {
|
||||
root.screenState.sidebar = false;
|
||||
if (Config.launcher.uwsm)
|
||||
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
|
||||
else
|
||||
Quickshell.execDetached([...Config.general.apps.explorer, recording.modelData.path]);
|
||||
}
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
model: FileSystemModel {
|
||||
nameFilters: ["recording_*.mp4"]
|
||||
path: Paths.recsdir
|
||||
sortReverse: true
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
asynchronous: true
|
||||
opacity: list.count === 0 ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredHeight: root.screenState.recordingListExpanded ? implicitHeight : 0
|
||||
color: Colors.palette.m3outline
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
opacity: root.screenState.recordingListExpanded ? 1 : 0
|
||||
scale: root.screenState.recordingListExpanded ? 1 : 0
|
||||
text: "scan_delete"
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: !root.screenState.recordingListExpanded ? implicitWidth : 0
|
||||
color: Colors.palette.m3outline
|
||||
opacity: !root.screenState.recordingListExpanded ? 1 : 0
|
||||
scale: !root.screenState.recordingListExpanded ? 1 : 0
|
||||
text: "scan_delete"
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3outline
|
||||
text: qsTr("No recordings found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Networking as QSNetwork
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
import qs.Daemons
|
||||
import qs.Modules.Settings
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property bool needExtraRow: quickToggles.length > 6
|
||||
readonly property var quickToggles: {
|
||||
const seenIds = new Set();
|
||||
|
||||
return Config.utilities.quickToggles.values.filter(item => {
|
||||
if (!item.enabled)
|
||||
return false;
|
||||
|
||||
if (seenIds.has(item.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.id === "bluetooth" && !Bluetooth.defaultAdapter)
|
||||
return false;
|
||||
|
||||
if (item.id === "wifi" && !QSNetwork.Networking.devices.values.some(n => n.type === QSNetwork.DeviceType.Wifi))
|
||||
return false;
|
||||
|
||||
seenIds.add(item.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
required property ScreenState screenState
|
||||
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
CustomText {
|
||||
font: Tokens.font.body.medium
|
||||
text: qsTr("Quick Toggles")
|
||||
}
|
||||
|
||||
QuickToggleRow {
|
||||
model: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles
|
||||
}
|
||||
|
||||
QuickToggleRow {
|
||||
model: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : []
|
||||
visible: root.needExtraRow
|
||||
}
|
||||
}
|
||||
|
||||
component QuickToggleRow: ButtonRow {
|
||||
property alias model: repeater.model
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
delegate: DelegateChooser {
|
||||
role: "id"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "wifi"
|
||||
|
||||
delegate: Toggle {
|
||||
checked: Network.wifiEnabled
|
||||
icon: checked ? "wifi" : "wifi_off"
|
||||
|
||||
onClicked: Network.toggleWifi()
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "bluetooth"
|
||||
|
||||
delegate: Toggle {
|
||||
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
|
||||
icon: "bluetooth"
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
|
||||
if (adapter)
|
||||
adapter.enabled = !adapter.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "mic"
|
||||
|
||||
delegate: Toggle {
|
||||
checked: !Audio.sourceMuted
|
||||
icon: checked ? "mic" : "mic_off"
|
||||
|
||||
onClicked: {
|
||||
const audio = Audio.source?.audio;
|
||||
if (audio)
|
||||
audio.muted = !audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "audio"
|
||||
|
||||
delegate: Toggle {
|
||||
checked: !Audio.muted
|
||||
icon: checked ? "volume_up" : "volume_off"
|
||||
|
||||
onClicked: {
|
||||
const audio = Audio.sink?.audio;
|
||||
if (audio)
|
||||
audio.muted = !audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "gameMode"
|
||||
|
||||
delegate: Toggle {
|
||||
checked: GameMode.enabled
|
||||
icon: "gamepad"
|
||||
|
||||
onClicked: GameMode.enabled = !GameMode.enabled
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "dnd"
|
||||
|
||||
delegate: Toggle {
|
||||
checked: NotifServer.dnd
|
||||
icon: "notifications_off"
|
||||
|
||||
onClicked: NotifServer.dnd = !NotifServer.dnd
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "settings"
|
||||
|
||||
delegate: Toggle {
|
||||
icon: "settings"
|
||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||
isToggle: false
|
||||
|
||||
onClicked: {
|
||||
root.screenState.sidebar = false;
|
||||
root.screenState.settings = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
component Toggle: IconButton {
|
||||
fillWidth: true
|
||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||
isRound: true
|
||||
isToggle: true
|
||||
shapeMorph: true
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Notifications.Sidebar.Utils.Cards
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ScreenState screenState
|
||||
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitWidth: layout.implicitWidth
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
IdleInhibit {}
|
||||
|
||||
Record {
|
||||
screenState: root.screenState
|
||||
z: 1
|
||||
}
|
||||
|
||||
Toggles {
|
||||
Layout.fillWidth: true
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + 18 * 2
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 18
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: 10
|
||||
|
||||
CustomRect {
|
||||
color: IdleInhibitor.enabled ? Colors.palette.m3secondary : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: icon.implicitHeight + 7 * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: IdleInhibitor.enabled ? Colors.palette.m3onSecondary : Colors.palette.m3onSecondaryContainer
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: "coffee"
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.body.medium
|
||||
text: qsTr("Keep Awake")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.body.small
|
||||
text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management")
|
||||
}
|
||||
}
|
||||
|
||||
CustomSwitch {
|
||||
checked: IdleInhibitor.enabled
|
||||
|
||||
onToggled: IdleInhibitor.enabled = checked
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: activeChip
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: IdleInhibitor.enabled ? 18 : -implicitHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 18
|
||||
anchors.topMargin: 20
|
||||
opacity: IdleInhibitor.enabled ? 1 : 0
|
||||
scale: IdleInhibitor.enabled ? 1 : 0.5
|
||||
|
||||
Behavior on anchors.bottomMargin {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: CustomRect {
|
||||
color: Colors.palette.m3primary
|
||||
implicitHeight: activeText.implicitHeight + 10 * 2
|
||||
implicitWidth: activeText.implicitWidth + 10 * 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
CustomText {
|
||||
id: activeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: Colors.palette.m3onPrimary
|
||||
font: Tokens.font.body.builders.small.size(Math.round(Tokens.font.body.small.pointSize * 0.9)).build()
|
||||
text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm"))
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: active = Qt.binding(() => opacity > 0)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real offsetScale: shouldBeActive ? 0 : 1
|
||||
required property ScreenState screenState
|
||||
readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive
|
||||
required property Item sidebar
|
||||
|
||||
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
|
||||
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
|
||||
opacity: 1 - offsetScale
|
||||
visible: offsetScale < 1
|
||||
|
||||
Behavior on offsetScale {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 8
|
||||
anchors.top: parent.top
|
||||
|
||||
sourceComponent: Content {
|
||||
implicitWidth: root.implicitWidth - 8 * 2
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.Components
|
||||
import qs.Components.Toast
|
||||
import ZShell
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property bool chatActive: screenState.sidebarTab === 1
|
||||
property real offsetScale: shouldBeActive ? 0 : 1
|
||||
required property var panels
|
||||
required property ScreenState screenState
|
||||
readonly property bool shouldBeActive: root.screenState.sidebar && Config.sidebar.enabled
|
||||
|
||||
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
|
||||
implicitWidth: Config.sidebar.sizes.width
|
||||
opacity: 1 - offsetScale
|
||||
visible: offsetScale < 1
|
||||
|
||||
Behavior on offsetScale {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: LlmClient
|
||||
|
||||
function onGenerationFailed(session: ChatSession, message: string): void {
|
||||
if (root.shouldBeActive && root.chatActive)
|
||||
return;
|
||||
|
||||
Toaster.toast(qsTr("Chat"), message, "error_outline", Toast.Error);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 8
|
||||
anchors.top: parent.top
|
||||
|
||||
sourceComponent: Content {
|
||||
implicitWidth: Config.sidebar.sizes.width - Tokens.padding.small * 2
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user