From 37c6a9986ed3c509469b258b2a6410b47cc136aa Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 20 Aug 2026 20:24:54 +0200 Subject: [PATCH] start initial work on detaching chat to floating window --- .../Sidebar/Chat/ChatContent.qml | 6 +- .../Sidebar/Chat/ChatDelegate.qml | 9 ++- .../Notifications/Sidebar/Chat/ChatList.qml | 8 ++- .../Notifications/Sidebar/Chat/ChatPanel.qml | 60 +++++++++++++------ .../Notifications/Sidebar/Chat/ChatState.qml | 14 +++++ Modules/Notifications/Sidebar/Chat/Detach.qml | 51 ++++++++++++++++ .../Sidebar/Chat/SidebarView.qml | 57 ++++++++++++++++++ Modules/Notifications/Sidebar/Content.qml | 12 +++- Modules/Notifications/Sidebar/Props.qml | 2 - 9 files changed, 190 insertions(+), 29 deletions(-) create mode 100644 Modules/Notifications/Sidebar/Chat/ChatState.qml create mode 100644 Modules/Notifications/Sidebar/Chat/Detach.qml create mode 100644 Modules/Notifications/Sidebar/Chat/SidebarView.qml diff --git a/Modules/Notifications/Sidebar/Chat/ChatContent.qml b/Modules/Notifications/Sidebar/Chat/ChatContent.qml index 8438147..637e273 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatContent.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatContent.qml @@ -11,7 +11,7 @@ import qs.Services Item { id: root - required property ChatSession chatData + property ChatSession chatData property bool following: true readonly property int messageCount: chatData.messages.length @@ -257,7 +257,7 @@ Item { anchors.fill: parent spacing: Tokens.spacing.small - visible: root.messageCount === 0 && !Chat.busy + visible: (root.messageCount === 0 && !Chat.busy) || !root.chatData Item { Layout.fillHeight: true @@ -273,7 +273,7 @@ Item { CustomText { Layout.alignment: Qt.AlignHCenter color: Colors.tPalette.m3onSurfaceVariant - text: qsTr("No messages yet") + text: !root.chatData ? qsTr("Open a previous chat or start a new one") : qsTr("No messages yet") } Item { diff --git a/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml index 129958a..b56685f 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml @@ -8,6 +8,7 @@ import qs.Services CustomRect { id: root + required property Item chatItem property alias layout: layout required property ChatSession modelData @@ -20,6 +21,7 @@ CustomRect { anchors.left: parent.left anchors.leftMargin: Tokens.padding.large anchors.verticalCenter: parent.verticalCenter + color: root.chatItem.ListView.isCurrentItem ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface font.pointSize: Tokens.font.size.extraLarge text: root.modelData.icon || "check" } @@ -48,6 +50,7 @@ CustomRect { CustomText { id: title + color: root.chatItem.ListView.isCurrentItem ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface text: root.modelData.title } @@ -57,7 +60,7 @@ CustomRect { anchors.left: title.right anchors.leftMargin: Tokens.spacing.small anchors.verticalCenter: title.verticalCenter - color: Colors.palette.m3onSurfaceVariant + color: root.chatItem.ListView.isCurrentItem ? Qt.alpha(Colors.palette.m3onPrimary, 0.9) : Colors.palette.m3onSurfaceVariant text: "•" visible: titleContainer.enoughSpace } @@ -70,7 +73,7 @@ CustomRect { anchors.top: titleContainer.enoughSpace ? undefined : title.bottom anchors.topMargin: Tokens.spacing.extraSmall anchors.verticalCenter: titleContainer.enoughSpace ? title.verticalCenter : undefined - color: Colors.palette.m3onSurfaceVariant + color: root.chatItem.ListView.isCurrentItem ? Qt.alpha(Colors.palette.m3onPrimary, 0.9) : Colors.palette.m3onSurfaceVariant font.pointSize: Tokens.font.size.small text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP") } @@ -78,7 +81,7 @@ CustomRect { CustomText { Layout.fillWidth: true - color: Colors.palette.m3outline + color: root.chatItem.ListView.isCurrentItem ? Qt.alpha(Colors.palette.m3onPrimary, 0.9) : Colors.palette.m3onSurfaceVariant elide: Text.ElideRight font.pointSize: Tokens.font.size.small maximumLineCount: 1 diff --git a/Modules/Notifications/Sidebar/Chat/ChatList.qml b/Modules/Notifications/Sidebar/Chat/ChatList.qml index 20979fb..160936b 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatList.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatList.qml @@ -11,10 +11,11 @@ import qs.Components Item { id: root + property alias list: list property alias model: list.model signal deleteChatRequest(content: ChatSession) - signal loadChatRequest(content: ChatSession) + signal loadChatRequest(content: ChatSession, index: int) signal newChatRequest CustomText { @@ -47,8 +48,10 @@ Item { Item { id: chatItem + property alias chat: chat property bool closing: false property bool editVisible: false + required property int index required property var modelData property real startY: 0.0 @@ -87,6 +90,7 @@ Item { ChatDelegate { id: chat + chatItem: chatItem implicitWidth: chatItem.implicitWidth modelData: chatItem.modelData radius: Tokens.rounding.medium @@ -111,7 +115,7 @@ Item { if (chatItem.editVisible) chatItem.editVisible = false; else - root.loadChatRequest(chatItem.modelData); + root.loadChatRequest(chatItem.modelData, chatItem.index); } onPressed: event => { chatItem.startY = event.y; diff --git a/Modules/Notifications/Sidebar/Chat/ChatPanel.qml b/Modules/Notifications/Sidebar/Chat/ChatPanel.qml index bc87a94..606e65e 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatPanel.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatPanel.qml @@ -5,6 +5,7 @@ import ZShell.Llm import QtQuick import QtQuick.Controls import Quickshell +import qs.Helpers import qs.Modules.Notifications.Sidebar import qs.Components import qs.Services @@ -12,26 +13,49 @@ import qs.Services Item { id: root - property bool chatOpen: false - required property Props props - anchors.fill: parent anchors.margins: Tokens.padding.small Component.onCompleted: { - if (root.props.inChat) { - stack.push(chatList); - stack.push(chatContent, { - "chatData": root.props.chatSession + if (ChatState.inChat) { + stackLoader.item?.push(chatList); + stackLoader.item?.push(chatContent, { + "chatData": ChatState.chatSession }); } else - stack.push(chatList); + stackLoader.item?.push(chatList); } - StackView { - id: stack + Loader { + id: stackLoader + active: !ChatState.isWindow || root.width <= (ChatState.screen.width / 4) anchors.fill: parent + + sourceComponent: StackView { + id: stack + } + } + + Loader { + id: sidebarViewLoader + + active: ChatState.isWindow && root.width > (ChatState.screen.width / 4) + anchors.fill: parent + + sourceComponent: SidebarView { + } + } + + IconButton { + anchors.right: parent.right + icon: "close" + visible: !ChatState.isWindow + + onClicked: { + Detach.create(); + Visibilities.getForActive().sidebar = false; + } } Component { @@ -46,19 +70,19 @@ Item { Chat.chats.remove(chat); } onLoadChatRequest: chat => { - stack.push(chatContent, { + stackLoader.item?.push(chatContent, { "chatData": chat }); - root.props.inChat = true; - root.props.chatSession = chat; + ChatState.inChat = true; + ChatState.chatSession = chat; } onNewChatRequest: { const data = Chat.chats.insert(); - stack.push(chatContent, { + stackLoader.item?.push(chatContent, { "chatData": data }); - root.props.inChat = true; - root.props.chatSession = data; + ChatState.inChat = true; + ChatState.chatSession = data; } } } @@ -68,8 +92,8 @@ Item { ChatContent { onRequestClose: { - stack.pop(); - root.props.inChat = false; + stackLoader.item?.pop(); + ChatState.inChat = false; } } } diff --git a/Modules/Notifications/Sidebar/Chat/ChatState.qml b/Modules/Notifications/Sidebar/Chat/ChatState.qml new file mode 100644 index 0000000..df96065 --- /dev/null +++ b/Modules/Notifications/Sidebar/Chat/ChatState.qml @@ -0,0 +1,14 @@ +pragma Singleton + +import QtQuick +import Quickshell +import ZShell.Llm + +Singleton { + id: root + + property ChatSession chatSession + property bool inChat: false + property bool isWindow: false + property ShellScreen screen +} diff --git a/Modules/Notifications/Sidebar/Chat/Detach.qml b/Modules/Notifications/Sidebar/Chat/Detach.qml new file mode 100644 index 0000000..86fe6bd --- /dev/null +++ b/Modules/Notifications/Sidebar/Chat/Detach.qml @@ -0,0 +1,51 @@ +pragma Singleton + +import Quickshell +import QtQuick +import ZShell.Config +import qs.Services + +Singleton { + id: root + + function create(parent: Item, props: var): void { + chatComp.createObject(parent ?? dummy, props); + 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 - Chat") + + Component.onCompleted: ChatState.screen = screen + onVisibleChanged: { + if (!visible) { + destroy(); + ChatState.isWindow = false; + } + } + + ChatPanel { + id: chat + + anchors.fill: parent + } + } + } +} diff --git a/Modules/Notifications/Sidebar/Chat/SidebarView.qml b/Modules/Notifications/Sidebar/Chat/SidebarView.qml new file mode 100644 index 0000000..fc9c5b4 --- /dev/null +++ b/Modules/Notifications/Sidebar/Chat/SidebarView.qml @@ -0,0 +1,57 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import ZShell.Config +import ZShell.Llm +import qs.Components +import qs.Services + +RowLayout { + id: root + + ChatList { + id: chatList + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.maximumWidth: Config.sidebar.sizes.width + Layout.minimumWidth: Config.sidebar.sizes.width / 2 + list.currentIndex: -1 + list.highlightFollowsCurrentItem: false + + list.highlight: CustomRect { + color: Colors.palette.m3primary + implicitHeight: chatList.list.currentItem?.implicitHeight ?? 0 + implicitWidth: chatList.list.width + radius: Tokens.rounding.medium + x: chatList.list.currentItem?.chat.x ?? 0 + y: chatList.list.currentItem?.y ?? 0 + + Behavior on y { + Anim { + duration: Tokens.anim.durations.small + easing: Tokens.anim.expressiveDefaultEffects + } + } + } + model: ScriptModel { + values: Chat.chats.values + } + + onLoadChatRequest: (chat, index) => { + chatList.list.currentIndex = index; + chatContent.chatData = chat; + } + } + + ChatContent { + id: chatContent + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.minimumWidth: Config.sidebar.sizes.width + Layout.preferredWidth: Math.round(Config.sidebar.sizes.width * 1.5) + } +} diff --git a/Modules/Notifications/Sidebar/Content.qml b/Modules/Notifications/Sidebar/Content.qml index cfd33c9..290a3d0 100644 --- a/Modules/Notifications/Sidebar/Content.qml +++ b/Modules/Notifications/Sidebar/Content.qml @@ -14,6 +14,15 @@ Item { required property Props props required property var visibilities + Connections { + function onIsWindowChanged(): void { + if (ChatState.isWindow) + root.props.currentTab = 0; + } + + target: ChatState + } + ColumnLayout { id: layout @@ -22,8 +31,10 @@ Item { Tabs { Layout.fillWidth: true + Layout.preferredHeight: ChatState.isWindow ? 0 : implicitHeight dashState: root.props nonAnimWidth: layout.width + visible: height > 0 } CustomClippingRect { @@ -75,7 +86,6 @@ Item { ChatPanel { anchors.fill: parent - props: root.props } } } diff --git a/Modules/Notifications/Sidebar/Props.qml b/Modules/Notifications/Sidebar/Props.qml index 02c0354..dcb4eb0 100644 --- a/Modules/Notifications/Sidebar/Props.qml +++ b/Modules/Notifications/Sidebar/Props.qml @@ -2,10 +2,8 @@ import Quickshell import ZShell.Llm PersistentProperties { - property ChatSession chatSession property int currentTab: 0 property list expandedNotifs: [] - property bool inChat: false reloadableId: "sidebar" }