From ae5c1fa148455da5a2e4700c7d80c8eeb3b99260 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 1 Sep 2026 23:14:21 +0200 Subject: [PATCH] use blobgroup for window chat interface + slim sidebar --- .../Sidebar/Chat/ChatContent.qml | 2 +- .../Sidebar/Chat/ChatDelegate.qml | 24 +- .../Notifications/Sidebar/Chat/ChatList.qml | 10 + .../Notifications/Sidebar/Chat/ChatState.qml | 1 + .../Sidebar/Chat/Content/ChatHost.qml | 211 +++++++++--------- .../Sidebar/Chat/SidebarView.qml | 98 ++++++-- 6 files changed, 224 insertions(+), 122 deletions(-) diff --git a/Modules/Notifications/Sidebar/Chat/ChatContent.qml b/Modules/Notifications/Sidebar/Chat/ChatContent.qml index 85e2a6d..4ae1718 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatContent.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatContent.qml @@ -161,7 +161,7 @@ Item { scrollAnim.start(); } - cacheBuffer: Math.max(height * 20, 0) + cacheBuffer: Math.max(height * 2, 0) clip: true fadeAmount: 0.05 fadeThreshold: Tokens.padding.medium diff --git a/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml index 1bcb78f..5a2613f 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml @@ -11,6 +11,7 @@ CustomClippingRect { property bool expanded: false property bool highlighted: false + property bool slim: false required property ChatSession modelData signal clicked(content: ChatSession) @@ -20,6 +21,9 @@ CustomClippingRect { implicitHeight: { let h = 0; + if (slim) + return chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2; + h += infoContainer.implicitHeight; if (expanded) @@ -85,6 +89,14 @@ CustomClippingRect { return h; } + opacity: root.slim ? 0 : 1 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + CustomRect { id: title @@ -246,7 +258,7 @@ CustomClippingRect { color: Colors.palette.m3onSurfaceVariant font.pointSize: Tokens.font.size.small maximumLineCount: 1 - opacity: !root.expanded ? 1 : 0 + opacity: !root.expanded && !root.slim ? 1 : 0 text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet") Behavior on y { @@ -347,6 +359,16 @@ CustomClippingRect { 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 } diff --git a/Modules/Notifications/Sidebar/Chat/ChatList.qml b/Modules/Notifications/Sidebar/Chat/ChatList.qml index 541eb14..7f0c912 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatList.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatList.qml @@ -14,6 +14,7 @@ Item { property alias list: list property alias model: list.model property bool highlight: false + property bool slim: false signal deleteChatRequest(content: ChatSession) signal loadChatRequest(content: ChatSession, index: int) @@ -79,6 +80,7 @@ Item { implicitWidth: ListView.view.width highlighted: root.highlight && ChatState.chatSession === modelData + slim: root.slim onHighlightedChanged: if (highlighted) list.currentIndex = index @@ -248,6 +250,14 @@ Item { font.pointSize: Math.round(18 * 1.2) icon: "add" isRound: ChatState.fabExpanded + opacity: root.slim ? 0 : 1 + visible: opacity > 0 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } label.transform: Rotation { origin.y: fabRoot.label.height / 2 diff --git a/Modules/Notifications/Sidebar/Chat/ChatState.qml b/Modules/Notifications/Sidebar/Chat/ChatState.qml index c24e59a..1847b35 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatState.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatState.qml @@ -14,6 +14,7 @@ Singleton { property bool inChat: false property bool fabExpanded: false property bool isWindow: false + property bool windowIsWide: false property ShellScreen screen property bool sidebarVisible: Visibilities.getForActive().sidebar diff --git a/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml b/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml index 0a75bb3..9869855 100644 --- a/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml +++ b/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml @@ -6,127 +6,138 @@ import qs.Services import qs.Modules.Notifications.Sidebar.Chat Item { - id: root + id: root - property int animOff - property Item currentItem - property bool loading: false - property int lastIdx: -1 + property int animOff + property Item currentItem + property bool noAnim: false + property bool loading: false + property int lastIdx: -1 - readonly property Component conversationComp: ChatContent {} + readonly property Component conversationComp: ChatContent {} - function loadConversation(chat): void { - if (currentItem) { - currentItem.destroy(); - currentItem = null; - } + signal requestClose - if (!chat) - return; + function loadConversation(chat): void { + if (currentItem) { + currentItem.destroy(); + currentItem = null; + } - root.loading = true; + if (!chat) + return; - const incubator = root.conversationComp.incubateObject(container, { - chatData: chat - }); + root.loading = true; - const attach = () => { - incubator.object.anchors.fill = container; - currentItem = incubator.object; - root.loading = false; - enterAnim.start(); - }; + const incubator = root.conversationComp.incubateObject(container, { + chatData: chat + }); - if (incubator.status === Component.Ready) - attach(); - else - incubator.onStatusChanged = status => { - if (status === Component.Ready) - attach(); - }; - } + const attach = () => { + incubator.object.anchors.fill = container; + incubator.object.requestClose.connect(root.requestClose); + currentItem = incubator.object; + root.loading = false; + if (!noAnim) + enterAnim.start(); + }; - Item { - id: container + if (incubator.status === Component.Ready) + attach(); + else + incubator.onStatusChanged = status => { + if (status === Component.Ready) + attach(); + }; + } - anchors.fill: parent - layer.enabled: opacity < 1 - objectName: "ConversationContainer" + Item { + id: container - Component.onCompleted: { - if (ChatState.chatSession) - root.loadConversation(ChatState.chatSession); - } - } + anchors.fill: parent + layer.enabled: opacity < 1 + objectName: "ConversationContainer" - LoadingIndicator { - anchors.centerIn: parent - implicitSize: Tokens.font.size.extraLarge * 4 - opacity: root.loading ? 1 : 0 - visible: opacity > 0 + Component.onCompleted: { + if (ChatState.chatSession) + root.loadConversation(ChatState.chatSession); + } + } - Behavior on opacity { - Anim {} - } - } + LoadingIndicator { + anchors.centerIn: parent + implicitSize: Tokens.font.size.extraLarge * 4 + opacity: root.loading ? 1 : 0 + visible: opacity > 0 - Connections { - function onChatSessionChanged(): void { - exitAnim.complete(); - enterAnim.complete(); - root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1); - root.lastIdx = ChatState.currentIdx; - exitAnim.start(); - } + Behavior on opacity { + Anim {} + } + } - target: ChatState - } + Connections { + function onChatSessionChanged(): void { + exitAnim.complete(); + enterAnim.complete(); - SequentialAnimation { - id: exitAnim + if (root.noAnim) { + root.loadConversation(ChatState.chatSession); + return; + } - Anim { - property: "opacity" - target: container - to: 0 - type: Anim.DefaultEffects - } + root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1); + root.lastIdx = ChatState.currentIdx; + exitAnim.start(); + } - ScriptAction { - script: root.loadConversation(ChatState.chatSession) - } - } + target: ChatState + } - SequentialAnimation { - id: enterAnim + SequentialAnimation { + id: exitAnim - PropertyAction { - property: "topMargin" - target: container.anchors - value: root.animOff - } + Anim { + property: "opacity" + target: container + to: 0 + type: Anim.DefaultEffects + } - PropertyAction { - property: "bottomMargin" - target: container.anchors - value: -root.animOff - } + ScriptAction { + script: root.loadConversation(ChatState.chatSession) + } + } - ParallelAnimation { - Anim { - from: 0 - property: "opacity" - target: container - to: 1 - type: Anim.SlowEffects - } + SequentialAnimation { + id: enterAnim - Anim { - properties: "topMargin,bottomMargin" - target: container.anchors - to: 0 - type: Anim.SlowEffects - } - } - } + 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 + } + } + } } diff --git a/Modules/Notifications/Sidebar/Chat/SidebarView.qml b/Modules/Notifications/Sidebar/Chat/SidebarView.qml index 1ffdb0c..210d780 100644 --- a/Modules/Notifications/Sidebar/Chat/SidebarView.qml +++ b/Modules/Notifications/Sidebar/Chat/SidebarView.qml @@ -4,38 +4,65 @@ 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 -RowLayout { +Item { id: root - property int breakpoint: 700 + 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.size.extraLarge * (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 - property bool narrowShowsSidebar: true + + 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 { + 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 - Layout.fillHeight: true - Layout.margins: Tokens.padding.medium - Layout.rightMargin: Tokens.spacing.medium - Layout.maximumWidth: Config.sidebar.sizes.width - Layout.preferredWidth: root.width / 4 - Layout.minimumWidth: Config.sidebar.sizes.width / 2 + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.medium + implicitWidth: root.isWide ? Config.sidebar.sizes.width : root.iconSize highlight: true + slim: !root.isWide Behavior on implicitWidth { - Anim {} + Anim { + type: Anim.DefaultEffects + } } + model: ScriptModel { values: Chat.chats.values } @@ -46,9 +73,11 @@ RowLayout { } onDeleteChatRequest: chat => { + if (chat === ChatState.chatSession) { + ChatState.chatSession = null; + ChatState.currentIdx = -1; + } Chat.chats.remove(chat); - ChatState.currentIdx = -1; - ChatState.chatSession = null; } onNewChatRequest: { const data = Chat.chats.insert(); @@ -58,12 +87,34 @@ RowLayout { } Item { - Layout.margins: Tokens.padding.extraLarge - Layout.leftMargin: Tokens.spacing.extraLarge - Layout.topMargin: Tokens.padding.large - Layout.preferredWidth: Config.sidebar.sizes.width * 2 - Layout.fillWidth: true - Layout.fillHeight: true + id: contentArea + + property int leftMargin: sidebar.implicitWidth + + z: 1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.medium * 2 + anchors.right: parent.right + implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2 - anchors.margins + + 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 @@ -71,10 +122,11 @@ RowLayout { anchors.top: parent.top anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - implicitWidth: Math.min(parent.width, 800) + implicitWidth: Math.max(Math.min(parent.width, 800), Config.sidebar.sizes.width) clip: true - onImplicitWidthChanged: console.log(implicitWidth) + onLoadingChanged: if (!loading) + root.chatOpen = true } ColumnLayout { @@ -102,6 +154,12 @@ RowLayout { } } + Binding { + target: ChatState + property: "windowIsWide" + value: root.showList + } + Component { id: conversationView