diff --git a/Modules/Notifications/Sidebar/Chat/ChatList.qml b/Modules/Notifications/Sidebar/Chat/ChatList.qml index 78be0e3..541eb14 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatList.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatList.qml @@ -36,7 +36,7 @@ Item { CustomListView { id: list - anchors.bottom: parent.bottom + anchors.bottom: modelsContainer.top anchors.left: parent.left anchors.right: parent.right anchors.top: header.bottom @@ -85,10 +85,161 @@ Item { onClicked: content => root.loadChatRequest(content, index) onRemove: content => root.deleteChatRequest(content) } + + TapHandler { + onTapped: { + root.focus = true; + ChatState.fabExpanded = false; + modelsContainer.expanded = false; + } + } + } + + CustomClippingRect { + id: modelsContainer + + property bool expanded: false + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: fabRoot.left + anchors.margins: Tokens.padding.small + anchors.rightMargin: Tokens.spacing.small + + implicitHeight: expanded ? 400 : fabRoot.implicitHeight + radius: modelLayer.pressed ? Tokens.rounding.small : modelName.implicitHeight / 2 + + function prettyModelName(): string { + const pathList = Chat.model.split("\/"); + const name = pathList[pathList.length - 1]; + return name; + } + + Behavior on implicitHeight { + Anim {} + } + + Behavior on radius { + Anim {} + } + + Item { + id: modelName + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: fabRoot.implicitHeight + + CustomText { + text: modelsContainer.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: modelsContainer.expanded ? "unfold_less" : "unfold_more" + animate: true + font.pointSize: Tokens.font.size.large + } + + StateLayer { + id: modelLayer + onClicked: { + ChatState.fabExpanded = false; + modelsContainer.expanded = !modelsContainer.expanded; + } + radius: modelsContainer.radius + } + } + + VerticalFadeListView { + id: modelsList + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.top: modelName.bottom + anchors.margins: Tokens.padding.medium + anchors.topMargin: Tokens.spacing.medium + model: Chat.availableModels + opacity: modelsContainer.expanded ? 1 : 0 + currentIndex: Chat.availableModels.indexOf(Chat.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: model.ListView.view.currentIndex === model.index + + implicitWidth: ListView.view.width + implicitHeight: layout.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.fill: parent + anchors.margins: Tokens.padding.small + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large + + CustomText { + text: model.modelData + color: model.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface + } + + MaterialIcon { + Layout.alignment: Qt.AlignRight + visible: model.selected + color: Colors.palette.m3onTertiaryContainer + font.pointSize: Tokens.font.size.normal + opacity: model.selected ? 1 : 0 + text: "check" + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } + } + + StateLayer { + id: stateLayer + + onClicked: { + Chat.selectModel(model.modelData); + } + } + } + } } IconButton { - id: newChatBtn + id: fabRoot anchors.bottom: parent.bottom anchors.right: parent.right @@ -96,17 +247,168 @@ Item { padding: 8 font.pointSize: Math.round(18 * 1.2) icon: "add" - isRound: true + isRound: ChatState.fabExpanded + + label.transform: Rotation { + origin.y: fabRoot.label.height / 2 + origin.x: fabRoot.label.width / 2 + angle: ChatState.fabExpanded ? 135 : 0 + + Behavior on angle { + Anim {} + } + } onClicked: { - root.newChatRequest(); + modelsContainer.expanded = false; + ChatState.fabExpanded = !ChatState.fabExpanded; } Elevation { anchors.fill: parent - level: newChatBtn.stateLayer.containsMouse ? 4 : 3 + 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" + } + } + + CustomClippingRect { + 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) + + 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 + font.pointSize: Tokens.font.size.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; + } + } + } + } + } } diff --git a/Modules/Notifications/Sidebar/Chat/ChatPanel.qml b/Modules/Notifications/Sidebar/Chat/ChatPanel.qml index ba17161..6f8c765 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatPanel.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatPanel.qml @@ -31,22 +31,6 @@ Item { anchors.fill: parent } - IconButton { - anchors.left: parent.left - anchors.bottom: parent.bottom - icon: "open_in_new" - anchors.margins: Tokens.padding.small - isRound: true - padding: 8 - font.pointSize: Math.round(18 * 1.2) - visible: !ChatState.isWindow - - onClicked: { - Detach.create(); - Visibilities.getForActive().sidebar = false; - } - } - Component { id: chatList diff --git a/Modules/Notifications/Sidebar/Chat/ChatState.qml b/Modules/Notifications/Sidebar/Chat/ChatState.qml index 219a0b9..c24e59a 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatState.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatState.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import Quickshell import ZShell.Llm +import qs.Helpers Singleton { id: root @@ -11,6 +12,11 @@ Singleton { readonly property string title: chatSession ? chatSession.title : "Chat" property int currentIdx: -1 property bool inChat: false + property bool fabExpanded: false property bool isWindow: false property ShellScreen screen + property bool sidebarVisible: Visibilities.getForActive().sidebar + + onSidebarVisibleChanged: if (!sidebarVisible) + fabExpanded = false } diff --git a/Modules/Notifications/Sidebar/Chat/Detach.qml b/Modules/Notifications/Sidebar/Chat/Detach.qml index 1064dde..c0ab82d 100644 --- a/Modules/Notifications/Sidebar/Chat/Detach.qml +++ b/Modules/Notifications/Sidebar/Chat/Detach.qml @@ -3,6 +3,7 @@ pragma Singleton import Quickshell import QtQuick import ZShell.Config +import qs.Helpers import qs.Services Singleton { @@ -10,6 +11,7 @@ Singleton { function create(parent: Item, props: var): void { chatComp.createObject(parent ?? dummy, props); + Visibilities.getForActive().sidebar = false; ChatState.isWindow = true; }