415 lines
8.7 KiB
QML
415 lines
8.7 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import Quickshell
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import ZShell.Llm
|
|
import ZShell.Config
|
|
import qs.Services
|
|
import qs.Components
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property alias list: list
|
|
property alias model: list.model
|
|
property bool highlight: false
|
|
|
|
signal deleteChatRequest(content: ChatSession)
|
|
signal loadChatRequest(content: ChatSession, index: int)
|
|
signal newChatRequest
|
|
|
|
CustomText {
|
|
id: header
|
|
|
|
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: header.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
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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: fabRoot
|
|
|
|
anchors.bottom: parent.bottom
|
|
anchors.right: parent.right
|
|
anchors.margins: Tokens.padding.small
|
|
padding: 8
|
|
font.pointSize: Math.round(18 * 1.2)
|
|
icon: "add"
|
|
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: {
|
|
modelsContainer.expanded = false;
|
|
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"
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|