Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d4676ddf | ||
|
|
cd3da1941b | ||
|
|
34dde4bbfb | ||
|
|
fcb848b6d2 | ||
|
|
ae5c1fa148 | ||
|
|
b254000737 | ||
|
|
04fe8b1e43 | ||
|
|
dfa6b010e6 | ||
|
|
8a4e9bac31 | ||
|
|
f20fa45b29 | ||
|
|
53a3c76dc0 | ||
|
|
617e87d64f | ||
|
|
ef2fb95fab | ||
|
|
2150859847 | ||
|
|
c9f7856982
|
||
|
|
51dc0fc026 | ||
|
|
cef91caebc | ||
|
|
c5a089adaf | ||
|
|
52feb6006a | ||
|
|
dbb51ceadd | ||
|
|
487615f482 | ||
|
|
d4e936a7e2 | ||
|
|
703d804746 | ||
|
|
327270f3fa | ||
|
|
8a66c3b064 | ||
|
|
293bf67e09 | ||
|
|
c33dbc147f | ||
|
|
78988700be | ||
|
|
3946541f87 | ||
|
|
e18875a752 | ||
|
|
aae3757daf | ||
|
|
37c6a9986e | ||
|
|
9657e5092a | ||
|
|
47bab21e22 |
+2
-1
@@ -3,8 +3,9 @@ FunctionsSpacing=true
|
|||||||
IndentWidth=4
|
IndentWidth=4
|
||||||
MaxColumnWidth=-1
|
MaxColumnWidth=-1
|
||||||
NewlineType=native
|
NewlineType=native
|
||||||
NormalizeOrder=true
|
GroupAttributesTogether=false
|
||||||
ObjectsSpacing=true
|
ObjectsSpacing=true
|
||||||
SemicolonRule=always
|
SemicolonRule=always
|
||||||
|
SingleLineEmptyObjects=true
|
||||||
SortImports=false
|
SortImports=false
|
||||||
UseTabs=true
|
UseTabs=true
|
||||||
|
|||||||
+20
-20
@@ -26,22 +26,36 @@ if(NOT DEFINED VERSION)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(NOT DEFINED GIT_REVISION)
|
||||||
|
execute_process(COMMAND git rev-parse HEAD
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
OUTPUT_VARIABLE GIT_REVISION
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
if("${GIT_REVISION}" STREQUAL "")
|
||||||
|
message(FATAL_ERROR "GIT_REVISION is not set and failed to get from git")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
|
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
|
||||||
|
|
||||||
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
|
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
|
||||||
message(STATUS "ZShell version: ${VERSION}")
|
message(STATUS "ZShell version: ${VERSION}")
|
||||||
|
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install")
|
set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
|
||||||
|
|
||||||
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
|
||||||
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
set(INSTALL_QMLDIR "${CMAKE_INSTALL_LIBDIR}/qt6/qml" CACHE STRING "QML install dir")
|
||||||
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
set(INSTALL_QSCONFDIR "${CMAKE_INSTALL_SYSCONFDIR}/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
||||||
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
set(INSTALL_GREETERCONFDIR "${CMAKE_INSTALL_SYSCONFDIR}/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
||||||
|
|
||||||
set(CMAKE_INSTALL_MESSAGE NEVER)
|
set(CMAKE_INSTALL_MESSAGE NEVER)
|
||||||
|
|
||||||
@@ -54,21 +68,7 @@ add_compile_options(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if("shell" IN_LIST ENABLE_MODULES)
|
if("cli" IN_LIST ENABLE_MODULES)
|
||||||
# Build settings index
|
|
||||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
|
||||||
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${Python3_EXECUTABLE}
|
|
||||||
"${CMAKE_SOURCE_DIR}/scripts/build-settings-index.py"
|
|
||||||
"${CMAKE_SOURCE_DIR}/Modules/Settings"
|
|
||||||
"${SETTINGS_INDEX_JSON}"
|
|
||||||
RESULT_VARIABLE SETTINGS_INDEX_RESULT
|
|
||||||
)
|
|
||||||
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
|
|
||||||
message(FATAL_ERROR "Failed to build settings search index")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Nuitka compilation
|
# Nuitka compilation
|
||||||
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
||||||
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ Flickable {
|
|||||||
interval: 10
|
interval: 10
|
||||||
running: root.doneFakeFlick
|
running: root.doneFakeFlick
|
||||||
|
|
||||||
onTriggered: root.doneFakeFlick = false
|
onTriggered: {
|
||||||
|
root.doneFakeFlick = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,24 +10,27 @@ ListView {
|
|||||||
maximumFlickVelocity: 3000
|
maximumFlickVelocity: 3000
|
||||||
|
|
||||||
rebound: Transition {
|
rebound: Transition {
|
||||||
onRunningChanged: {
|
// onRunningChanged: {
|
||||||
if (!running && !root.doneFakeFlick) {
|
// if (!running && !root.doneFakeFlick) {
|
||||||
root.doneFakeFlick = true;
|
// root.doneFakeFlick = true;
|
||||||
root.flick(1, 1);
|
// root.flick(1, 1);
|
||||||
root.flick(-1, -1);
|
// root.flick(-1, -1);
|
||||||
Qt.callLater(() => root.cancelFlick());
|
// Qt.callLater(() => root.cancelFlick());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
properties: "x,y"
|
properties: "x,y"
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
// Timer {
|
||||||
interval: 10
|
// interval: 10
|
||||||
running: root.doneFakeFlick
|
// running: root.doneFakeFlick
|
||||||
|
//
|
||||||
onTriggered: root.doneFakeFlick = false
|
// onTriggered: {
|
||||||
}
|
// root.doneFakeFlick = false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ MouseArea {
|
|||||||
property int scrollAccumulatedY: 0
|
property int scrollAccumulatedY: 0
|
||||||
|
|
||||||
function onWheel(event: WheelEvent): void {
|
function onWheel(event: WheelEvent): void {
|
||||||
|
event.accepted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onWheel: event => {
|
onWheel: event => {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Rectangle {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Templates
|
import QtQuick.Templates
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
ScrollBar {
|
ScrollBar {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
required property Flickable flickable
|
||||||
|
readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
|
||||||
|
readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
|
||||||
|
readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
|
||||||
|
readonly property real axisContentPos: isHorizontal ? (reversed ? flickable.contentX : (flickable.contentX - flickable.originX)) : (reversed ? flickable.contentY : (flickable.contentY - flickable.originY))
|
||||||
|
readonly property real axisLength: isHorizontal ? root.width : root.height
|
||||||
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
|
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
|
||||||
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
|
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
|
||||||
required property Flickable flickable
|
readonly property real nonAnimHeight: root.axisSize / root.axisContentSize
|
||||||
readonly property real nonAnimHeight: flickable.height / flickable.contentHeight
|
readonly property real nonAnimY: root.axisContentPos / root.axisContentSize
|
||||||
readonly property real nonAnimY: flickable.contentY / flickable.contentHeight
|
|
||||||
readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight)
|
readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight)
|
||||||
readonly property bool reversed: flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop
|
readonly property bool reversed: isHorizontal ? (flickable instanceof ListView && flickable.layoutDirection === Qt.RightToLeft) : (flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop)
|
||||||
property bool shouldBeActive
|
property bool shouldBeActive
|
||||||
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
|
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
|
||||||
|
|
||||||
enabled: !Visibilities.getForActive().isDrawing
|
enabled: !Visibilities.getForActive().isDrawing
|
||||||
implicitWidth: Tokens.padding.extraSmall * 2
|
implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
|
||||||
|
implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
|
||||||
|
|
||||||
contentItem: Item {
|
contentItem: Item {}
|
||||||
}
|
|
||||||
Behavior on position {
|
Behavior on position {
|
||||||
enabled: !fullMouse.pressed
|
enabled: !fullMouse.pressed
|
||||||
|
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onHoveredChanged: {
|
onHoveredChanged: {
|
||||||
@@ -47,54 +52,27 @@ ScrollBar {
|
|||||||
target: root.flickable
|
target: root.flickable
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomClippingRect {
|
Loader {
|
||||||
anchors.bottom: parent.bottom
|
anchors.fill: parent
|
||||||
anchors.right: parent.right
|
active: root.size < 1
|
||||||
anchors.top: parent.top
|
sourceComponent: root.isHorizontal ? horizontalTrack : verticalTrack
|
||||||
implicitWidth: handle.implicitWidth
|
}
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
CustomRect {
|
Component {
|
||||||
id: handle
|
id: verticalTrack
|
||||||
|
|
||||||
anchors.right: parent.right
|
VerticalScrollBarTrack {
|
||||||
color: Colors.palette.m3secondary
|
scrollBar: root
|
||||||
implicitHeight: root.height * root.effectiveSize
|
mouseArea: fullMouse
|
||||||
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
|
}
|
||||||
opacity: {
|
}
|
||||||
if (!root.enabled)
|
|
||||||
return 0;
|
|
||||||
if (root.size === 1)
|
|
||||||
return 0;
|
|
||||||
if (fullMouse.pressed)
|
|
||||||
return 1;
|
|
||||||
if (fullMouse.containsMouse)
|
|
||||||
return 0.8;
|
|
||||||
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
|
|
||||||
return 0.6;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
y: root.reversed ? root.height * (1 + root.nonAnimY) * root.travelScale : root.height * root.nonAnimY * root.travelScale
|
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Component {
|
||||||
Anim {
|
id: horizontalTrack
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
HorizontalScrollBarTrack {
|
||||||
id: mouse
|
scrollBar: root
|
||||||
|
mouseArea: fullMouse
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
hoverEnabled: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,25 +89,40 @@ ScrollBar {
|
|||||||
|
|
||||||
property real pressOffset: 0
|
property real pressOffset: 0
|
||||||
|
|
||||||
function contentYFromThumbTop(thumbTop) {
|
function contentPosFromThumbStart(thumbStart) {
|
||||||
var visualPos = root.effectiveTravel > 0 ? thumbTop / root.travelScale : 0;
|
var visualPos = root.effectiveTravel > 0 ? thumbStart / root.travelScale : 0;
|
||||||
|
return root.reversed ? (visualPos - 1) * root.axisContentSize : visualPos * root.axisContentSize;
|
||||||
return root.reversed ? (visualPos - 1) * root.flickable.contentHeight : visualPos * root.flickable.contentHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromEvent(event) {
|
function updateFromEvent(event) {
|
||||||
var posInTrack = event.y / root.height;
|
var eventPos = root.isHorizontal ? event.x : event.y;
|
||||||
var thumbTop = posInTrack - pressOffset;
|
var posInTrack = eventPos / root.axisLength;
|
||||||
thumbTop = Math.max(0, Math.min(root.effectiveTravel, thumbTop));
|
var thumbStart = posInTrack - pressOffset;
|
||||||
|
thumbStart = Math.max(0, Math.min(root.effectiveTravel, thumbStart));
|
||||||
|
|
||||||
root.flickable.contentY = contentYFromThumbTop(thumbTop);
|
var newPos = contentPosFromThumbStart(thumbStart);
|
||||||
|
if (root.isHorizontal)
|
||||||
|
root.flickable.contentX = newPos + (root.flickable.originX + (root.reversed ? root.flickable.contentWidth : 0));
|
||||||
|
else
|
||||||
|
root.flickable.contentY = newPos + (root.flickable.originY + (root.reversed ? root.flickable.contentHeight : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
function visualThumbTop() {
|
function visualThumbStart() {
|
||||||
const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
|
const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
|
||||||
return visualPos * root.travelScale;
|
return visualPos * root.travelScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onWheel(event: WheelEvent): void {
|
||||||
|
if (root.horizontal) {
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
|
||||||
|
var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
|
||||||
|
root.position = newPos;
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: undefined
|
cursorShape: undefined
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
@@ -140,24 +133,15 @@ ScrollBar {
|
|||||||
updateFromEvent(event);
|
updateFromEvent(event);
|
||||||
}
|
}
|
||||||
onPressed: event => {
|
onPressed: event => {
|
||||||
var currentTop = visualThumbTop();
|
var currentStart = visualThumbStart();
|
||||||
var currentBottom = currentTop + root.effectiveSize;
|
var currentEnd = currentStart + root.effectiveSize;
|
||||||
var clickPos = event.y / root.height;
|
var eventPos = root.isHorizontal ? event.x : event.y;
|
||||||
|
var clickPos = eventPos / root.axisLength;
|
||||||
|
|
||||||
var clickedInsideThumb = clickPos >= currentTop && clickPos <= currentBottom;
|
var clickedInsideThumb = clickPos >= currentStart && clickPos <= currentEnd;
|
||||||
|
pressOffset = clickedInsideThumb ? (clickPos - currentStart) : root.effectiveSize / 2;
|
||||||
if (clickedInsideThumb) {
|
|
||||||
pressOffset = clickPos - currentTop;
|
|
||||||
} else {
|
|
||||||
pressOffset = root.effectiveSize / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFromEvent(event);
|
updateFromEvent(event);
|
||||||
}
|
}
|
||||||
onWheel: event => {
|
|
||||||
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
|
|
||||||
var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
|
|
||||||
root.position = newPos;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import QtQuick
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
id: track
|
||||||
|
|
||||||
|
required property var scrollBar
|
||||||
|
required property var mouseArea
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
implicitHeight: handle.implicitHeight
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: handle
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
color: Colors.palette.m3secondary
|
||||||
|
implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
|
||||||
|
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
|
||||||
|
opacity: {
|
||||||
|
if (!track.scrollBar.enabled)
|
||||||
|
return 0;
|
||||||
|
if (track.scrollBar.size === 1)
|
||||||
|
return 0;
|
||||||
|
if (track.mouseArea.pressed)
|
||||||
|
return 1;
|
||||||
|
if (track.mouseArea.containsMouse)
|
||||||
|
return 0.8;
|
||||||
|
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
|
||||||
|
return 0.6;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Templates
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
TextArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
color: Colors.palette.m3onSurface
|
||||||
|
cursorVisible: !readOnly
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
implicitHeight: contentHeight + topPadding + bottomPadding
|
||||||
|
implicitWidth: contentWidth + leftPadding + rightPadding
|
||||||
|
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
|
||||||
|
renderType: TextArea.NativeRendering
|
||||||
|
selectedTextColor: color
|
||||||
|
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
Behavior on selectionColor {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
cursorDelegate: Item {}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: cursor
|
||||||
|
|
||||||
|
property bool disableBlink
|
||||||
|
|
||||||
|
color: Colors.palette.m3primary
|
||||||
|
implicitHeight: root.cursorRectangle.height
|
||||||
|
implicitWidth: 1.5
|
||||||
|
radius: Tokens.rounding.largeIncreased
|
||||||
|
x: root.cursorRectangle.x
|
||||||
|
y: root.cursorRectangle.y
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveFastEffects
|
||||||
|
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.StandardSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCursorPositionChanged(): void {
|
||||||
|
if (root.activeFocus && root.cursorVisible) {
|
||||||
|
cursor.opacity = 1;
|
||||||
|
cursor.disableBlink = true;
|
||||||
|
enableBlink.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: enableBlink
|
||||||
|
|
||||||
|
interval: 500
|
||||||
|
|
||||||
|
onTriggered: cursor.disableBlink = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 500
|
||||||
|
repeat: true
|
||||||
|
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
|
||||||
|
triggeredOnStart: true
|
||||||
|
|
||||||
|
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
cursor.opacity: 0
|
||||||
|
when: !root.activeFocus || !root.cursorVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Templates
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool animateCursor: true
|
||||||
|
property alias cursor: cursor
|
||||||
|
|
||||||
|
color: Colors.palette.m3onSurface
|
||||||
|
cursorVisible: !readOnly
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
font.family: Config.appearance.font.family.sans
|
||||||
|
renderType: TextField.NativeRendering
|
||||||
|
selectedTextColor: color
|
||||||
|
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursorDelegate: Item {
|
||||||
|
}
|
||||||
|
Behavior on selectionColor {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: cursor
|
||||||
|
|
||||||
|
property bool disableBlink
|
||||||
|
|
||||||
|
color: Colors.palette.m3primary
|
||||||
|
implicitHeight: root.cursorRectangle.height
|
||||||
|
implicitWidth: 1.5
|
||||||
|
radius: Tokens.rounding.largeIncreased
|
||||||
|
x: root.cursorRectangle.x
|
||||||
|
y: root.cursorRectangle.y
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.StandardSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on x {
|
||||||
|
enabled: root.animateCursor
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveFastEffects
|
||||||
|
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on y {
|
||||||
|
enabled: root.animateCursor
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveFastEffects
|
||||||
|
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCursorPositionChanged(): void {
|
||||||
|
if (root.activeFocus && root.cursorVisible) {
|
||||||
|
cursor.opacity = 1;
|
||||||
|
cursor.disableBlink = true;
|
||||||
|
enableBlink.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: enableBlink
|
||||||
|
|
||||||
|
interval: 500
|
||||||
|
|
||||||
|
onTriggered: cursor.disableBlink = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 500
|
||||||
|
repeat: true
|
||||||
|
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
|
||||||
|
triggeredOnStart: true
|
||||||
|
|
||||||
|
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
cursor.opacity: 0
|
||||||
|
when: !root.activeFocus || !root.cursorVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ CustomListView {
|
|||||||
|
|
||||||
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||||
property real fadeAmount: 0.1
|
property real fadeAmount: 0.1
|
||||||
|
property real fadeThreshold: 0.0
|
||||||
readonly property bool horizontal: orientation === ListView.Horizontal
|
readonly property bool horizontal: orientation === ListView.Horizontal
|
||||||
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||||
|
|
||||||
@@ -28,11 +29,11 @@ CustomListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function marginEnd(): real {
|
function marginEnd(): real {
|
||||||
return horizontal ? rightMargin : bottomMargin;
|
return horizontal ? rightMargin - fadeThreshold : bottomMargin - fadeThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
function marginStart(): real {
|
function marginStart(): real {
|
||||||
return horizontal ? leftMargin : topMargin;
|
return horizontal ? leftMargin - fadeThreshold : topMargin - fadeThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
function overshootStart(): real {
|
function overshootStart(): real {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import QtQuick
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
id: track
|
||||||
|
|
||||||
|
required property var scrollBar
|
||||||
|
required property var mouseArea
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
implicitWidth: handle.implicitWidth
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: handle
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
color: Colors.palette.m3secondary
|
||||||
|
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
|
||||||
|
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
|
||||||
|
opacity: {
|
||||||
|
if (!track.scrollBar.enabled)
|
||||||
|
return 0;
|
||||||
|
if (track.scrollBar.size === 1)
|
||||||
|
return 0;
|
||||||
|
if (track.mouseArea.pressed)
|
||||||
|
return 1;
|
||||||
|
if (track.mouseArea.containsMouse)
|
||||||
|
return 0.8;
|
||||||
|
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
|
||||||
|
return 0.6;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
|
||||||
|
|
||||||
|
Behavior on implicitWidth {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -140,7 +140,6 @@ Item {
|
|||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
popouts: popouts
|
|
||||||
sidebar: sidebar
|
sidebar: sidebar
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-7
@@ -55,7 +55,7 @@ CustomWindow {
|
|||||||
property color surfaceColor: Colors.tPalette.m3surface
|
property color surfaceColor: Colors.tPalette.m3surface
|
||||||
|
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings || visibilities.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||||
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
|
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
contentItem.focus: true
|
contentItem.focus: true
|
||||||
@@ -63,12 +63,10 @@ CustomWindow {
|
|||||||
name: "Bar"
|
name: "Bar"
|
||||||
|
|
||||||
Behavior on fsTransitionProg {
|
Behavior on fsTransitionProg {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on surfaceColor {
|
Behavior on surfaceColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem.Keys.onEscapePressed: {
|
contentItem.Keys.onEscapePressed: {
|
||||||
@@ -293,8 +291,7 @@ CustomWindow {
|
|||||||
y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0)
|
y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0)
|
||||||
|
|
||||||
Behavior on extraExtent {
|
Behavior on extraExtent {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,21 +13,17 @@ import qs.Paths
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property M3Palette current: M3Palette {
|
readonly property M3Palette current: M3Palette {}
|
||||||
}
|
|
||||||
property bool currentLight
|
property bool currentLight
|
||||||
property string flavour
|
property string flavour
|
||||||
readonly property bool light: showPreview ? previewLight : currentLight
|
readonly property bool light: showPreview ? previewLight : currentLight
|
||||||
readonly property M3Palette palette: showPreview ? preview : current
|
readonly property M3Palette palette: showPreview ? preview : current
|
||||||
readonly property M3Palette preview: M3Palette {
|
readonly property M3Palette preview: M3Palette {}
|
||||||
}
|
|
||||||
property bool previewLight
|
property bool previewLight
|
||||||
property string scheme
|
property string scheme
|
||||||
property bool showPreview
|
property bool showPreview
|
||||||
readonly property M3TPalette tPalette: M3TPalette {
|
readonly property M3TPalette tPalette: M3TPalette {}
|
||||||
}
|
readonly property Transparency transparency: Transparency {}
|
||||||
readonly property Transparency transparency: Transparency {
|
|
||||||
}
|
|
||||||
readonly property alias wallLuminance: analyser.luminance
|
readonly property alias wallLuminance: analyser.luminance
|
||||||
|
|
||||||
function alterColor(c: color, a: real, layer: int): color {
|
function alterColor(c: color, a: real, layer: int): color {
|
||||||
@@ -112,8 +108,7 @@ Singleton {
|
|||||||
|
|
||||||
function setMode(mode: string): void {
|
function setMode(mode: string): void {
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
|
||||||
Config.general.color.mode = mode;
|
Config.colors.mode = mode;
|
||||||
Config.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function swapRG(c: color): color {
|
function swapRG(c: color): color {
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property bool enabled: service.enabled
|
readonly property bool enabled: service.enabled
|
||||||
readonly property int end: Config.general.color.scheduleHyprsunsetEnd
|
readonly property int end: Config.display.nightlight.scheduleEnd
|
||||||
property bool manualToggle: false
|
property bool manualToggle: false
|
||||||
readonly property int start: Config.general.color.scheduleHyprsunsetStart
|
readonly property int start: Config.display.nightlight.scheduleStart
|
||||||
readonly property int temp: Config.general.color.hyprsunsetTemp
|
readonly property int temp: Config.display.nightlight.temp
|
||||||
|
|
||||||
function checkStartup(): void {
|
function checkStartup(): void {
|
||||||
if (!Config.general.color.scheduleHyprsunset)
|
if (!Config.display.nightlight.schedule)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
service.apply();
|
service.apply();
|
||||||
@@ -26,12 +26,14 @@ Singleton {
|
|||||||
service.toggle();
|
service.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprsunsetManager {
|
NightlightManager {
|
||||||
id: service
|
id: service
|
||||||
|
|
||||||
activeAuto: Config.general.color.scheduleHyprsunset
|
activeAuto: Config.display.nightlight.schedule
|
||||||
endTime: root.end
|
endTime: root.end
|
||||||
startTime: root.start
|
startTime: root.start
|
||||||
temp: root.temp
|
temp: root.temp
|
||||||
|
fadeDuration: Config.display.nightlight.fadeDuration / 1000
|
||||||
|
useNativeNightlight: Config.display.nightlight.useNative
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import qs.Services
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int darkEnd: Config.general.color.scheduleDarkEnd
|
readonly property int darkEnd: Config.colors.scheduleDarkEnd
|
||||||
readonly property int darkStart: Config.general.color.scheduleDarkStart
|
readonly property int darkStart: Config.colors.scheduleDarkStart
|
||||||
readonly property bool enabled: Config.general.color.scheduleDark && Config.general.color.schemeGeneration
|
readonly property bool enabled: Config.colors.scheduleDark && Config.colors.schemeGen
|
||||||
|
|
||||||
function applyDarkMode() {
|
function applyDarkMode() {
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]);
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]);
|
||||||
|
|
||||||
Config.general.color.mode = "dark";
|
Config.colors.mode = "dark";
|
||||||
|
|
||||||
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]);
|
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]);
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ Singleton {
|
|||||||
function applyLightMode() {
|
function applyLightMode() {
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "light"]);
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "light"]);
|
||||||
|
|
||||||
Config.general.color.mode = "light";
|
Config.colors.mode = "light";
|
||||||
|
|
||||||
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]);
|
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]);
|
||||||
|
|
||||||
Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]);
|
Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]);
|
||||||
|
|
||||||
if (Config.general.color.neovimColors)
|
if (Config.colors.neovimColors)
|
||||||
Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]);
|
Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-16
@@ -126,33 +126,29 @@ Item {
|
|||||||
focus: true
|
focus: true
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
duration: 300
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on rsx {
|
Behavior on rsx {
|
||||||
enabled: !selectArea.pressed && root.mode === "select"
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on rsy {
|
Behavior on rsy {
|
||||||
enabled: !selectArea.pressed && root.mode === "select"
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on sh {
|
Behavior on sh {
|
||||||
enabled: !selectArea.pressed && root.mode === "select"
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on sw {
|
Behavior on sw {
|
||||||
enabled: !selectArea.pressed && root.mode === "select"
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
duration: 300
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,8 +348,7 @@ Item {
|
|||||||
y: selectionRect.y - root.realBorderWidth
|
y: selectionRect.y - root.realBorderWidth
|
||||||
|
|
||||||
Behavior on border.color {
|
Behavior on border.color {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +397,5 @@ Item {
|
|||||||
onUndoRequested: annotations.undo()
|
onUndoRequested: annotations.undo()
|
||||||
}
|
}
|
||||||
|
|
||||||
component ExAnim: Anim {
|
component ExAnim: Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Searcher {
|
|||||||
previewPath = path;
|
previewPath = path;
|
||||||
showPreview = true;
|
showPreview = true;
|
||||||
|
|
||||||
if (Config.general.color.schemeGeneration)
|
if (Config.colors.schemeGen)
|
||||||
previewColorsProc.running = true;
|
previewColorsProc.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ Searcher {
|
|||||||
WallpaperPath.currentWallpaperPath = path;
|
WallpaperPath.currentWallpaperPath = path;
|
||||||
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
|
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
|
||||||
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
||||||
if (Config.general.color.schemeGeneration)
|
if (Config.colors.schemeGen)
|
||||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPreview(): void {
|
function stopPreview(): void {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ WidgetBase {
|
|||||||
}
|
}
|
||||||
required property GridLayout loader
|
required property GridLayout loader
|
||||||
required property Wrapper popouts
|
required property Wrapper popouts
|
||||||
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.small * 2 : verticalColumn.implicitHeight + Tokens.padding.small * 2
|
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
|
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
|
||||||
@@ -57,14 +57,13 @@ WidgetBase {
|
|||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: root.contentColor
|
color: root.contentColor
|
||||||
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
text: Time.dateStr
|
text: Time.dateStr
|
||||||
visible: root.horizontal
|
visible: root.horizontal
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,12 +83,11 @@ WidgetBase {
|
|||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: root.contentColor
|
color: root.contentColor
|
||||||
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
|
||||||
text: Qt.formatDateTime(Time.date, modelData)
|
text: Qt.formatDateTime(Time.date, modelData)
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +112,7 @@ WidgetBase {
|
|||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: root.contentColor
|
color: root.contentColor
|
||||||
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
|
||||||
text: {
|
text: {
|
||||||
if (modelData.includes("h"))
|
if (modelData.includes("h"))
|
||||||
return Time.hourStr;
|
return Time.hourStr;
|
||||||
@@ -126,8 +124,7 @@ WidgetBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,13 +132,12 @@ WidgetBase {
|
|||||||
CustomText {
|
CustomText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: root.contentColor
|
color: root.contentColor
|
||||||
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
|
font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
|
||||||
text: Qt.formatDateTime(Time.date, "AP")
|
text: Qt.formatDateTime(Time.date, "AP")
|
||||||
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
|
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ CustomListView {
|
|||||||
return [0];
|
return [0];
|
||||||
case "variant":
|
case "variant":
|
||||||
return SchemeVariants.query(text);
|
return SchemeVariants.query(text);
|
||||||
|
case "files":
|
||||||
|
return Files.query(text);
|
||||||
default:
|
default:
|
||||||
return Apps.search(text);
|
return Apps.search(text);
|
||||||
}
|
}
|
||||||
@@ -33,7 +35,7 @@ CustomListView {
|
|||||||
function stateForText(text: string): string {
|
function stateForText(text: string): string {
|
||||||
const prefix = Config.launcher.actionPrefix;
|
const prefix = Config.launcher.actionPrefix;
|
||||||
if (text.startsWith(prefix)) {
|
if (text.startsWith(prefix)) {
|
||||||
for (const action of ["calc", "scheme", "variant"])
|
for (const action of ["calc", "scheme", "variant", "files"])
|
||||||
if (text.startsWith(`${prefix}${action} `))
|
if (text.startsWith(`${prefix}${action} `))
|
||||||
return action;
|
return action;
|
||||||
|
|
||||||
@@ -163,6 +165,13 @@ CustomListView {
|
|||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
root.delegate: variantItem
|
root.delegate: variantItem
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "files"
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
root.delegate: filesItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
transitions: Transition {
|
transitions: Transition {
|
||||||
@@ -264,6 +273,14 @@ CustomListView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: filesItem
|
||||||
|
|
||||||
|
FilesItem {
|
||||||
|
list: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onTextChanged() {
|
function onTextChanged() {
|
||||||
root.syncDisplayText();
|
root.syncDisplayText();
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ Item {
|
|||||||
} else if (text.startsWith(Config.launcher.actionPrefix)) {
|
} else if (text.startsWith(Config.launcher.actionPrefix)) {
|
||||||
if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
|
if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
|
||||||
currentItem.onClicked();
|
currentItem.onClicked();
|
||||||
|
else if (text.startsWith(`${Config.launcher.actionPrefix}files `))
|
||||||
|
currentItem.onClicked();
|
||||||
else
|
else
|
||||||
currentItem.modelData.onClicked(list.currentList);
|
currentItem.modelData.onClicked(list.currentList);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Item {
|
|||||||
implicitHeight: Config.launcher.sizes.itemHeight
|
implicitHeight: Config.launcher.sizes.itemHeight
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.medium
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.modelData?.onClicked(root.list);
|
root.modelData?.onClicked(root.list);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Item {
|
|||||||
implicitHeight: Config.launcher.sizes.itemHeight
|
implicitHeight: Config.launcher.sizes.itemHeight
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.medium
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Apps.launch(root.modelData);
|
Apps.launch(root.modelData);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Item {
|
|||||||
implicitHeight: Config.launcher.sizes.itemHeight
|
implicitHeight: Config.launcher.sizes.itemHeight
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.medium
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.onClicked();
|
root.onClicked();
|
||||||
@@ -97,8 +97,7 @@ Item {
|
|||||||
text: qsTr("Open in calculator")
|
text: qsTr("Open in calculator")
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Modules.Launcher.Services
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var list
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
anchors.left: parent?.left
|
||||||
|
anchors.right: parent?.right
|
||||||
|
implicitHeight: Config.launcher.sizes.itemHeight
|
||||||
|
|
||||||
|
function onClicked(): void {
|
||||||
|
root.list.visibilities.launcher = false;
|
||||||
|
if (root.modelData?.path)
|
||||||
|
Files.launch(["xdg-open", root.modelData.path]);
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
id: sLayer
|
||||||
|
|
||||||
|
radius: Tokens.rounding.medium
|
||||||
|
manualHoverOverride: openLocation.hovered
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.onClicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: openLocation
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Tokens.padding.medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
icon: "open_in_new"
|
||||||
|
isRound: true
|
||||||
|
|
||||||
|
scale: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0.4
|
||||||
|
opacity: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.list.visibilities.launcher = false;
|
||||||
|
if (root.modelData?.path)
|
||||||
|
Files.showInFolder(root.modelData.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Tokens.padding.small
|
||||||
|
anchors.margins: Tokens.padding.small
|
||||||
|
anchors.rightMargin: Tokens.padding.small
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: icon
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
implicitSize: Tokens.font.size.extraLarge
|
||||||
|
source: Quickshell.iconPath(root.modelData?.icon) ?? ""
|
||||||
|
visible: !root.modelData?.isImage
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
asynchronous: true
|
||||||
|
opacity: root.modelData?.isImage && image.status === Image.Loading ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceComponent: LoadingIndicator {
|
||||||
|
color: Colors.palette.m3primaryContainer
|
||||||
|
implicitSize: Tokens.font.size.extraLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FadeImage {
|
||||||
|
id: image
|
||||||
|
|
||||||
|
property int implicitSize: Tokens.font.size.extraLarge
|
||||||
|
|
||||||
|
width: implicitSize
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
source: root.modelData?.isImage ? root.modelData?.path ?? "" : ""
|
||||||
|
visible: root.modelData?.isImage
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.left: icon.right
|
||||||
|
anchors.leftMargin: Tokens.spacing.medium
|
||||||
|
anchors.verticalCenter: icon.verticalCenter
|
||||||
|
implicitHeight: name.implicitHeight + desc.implicitHeight
|
||||||
|
implicitWidth: parent.width - icon.width
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: name
|
||||||
|
|
||||||
|
font.pointSize: Tokens.font.size.normal
|
||||||
|
text: root.modelData?.name ?? ""
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: desc
|
||||||
|
|
||||||
|
anchors.top: name.bottom
|
||||||
|
color: Colors.palette.m3outline
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
text: root.modelData?.path ?? ""
|
||||||
|
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ Item {
|
|||||||
implicitHeight: Config.launcher.sizes.itemHeight
|
implicitHeight: Config.launcher.sizes.itemHeight
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.medium
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.modelData?.onClicked(root.list);
|
root.modelData?.onClicked(root.list);
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
// qs/Services/Files.qml
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import ZShell.Config
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property list<var> results: []
|
||||||
|
|
||||||
|
property string _pendingQuery: ""
|
||||||
|
property int _generation: 0
|
||||||
|
|
||||||
|
readonly property var _imageExts: ({
|
||||||
|
"png": true,
|
||||||
|
"jpg": true,
|
||||||
|
"jpeg": true,
|
||||||
|
"gif": true,
|
||||||
|
"bmp": true,
|
||||||
|
"webp": true,
|
||||||
|
"svg": true,
|
||||||
|
"tiff": true,
|
||||||
|
"tif": true,
|
||||||
|
"ico": true,
|
||||||
|
"heic": true,
|
||||||
|
"avif": true
|
||||||
|
})
|
||||||
|
|
||||||
|
// freedesktop mimetype-icon names; extend as needed
|
||||||
|
readonly property var _extIcons: ({
|
||||||
|
// images
|
||||||
|
"png": "image-x-generic",
|
||||||
|
"jpg": "image-x-generic",
|
||||||
|
"jpeg": "image-x-generic",
|
||||||
|
"gif": "image-x-generic",
|
||||||
|
"bmp": "image-x-generic",
|
||||||
|
"webp": "image-x-generic",
|
||||||
|
"svg": "image-x-generic",
|
||||||
|
"tiff": "image-x-generic",
|
||||||
|
"tif": "image-x-generic",
|
||||||
|
"ico": "image-x-generic",
|
||||||
|
"heic": "image-x-generic",
|
||||||
|
"avif": "image-x-generic",
|
||||||
|
// documents
|
||||||
|
"pdf": "application-pdf",
|
||||||
|
"doc": "x-office-document",
|
||||||
|
"docx": "x-office-document",
|
||||||
|
"odt": "x-office-document",
|
||||||
|
"txt": "text-x-generic",
|
||||||
|
"md": "text-x-generic",
|
||||||
|
"xls": "x-office-spreadsheet",
|
||||||
|
"xlsx": "x-office-spreadsheet",
|
||||||
|
"ods": "x-office-spreadsheet",
|
||||||
|
"ppt": "x-office-presentation",
|
||||||
|
"pptx": "x-office-presentation",
|
||||||
|
"odp": "x-office-presentation",
|
||||||
|
// archives
|
||||||
|
"zip": "package-x-generic",
|
||||||
|
"tar": "package-x-generic",
|
||||||
|
"gz": "package-x-generic",
|
||||||
|
"xz": "package-x-generic",
|
||||||
|
"7z": "package-x-generic",
|
||||||
|
"rar": "package-x-generic",
|
||||||
|
// audio/video
|
||||||
|
"mp3": "audio-x-generic",
|
||||||
|
"flac": "audio-x-generic",
|
||||||
|
"wav": "audio-x-generic",
|
||||||
|
"ogg": "audio-x-generic",
|
||||||
|
"mp4": "video-x-generic",
|
||||||
|
"mkv": "video-x-generic",
|
||||||
|
"webm": "video-x-generic",
|
||||||
|
"avi": "video-x-generic",
|
||||||
|
"mov": "video-x-generic",
|
||||||
|
// code
|
||||||
|
"js": "text-x-script",
|
||||||
|
"ts": "text-x-script",
|
||||||
|
"py": "text-x-script",
|
||||||
|
"sh": "text-x-script",
|
||||||
|
"qml": "text-x-script",
|
||||||
|
"cpp": "text-x-c++src",
|
||||||
|
"c": "text-x-csrc",
|
||||||
|
"h": "text-x-chdr",
|
||||||
|
"json": "application-json",
|
||||||
|
"html": "text-html",
|
||||||
|
"css": "text-css"
|
||||||
|
})
|
||||||
|
|
||||||
|
function showInFolder(path: string): void {
|
||||||
|
const explorer = Config.general.apps.explorer;
|
||||||
|
let args = [];
|
||||||
|
|
||||||
|
if (explorer.includes("dolphin"))
|
||||||
|
args = [...explorer, "--select", path];
|
||||||
|
else
|
||||||
|
args = [...explorer, path];
|
||||||
|
|
||||||
|
if (Config.launcher.uwsm)
|
||||||
|
Quickshell.execDetached(["app2unit", "--", ...args]);
|
||||||
|
else
|
||||||
|
Quickshell.execDetached([...args]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function launch(command: list<string>): void {
|
||||||
|
if (Config.launcher.uwsm)
|
||||||
|
Quickshell.execDetached(["app2unit", "--"].concat(command));
|
||||||
|
else
|
||||||
|
Quickshell.execDetached(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformSearch(search: string): string {
|
||||||
|
return search.slice(Config.launcher.actionPrefix.length + "files ".length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function query(search: string): list<var> {
|
||||||
|
search = transformSearch(search);
|
||||||
|
if (search !== _pendingQuery) {
|
||||||
|
_pendingQuery = search;
|
||||||
|
debounceTimer.restart();
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _extOf(name: string): string {
|
||||||
|
const i = name.lastIndexOf(".");
|
||||||
|
// no dot, or dotfile with no real extension (".bashrc" -> treat as no ext)
|
||||||
|
if (i <= 0)
|
||||||
|
return "";
|
||||||
|
return name.slice(i + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _iconFor(name: string, isDir: bool): string {
|
||||||
|
if (isDir)
|
||||||
|
return "folder";
|
||||||
|
const ext = _extOf(name);
|
||||||
|
return _extIcons[ext] ?? "text-x-generic";
|
||||||
|
}
|
||||||
|
|
||||||
|
function _buildEntry(path: string): var {
|
||||||
|
const name = path.split("/").pop();
|
||||||
|
const ext = _extOf(name);
|
||||||
|
// trailing slash from plocate's directory output (if -d/dir results ever appear)
|
||||||
|
const isDir = path.endsWith("/");
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: path,
|
||||||
|
name: name,
|
||||||
|
icon: _iconFor(name, isDir),
|
||||||
|
isImage: !isDir && (_imageExts[ext] ?? false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: debounceTimer
|
||||||
|
interval: 120
|
||||||
|
onTriggered: root._runQuery(root._pendingQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _runQuery(search: string): void {
|
||||||
|
_generation++;
|
||||||
|
|
||||||
|
if (!search) {
|
||||||
|
proc.running = false;
|
||||||
|
results = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proc.running)
|
||||||
|
proc.running = false;
|
||||||
|
|
||||||
|
proc.buffer = [];
|
||||||
|
proc.myGeneration = _generation;
|
||||||
|
proc.command = ["plocate", "-i", "-b", "--limit", "50", search];
|
||||||
|
proc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: proc
|
||||||
|
|
||||||
|
property var buffer: []
|
||||||
|
property int myGeneration: 0
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: line => proc.buffer.push(root._buildEntry(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
onRunningChanged: {
|
||||||
|
if (!running && myGeneration === root._generation)
|
||||||
|
root.results = buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import QtQuick
|
||||||
|
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
|
||||||
|
property bool following: true
|
||||||
|
// 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
|
||||||
|
|
||||||
|
function send(text: string): void {
|
||||||
|
if (text.trim() === "")
|
||||||
|
return;
|
||||||
|
following = true;
|
||||||
|
chatData.sendMessage(text);
|
||||||
|
input.text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.pointSize: Tokens.font.size.larger
|
||||||
|
text: qsTr(root.chatData.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: VerticalFadeListView {
|
||||||
|
id: list
|
||||||
|
|
||||||
|
property bool userScrolledUp: false
|
||||||
|
property real lastCHeight: 0.0
|
||||||
|
|
||||||
|
function scrollToBottom(): void {
|
||||||
|
scrollAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheBuffer: Math.max(height * 20, 0)
|
||||||
|
clip: true
|
||||||
|
fadeAmount: 0.05
|
||||||
|
fadeThreshold: Tokens.padding.medium
|
||||||
|
model: root.chatData.messagesModel
|
||||||
|
spacing: 0
|
||||||
|
rotation: 180
|
||||||
|
add: Transition {
|
||||||
|
Anim {
|
||||||
|
from: -100
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomScrollBar.vertical: CustomScrollBar {
|
||||||
|
id: scrollBar
|
||||||
|
|
||||||
|
flickable: list
|
||||||
|
parent: list.parent
|
||||||
|
anchors.top: list.top
|
||||||
|
anchors.right: list.right
|
||||||
|
anchors.bottom: list.bottom
|
||||||
|
|
||||||
|
transform: Rotation {
|
||||||
|
origin.y: list.height / 2
|
||||||
|
origin.x: scrollBar.width / 2
|
||||||
|
angle: 180
|
||||||
|
|
||||||
|
axis {
|
||||||
|
y: 0
|
||||||
|
x: 1
|
||||||
|
z: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate: MessageDelegate {
|
||||||
|
rotation: 180
|
||||||
|
rootParent: list
|
||||||
|
}
|
||||||
|
|
||||||
|
displaced: Transition {
|
||||||
|
Anim {
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
move: Transition {
|
||||||
|
Anim {
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
positionViewAtBeginning();
|
||||||
|
}
|
||||||
|
onAtYBeginningChanged: {
|
||||||
|
if (atYBeginning)
|
||||||
|
userScrolledUp = false;
|
||||||
|
}
|
||||||
|
onContentHeightChanged: {
|
||||||
|
if (userScrolledUp && Chat.busy) {
|
||||||
|
const delta = contentHeight - lastCHeight;
|
||||||
|
contentY += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCHeight = contentHeight;
|
||||||
|
}
|
||||||
|
onMovingChanged: {
|
||||||
|
if (moving)
|
||||||
|
userScrolledUp = !atYBeginning;
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
id: scrollAnim
|
||||||
|
|
||||||
|
property: "contentY"
|
||||||
|
target: list
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
to: list.originY
|
||||||
|
}
|
||||||
|
|
||||||
|
WheelInverter {
|
||||||
|
target: list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the load even before we're ready to show the 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: !listLoader.item.atYBeginning && listLoader.item.contentHeight > listLoader.item.height ? 1 : 0
|
||||||
|
visible: scale > 0
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Elevation {
|
||||||
|
anchors.fill: parent
|
||||||
|
level: 2
|
||||||
|
radius: scrollToBottom.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: scrollToBottom
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.pointSize: Tokens.font.size.large
|
||||||
|
icon: "arrow_downward"
|
||||||
|
isRound: true
|
||||||
|
padding: Tokens.padding.extraSmall
|
||||||
|
type: IconButton.Tonal
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
listLoader.item.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatInput {
|
||||||
|
id: input
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
bg.border.color: Colors.palette.m3outlineVariant
|
||||||
|
bg.color: Colors.tPalette.m3surfaceContainerLowest
|
||||||
|
font.pointSize: Tokens.font.size.normal
|
||||||
|
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
|
||||||
|
focus: true
|
||||||
|
placeholderText: qsTr("Send a message")
|
||||||
|
sendIcon.font.pointSize: Tokens.font.size.large
|
||||||
|
sendIcon.icon: "arrow_upward"
|
||||||
|
sendIcon.padding: Tokens.padding.extraSmall
|
||||||
|
|
||||||
|
Component.onCompleted: root.focusInput()
|
||||||
|
Keys.onPressed: e => {
|
||||||
|
if (e.key == Qt.Key_Return) {
|
||||||
|
if (!(e.modifiers & Qt.ShiftModifier)) {
|
||||||
|
root.send(text);
|
||||||
|
e.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSendPressed: root.send(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
import QtQuick
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Llm
|
||||||
|
import qs.Effects
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
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
|
||||||
|
font.pointSize: Tokens.font.size.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.pointSize: Tokens.font.size.normal
|
||||||
|
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.top: titleText.top
|
||||||
|
// anchors.bottom: titleText.bottom
|
||||||
|
// anchors.left: titleText.left
|
||||||
|
// anchors.right: parent.right
|
||||||
|
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.pointSize: Tokens.font.size.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.pointSize: Tokens.font.size.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.pointSize: Tokens.font.size.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.pointSize: Tokens.font.size.small
|
||||||
|
maximumLineCount: 1
|
||||||
|
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 {
|
||||||
|
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?.activeGeneration.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.pointSize: Tokens.font.size.large
|
||||||
|
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.pointSize: Tokens.font.size.large
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import QtQuick
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
TextAreaBase {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property alias bg: bg
|
||||||
|
readonly property alias sendIcon: sendIcon
|
||||||
|
|
||||||
|
signal sendPressed
|
||||||
|
|
||||||
|
bottomPadding: Tokens.padding.large
|
||||||
|
leftPadding: Tokens.padding.extraLarge
|
||||||
|
rightPadding: sendIcon.width + sendIcon.anchors.rightMargin + Tokens.spacing.small
|
||||||
|
topPadding: Tokens.padding.large
|
||||||
|
wrapMode: TextAreaBase.Wrap
|
||||||
|
|
||||||
|
background: CustomRect {
|
||||||
|
id: bg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
|
radius: Tokens.rounding.extraLarge
|
||||||
|
|
||||||
|
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.sendPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
id: tapHandler
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
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
|
||||||
|
property bool slim: false
|
||||||
|
|
||||||
|
signal requestExpand
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (modelsList.count < 1)
|
||||||
|
return qsTr("No models found");
|
||||||
|
|
||||||
|
const pathList = Chat.model.split("\/");
|
||||||
|
const name = pathList[pathList.length - 1];
|
||||||
|
|
||||||
|
if (!name)
|
||||||
|
return qsTr("No model selected");
|
||||||
|
|
||||||
|
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 {
|
||||||
|
id: label
|
||||||
|
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
|
||||||
|
visible: modelsList.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
id: modelLayer
|
||||||
|
enabled: modelsList.count > 0
|
||||||
|
|
||||||
|
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: root.slim ? "left_panel_open" : "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
|
||||||
|
origin.x: fabRoot.label.width / 2
|
||||||
|
angle: ChatState.fabExpanded ? 135 : 0
|
||||||
|
|
||||||
|
Behavior on angle {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.slim && !ChatState.narrowSidebarExpanded) {
|
||||||
|
root.requestExpand();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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.inChat) {
|
||||||
|
stack.push(chatList);
|
||||||
|
stack.push(chatContent, {
|
||||||
|
"chatData": ChatState.chatSession
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
stack.push(chatList);
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView {
|
||||||
|
id: stack
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: chatList
|
||||||
|
|
||||||
|
ChatList {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: Chat.chats.values
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteChatRequest: chat => {
|
||||||
|
Chat.chats.remove(chat);
|
||||||
|
}
|
||||||
|
onLoadChatRequest: (chat, index) => {
|
||||||
|
stack.push(chatContent, {
|
||||||
|
"chatData": chat
|
||||||
|
});
|
||||||
|
ChatState.inChat = true;
|
||||||
|
ChatState.chatSession = chat;
|
||||||
|
}
|
||||||
|
onNewChatRequest: {
|
||||||
|
const data = Chat.chats.insert();
|
||||||
|
stack.push(chatContent, {
|
||||||
|
"chatData": data
|
||||||
|
});
|
||||||
|
ChatState.inChat = true;
|
||||||
|
ChatState.chatSession = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: chatContent
|
||||||
|
|
||||||
|
ChatContent {
|
||||||
|
onRequestClose: {
|
||||||
|
stack.pop();
|
||||||
|
ChatState.inChat = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import ZShell.Llm
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ChatSession chatSession
|
||||||
|
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 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: Visibilities.getForActive().sidebar
|
||||||
|
|
||||||
|
onSidebarVisibleChanged: if (!sidebarVisible)
|
||||||
|
fabExpanded = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Llm
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property ChatGeneration current
|
||||||
|
required property TextEdit edit
|
||||||
|
required property bool hovered
|
||||||
|
required property bool isUser
|
||||||
|
required property ChatMessage message
|
||||||
|
required property LlmSegment segment
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.family: Config.appearance.font.family.sans
|
||||||
|
label.font.pointSize: Tokens.font.size.small
|
||||||
|
type: IconButton.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== root.message.generationCount - 1
|
||||||
|
icon: "chevron_right"
|
||||||
|
type: IconButton.Tonal
|
||||||
|
|
||||||
|
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.current.streaming && 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: Quickshell.clipboardText = root.current.content
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: editButton
|
||||||
|
|
||||||
|
icon: root.edit.readOnly ? "edit" : "check"
|
||||||
|
inactiveColor: Colors.palette.m3tertiary
|
||||||
|
inactiveOnColor: Colors.palette.m3onTertiary
|
||||||
|
visible: root.isUser
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.edit.readOnly = !root.edit.readOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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.pointSize: Tokens.font.size.smaller
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
|
||||||
|
onLinkActivated: link => {
|
||||||
|
Qt.openUrlExternally(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomMouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
cursorShape: root.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||||
|
preventStealing: false
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
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.fill = container;
|
||||||
|
incubator.object.requestClose.connect(root.requestClose);
|
||||||
|
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.size.extraLarge * 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
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 var codeSpans: []
|
||||||
|
property int highlightToken: 0
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
const token = ++root.highlightToken;
|
||||||
|
CodeHighlighter.highlight(root.code, root.language, root, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHighlightSpans(token, spans) {
|
||||||
|
if (token !== root.highlightToken)
|
||||||
|
return;
|
||||||
|
codeSpans = spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
codeSpans = [];
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
onCodeChanged: 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.pointSize: Tokens.font.size.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
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: code
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
x: implicitWidth * (0 - codeFlick.visibleArea.xPosition) + Tokens.padding.small
|
||||||
|
anchors.margins: Tokens.padding.small
|
||||||
|
text: root.highlightedHtml(root.code, root.codeSpans)
|
||||||
|
textFormat: Text.RichText
|
||||||
|
color: CodeColors.active.normal
|
||||||
|
layer.enabled: true
|
||||||
|
clip: false
|
||||||
|
font.family: Config.appearance.font.family.mono
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
leftInset: Tokens.padding.small
|
||||||
|
clip: false
|
||||||
|
font.family: Config.appearance.font.family.mono
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
textFormat: Text.RichText
|
||||||
|
text: code.text
|
||||||
|
readOnly: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
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
@@ -0,0 +1,167 @@
|
|||||||
|
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 LlmSegment segment
|
||||||
|
required property bool hovered
|
||||||
|
required property Repeater repeater
|
||||||
|
required property int index
|
||||||
|
required property ChatMessage message
|
||||||
|
required property ChatGeneration current
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
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 : blocks.implicitHeight + blocks.anchors.topMargin * 2
|
||||||
|
anchors.right: root.isUser ? parent.right : undefined
|
||||||
|
layer.enabled: false
|
||||||
|
layer.smooth: 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.segment.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.pointSize: Tokens.font.size.smaller
|
||||||
|
readOnly: true
|
||||||
|
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
|
||||||
|
text: root.isUser ? root.segment.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assistant content: parsed markdown blocks.
|
||||||
|
MarkdownBlocks {
|
||||||
|
id: blocks
|
||||||
|
|
||||||
|
visible: !root.isUser
|
||||||
|
anchors.topMargin: Tokens.padding.medium
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
blocks: root.segment.markdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Actions {
|
||||||
|
id: actionsRow
|
||||||
|
|
||||||
|
readonly property bool shouldBeActive: (root.segment.type === LlmSegment.Type.Content) && !root.segment.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
|
||||||
|
segment: root.segment
|
||||||
|
current: root.current
|
||||||
|
message: root.message
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
|
font.pointSize: 36
|
||||||
|
text: "chat"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: Colors.tPalette.m3onSurfaceVariant
|
||||||
|
text: qsTr("Send a message to get started")
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
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: LlmMarkdown.Type.Code
|
||||||
|
|
||||||
|
delegate: CodeBlockView {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
language: modelData.language
|
||||||
|
code: modelData.code
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Tokens.spacing.small
|
||||||
|
anchors.left: parent.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: LlmMarkdown.Type.Math
|
||||||
|
|
||||||
|
delegate: MathBlockView {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
latex: modelData.latex
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Tokens.padding.medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: LlmMarkdown.Type.Heading
|
||||||
|
|
||||||
|
delegate: TextEditBase {
|
||||||
|
required property var 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.bold: true
|
||||||
|
font.pointSize: {
|
||||||
|
if (modelData.level <= 1)
|
||||||
|
return Tokens.font.size.large;
|
||||||
|
if (modelData.level === 2)
|
||||||
|
return Tokens.font.size.normal;
|
||||||
|
return Tokens.font.size.smaller;
|
||||||
|
}
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: LlmMarkdown.Type.Text
|
||||||
|
|
||||||
|
delegate: BubbleEdit {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Tokens.padding.medium
|
||||||
|
text: modelData.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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 real fontSize: Tokens.font.size.large
|
||||||
|
|
||||||
|
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
|
||||||
|
|
||||||
|
LlmMathText {
|
||||||
|
id: math
|
||||||
|
|
||||||
|
latex: root.latex
|
||||||
|
color: root.mathColor
|
||||||
|
fontPointSize: root.fontSize
|
||||||
|
devicePixelRatio: Screen.devicePixelRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: equationImage
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
source: math.imageUrl
|
||||||
|
asynchronous: true
|
||||||
|
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.family: Config.appearance.font.family.mono
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
text: root.latex
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
visible: !math.ok
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Llm
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ChatGeneration current: modelData.activeGeneration
|
||||||
|
required property int index
|
||||||
|
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
||||||
|
required property Item rootParent
|
||||||
|
required property ChatMessage modelData
|
||||||
|
property bool reasoningExpanded: false
|
||||||
|
readonly property var blocks: blockify(current.segments)
|
||||||
|
property real savedOffset: -1
|
||||||
|
property bool stickActive: false
|
||||||
|
|
||||||
|
function handleReasoningToggle(expanded: bool): void {
|
||||||
|
const view = root.ListView.view;
|
||||||
|
if (!view)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
root.savedOffset = view.contentY - root.y;
|
||||||
|
root.reasoningExpanded = true;
|
||||||
|
} else {
|
||||||
|
root.reasoningExpanded = false;
|
||||||
|
root.stickActive = false;
|
||||||
|
settleTimer.stop();
|
||||||
|
if (!isNaN(root.savedOffset)) {
|
||||||
|
restoreAnim.to = root.y + root.savedOffset;
|
||||||
|
restoreAnim.restart();
|
||||||
|
root.savedOffset = NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockify(segs) {
|
||||||
|
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,
|
||||||
|
segments: [seg]
|
||||||
|
});
|
||||||
|
} 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]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: ListView.view.width
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: false
|
||||||
|
implicitHeight: layout.implicitHeight + Tokens.padding.medium
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: settleTimer
|
||||||
|
interval: 16
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.stickActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onImplicitHeightChanged: {
|
||||||
|
if (root.reasoningExpanded) {
|
||||||
|
root.stickActive = true;
|
||||||
|
settleTimer.restart();
|
||||||
|
|
||||||
|
const view = root.ListView.view;
|
||||||
|
if (!view)
|
||||||
|
return;
|
||||||
|
const bottomEdge = root.y + root.height + Tokens.padding.large * 2;
|
||||||
|
const viewportBottom = view.contentY + view.height;
|
||||||
|
if (bottomEdge > viewportBottom)
|
||||||
|
view.contentY = bottomEdge - view.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
id: restoreAnim
|
||||||
|
|
||||||
|
property: "contentY"
|
||||||
|
target: root.ListView.view
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
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: "process"
|
||||||
|
|
||||||
|
delegate: ProcessBlock {
|
||||||
|
width: root.width
|
||||||
|
blocks: root.blocks
|
||||||
|
current: root.current
|
||||||
|
rootParent: root.rootParent
|
||||||
|
|
||||||
|
onExpandedChanged: root.handleReasoningToggle(expanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "content"
|
||||||
|
|
||||||
|
delegate: ContentBubble {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
isUser: root.isUser
|
||||||
|
segment: modelData.segments[0]
|
||||||
|
width: root.width
|
||||||
|
repeater: segmentRep
|
||||||
|
message: root.modelData
|
||||||
|
current: root.current
|
||||||
|
hovered: root.containsMouse
|
||||||
|
|
||||||
|
onEdit: text => {
|
||||||
|
root.modelData.edit(text);
|
||||||
|
|
||||||
|
if (root.isUser)
|
||||||
|
root.modelData.generate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
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 LlmSegment 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.filter(s => s.type === LlmSegment.Type.ToolCall).some(s => current.pendingToolCalls.includes(s))
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.pointSize: Tokens.font.size.large
|
||||||
|
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.pointSize: Tokens.font.size.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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: approvalPrompt
|
||||||
|
anchors.top: layout.bottom
|
||||||
|
anchors.topMargin: Tokens.spacing.small
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2
|
||||||
|
visible: root.pendingApproval
|
||||||
|
|
||||||
|
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.pointSize: Tokens.font.size.large
|
||||||
|
color: Colors.palette.m3onSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: toolsBg
|
||||||
|
|
||||||
|
property bool open: false
|
||||||
|
property Item rootParent: root.rootParent
|
||||||
|
property int openHeight: toolsBg.rootParent.height - Tokens.padding.medium * 2
|
||||||
|
property int openWidth: toolsBg.rootParent.width - Tokens.padding.medium * 2
|
||||||
|
|
||||||
|
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
function reparentWrapper(): void {
|
||||||
|
const newParent = open ? toolsBg.rootParent : toolsBg;
|
||||||
|
const pos = toolsWrapper.mapToItem(newParent, 0, 0);
|
||||||
|
toolsWrapper.parent = newParent;
|
||||||
|
toolsWrapper.x = pos.x;
|
||||||
|
toolsWrapper.y = pos.y;
|
||||||
|
console.log("X, Y:\n" + toolsWrapper.x, toolsWrapper.y, "\nWidth, Height:\n" + toolsWrapper.width, toolsWrapper.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ? toolsBg.rootParent : toolsBg
|
||||||
|
|
||||||
|
onClicked: toolsBg.open = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: toolsWrapper
|
||||||
|
|
||||||
|
width: toolsBg.width
|
||||||
|
height: toolBox.implicitHeight
|
||||||
|
|
||||||
|
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: (toolsBg.rootParent.width - toolsBg.openWidth) / 2
|
||||||
|
toolsWrapper.y: (toolsBg.rootParent.height - toolsBg.openHeight) / 2
|
||||||
|
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: {
|
||||||
|
console.log("X, Y:\n" + toolsWrapper.x, toolsWrapper.y, "\nWidth, Height:\n" + toolsWrapper.width, toolsWrapper.height);
|
||||||
|
toolsBg.open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: toolList
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: Tokens.padding.medium
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.current.pendingToolCalls ?? []
|
||||||
|
delegate: RowLayout {
|
||||||
|
id: toolText
|
||||||
|
|
||||||
|
required property LlmSegment modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
function toolTarget(seg: LlmSegment): 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
color: Colors.palette.m3onPrimaryContainer
|
||||||
|
text: String(toolText.index + 1)
|
||||||
|
font.pointSize: Tokens.font.size.large
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonRow {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
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 LlmSegment 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: LlmSegment): 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: LlmSegment): string {
|
||||||
|
if (seg.name === "webfetch") {
|
||||||
|
switch (seg.status) {
|
||||||
|
case LlmSegment.Status.Running:
|
||||||
|
return qsTr("Fetching");
|
||||||
|
case LlmSegment.Status.Pending:
|
||||||
|
return qsTr("Pending fetch");
|
||||||
|
case LlmSegment.Status.Success:
|
||||||
|
return qsTr("Fetched");
|
||||||
|
case LlmSegment.Status.Error:
|
||||||
|
return qsTr("Failed to fetch");
|
||||||
|
default:
|
||||||
|
return qsTr("Fetched website");
|
||||||
|
}
|
||||||
|
} else if (seg.name === "readfile") {
|
||||||
|
switch (seg.status) {
|
||||||
|
case LlmSegment.Status.Running:
|
||||||
|
return qsTr("Reading");
|
||||||
|
case LlmSegment.Status.Pending:
|
||||||
|
return qsTr("Pending read");
|
||||||
|
case LlmSegment.Status.Success:
|
||||||
|
return qsTr("Read");
|
||||||
|
case LlmSegment.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.pointSize: Tokens.font.size.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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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);
|
||||||
|
Visibilities.getForActive().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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
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.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
|
||||||
|
|
||||||
|
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: ScriptModel {
|
||||||
|
values: Chat.chats.values
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadChatRequest: (chat, index) => {
|
||||||
|
ChatState.currentIdx = index;
|
||||||
|
ChatState.chatSession = chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteChatRequest: chat => {
|
||||||
|
if (chat === ChatState.chatSession) {
|
||||||
|
ChatState.chatSession = null;
|
||||||
|
ChatState.currentIdx = -1;
|
||||||
|
}
|
||||||
|
Chat.chats.remove(chat);
|
||||||
|
}
|
||||||
|
onNewChatRequest: {
|
||||||
|
const data = Chat.chats.insert();
|
||||||
|
ChatState.currentIdx = 0;
|
||||||
|
ChatState.chatSession = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.margins: Tokens.padding.medium
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2 - anchors.margins * 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.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
implicitWidth: Math.max(Math.min(parent.width, 800), Config.sidebar.sizes.width)
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
onLoadingChanged: if (!loading)
|
||||||
|
root.chatOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: noChatLayout
|
||||||
|
anchors.centerIn: parent
|
||||||
|
opacity: ChatState.chatSession ? 0 : 1
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
text: "chat_bubble_off"
|
||||||
|
font.pointSize: Tokens.font.size.extraLarge
|
||||||
|
color: Colors.tPalette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
text: qsTr("Start or create a chat")
|
||||||
|
color: Colors.tPalette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,7 +1,10 @@
|
|||||||
import qs.Components
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Modules.Notifications.Sidebar.Chat
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -10,21 +13,98 @@ Item {
|
|||||||
required property Props props
|
required property Props props
|
||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onIsWindowChanged(): void {
|
||||||
|
if (ChatState.isWindow)
|
||||||
|
root.props.currentTab = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ChatState
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
CustomRect {
|
Tabs {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: ChatState.isWindow || !Config.llm.enabled ? 0 : implicitHeight
|
||||||
|
dashState: root.props
|
||||||
|
nonAnimWidth: layout.width
|
||||||
|
visible: height > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: Colors.tPalette.m3surfaceContainerLow
|
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
NotifDock {
|
Item {
|
||||||
props: root.props
|
id: pages
|
||||||
visibilities: root.visibilities
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.top: parent.top
|
||||||
|
implicitWidth: parent.width
|
||||||
|
opacity: root.props.currentTab === 0 ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
x: root.props.currentTab === 0 ? 0 : -root.width
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
Behavior on x {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Colors.tPalette.m3surfaceContainerLow
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
|
NotifDock {
|
||||||
|
props: root.props
|
||||||
|
visibilities: root.visibilities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.props.currentTab === 1 ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
x: root.props.currentTab === 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.small
|
||||||
|
|
||||||
|
ChatPanel {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
|
import ZShell.Llm
|
||||||
|
|
||||||
PersistentProperties {
|
PersistentProperties {
|
||||||
|
property int currentTab: 0
|
||||||
property list<string> expandedNotifs: []
|
property list<string> expandedNotifs: []
|
||||||
|
|
||||||
reloadableId: "sidebar"
|
reloadableId: "sidebar"
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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 PersistentProperties dashState
|
||||||
|
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.dashState.currentTab
|
||||||
|
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.dashState.currentTab = 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
|
||||||
|
font.pointSize: 18
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,7 +110,10 @@ ColumnLayout {
|
|||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.visibilities.sidebar = false;
|
root.visibilities.sidebar = false;
|
||||||
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
|
if (Config.launcher.uwsm)
|
||||||
|
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
|
||||||
|
else
|
||||||
|
Quickshell.execDetached([...Config.general.apps.playback, recording.modelData.path]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +123,10 @@ ColumnLayout {
|
|||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.visibilities.sidebar = false;
|
root.visibilities.sidebar = false;
|
||||||
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
|
if (Config.launcher.uwsm)
|
||||||
|
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
|
||||||
|
else
|
||||||
|
Quickshell.execDetached([...Config.general.apps.explorer, recording.modelData.path]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +141,7 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
model: FileSystemModel {
|
model: FileSystemModel {
|
||||||
nameFilters: ["recording_*.mp4"]
|
nameFilters: ["recording_*.mp4"]
|
||||||
@@ -162,8 +167,7 @@ ColumnLayout {
|
|||||||
opacity: list.count === 0 ? 1 : 0
|
opacity: list.count === 0 ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sourceComponent: ColumnLayout {
|
sourceComponent: ColumnLayout {
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
@@ -178,16 +182,13 @@ ColumnLayout {
|
|||||||
text: "scan_delete"
|
text: "scan_delete"
|
||||||
|
|
||||||
Behavior on Layout.preferredHeight {
|
Behavior on Layout.preferredHeight {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,16 +204,13 @@ ColumnLayout {
|
|||||||
text: "scan_delete"
|
text: "scan_delete"
|
||||||
|
|
||||||
Behavior on Layout.preferredWidth {
|
Behavior on Layout.preferredWidth {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ import qs.Services
|
|||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Modules.Settings
|
import qs.Modules.Settings
|
||||||
import qs.Modules.Bar.Popouts as BarPopouts
|
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property bool needExtraRow: quickToggles.length > 6
|
readonly property bool needExtraRow: quickToggles.length > 6
|
||||||
required property BarPopouts.Wrapper popouts
|
|
||||||
readonly property var quickToggles: {
|
readonly property var quickToggles: {
|
||||||
const seenIds = new Set();
|
const seenIds = new Set();
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Modules.Bar.Popouts as BarPopouts
|
|
||||||
import qs.Modules.Notifications.Sidebar.Utils.Cards
|
import qs.Modules.Notifications.Sidebar.Utils.Cards
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property BarPopouts.Wrapper popouts
|
|
||||||
required property PersistentProperties props
|
required property PersistentProperties props
|
||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
@@ -21,8 +19,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
IdleInhibit {
|
IdleInhibit {}
|
||||||
}
|
|
||||||
|
|
||||||
Record {
|
Record {
|
||||||
props: root.props
|
props: root.props
|
||||||
@@ -32,7 +29,6 @@ Item {
|
|||||||
|
|
||||||
Toggles {
|
Toggles {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
popouts: root.popouts
|
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Modules.Bar.Popouts as BarPopouts
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real offsetScale: shouldBeActive ? 0 : 1
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
required property BarPopouts.Wrapper popouts
|
|
||||||
readonly property PersistentProperties props: PersistentProperties {
|
readonly property PersistentProperties props: PersistentProperties {
|
||||||
property string recordingConfirmDelete
|
property string recordingConfirmDelete
|
||||||
property bool recordingListExpanded: false
|
property bool recordingListExpanded: false
|
||||||
@@ -18,12 +16,12 @@ Item {
|
|||||||
|
|
||||||
reloadableId: "utilities"
|
reloadableId: "utilities"
|
||||||
}
|
}
|
||||||
readonly property bool shouldBeActive: visibilities.sidebar
|
readonly property bool shouldBeActive: visibilities.sidebar && !sidebar.chatActive
|
||||||
required property Item sidebar
|
required property Item sidebar
|
||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||||
implicitHeight: content.implicitHeight + 8 * 2
|
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
|
||||||
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
|
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
|
||||||
opacity: 1 - offsetScale
|
opacity: 1 - offsetScale
|
||||||
visible: offsetScale < 1
|
visible: offsetScale < 1
|
||||||
@@ -45,7 +43,6 @@ Item {
|
|||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
implicitWidth: root.implicitWidth - 8 * 2
|
implicitWidth: root.implicitWidth - 8 * 2
|
||||||
popouts: root.popouts
|
|
||||||
props: root.props
|
props: root.props
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Components.Toast
|
||||||
|
import ZShell
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import ZShell.Llm
|
||||||
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -12,6 +16,7 @@ Item {
|
|||||||
readonly property Props props: Props {
|
readonly property Props props: Props {
|
||||||
}
|
}
|
||||||
readonly property bool shouldBeActive: root.visibilities.sidebar && Config.sidebar.enabled
|
readonly property bool shouldBeActive: root.visibilities.sidebar && Config.sidebar.enabled
|
||||||
|
readonly property bool chatActive: props.currentTab === 1
|
||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
|
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
|
||||||
@@ -26,6 +31,17 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Chat
|
||||||
|
|
||||||
|
function onErrorOccurred(message: string): void {
|
||||||
|
if (root.shouldBeActive && root.chatActive)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Toaster.toast(qsTr("Chat"), message, "error_outline", Toast.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,19 @@ Item {
|
|||||||
required property Component content
|
required property Component content
|
||||||
required property string header
|
required property string header
|
||||||
property int horizontalContentMargin
|
property int horizontalContentMargin
|
||||||
required property string icon
|
property string icon
|
||||||
required property string label
|
required property string label
|
||||||
|
property alias iconLabel: openButton.iconLabel
|
||||||
|
property alias rowButton: openButton
|
||||||
|
property string subtext: ""
|
||||||
property bool open
|
property bool open
|
||||||
property real openHeight: Math.min(rootParent.height * 0.8, 600)
|
property real openHeight: Math.min(rootParent.height * 0.8, 600)
|
||||||
property real openWidth: Math.min(rootParent.width * 0.8, 400)
|
property real openWidth: Math.min(rootParent.width * 0.8, 400)
|
||||||
required property Item rootParent
|
required property Item rootParent
|
||||||
property bool separateContent
|
property bool separateContent
|
||||||
|
required property string settingAnchor
|
||||||
|
property bool first: false
|
||||||
|
property bool last: false
|
||||||
|
|
||||||
signal accepted
|
signal accepted
|
||||||
signal cancelled
|
signal cancelled
|
||||||
@@ -44,8 +50,7 @@ Item {
|
|||||||
color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer
|
color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: false
|
enabled: false
|
||||||
hoverEnabled: enabled
|
hoverEnabled: enabled
|
||||||
|
preventStealing: true
|
||||||
parent: root.open ? root.rootParent : root
|
parent: root.open ? root.rootParent : root
|
||||||
|
|
||||||
onClicked: root.open = false
|
onClicked: root.open = false
|
||||||
@@ -75,7 +81,8 @@ Item {
|
|||||||
backdrop.enabled: true
|
backdrop.enabled: true
|
||||||
dialogBg.bottomLeftRadius: Tokens.rounding.largeIncreased
|
dialogBg.bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||||
dialogBg.bottomRightRadius: Tokens.rounding.largeIncreased
|
dialogBg.bottomRightRadius: Tokens.rounding.largeIncreased
|
||||||
dialogBg.radius: Tokens.rounding.largeIncreased
|
dialogBg.topRightRadius: Tokens.rounding.largeIncreased
|
||||||
|
dialogBg.topLeftRadius: Tokens.rounding.largeIncreased
|
||||||
dialogContent.opacity: 1
|
dialogContent.opacity: 1
|
||||||
dialogWrapper.height: root.openHeight
|
dialogWrapper.height: root.openHeight
|
||||||
dialogWrapper.width: root.openWidth
|
dialogWrapper.width: root.openWidth
|
||||||
@@ -103,7 +110,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
properties: "opacity,radius,bottomLeftRadius,bottomRightRadius"
|
properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
|
||||||
type: Anim.DefaultEffects
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,12 +138,13 @@ Item {
|
|||||||
id: dialogBg
|
id: dialogBg
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
bottomLeftRadius: Tokens.rounding.largeIncreased
|
bottomLeftRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
bottomRightRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
deformScale: 0
|
deformScale: 0
|
||||||
group: blobGroup
|
group: blobGroup
|
||||||
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
|
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
|
||||||
radius: Tokens.rounding.extraSmall
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowButton {
|
RowButton {
|
||||||
@@ -145,9 +153,11 @@ Item {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
height: Math.min(implicitHeight, parent.height) // Clamp to parent height due to overshoot anim
|
height: Math.min(implicitHeight, parent.height)
|
||||||
icon: root.icon
|
icon: root.icon
|
||||||
last: true
|
subtext: root.subtext
|
||||||
|
last: root.last
|
||||||
|
first: root.first
|
||||||
text: root.label
|
text: root.label
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
transform: Matrix4x4 {
|
||||||
@@ -167,6 +177,7 @@ Item {
|
|||||||
|
|
||||||
sourceComponent: MouseArea {
|
sourceComponent: MouseArea {
|
||||||
onWheel: event => event.accepted = true
|
onWheel: event => event.accepted = true
|
||||||
|
preventStealing: true
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.bottomMargin: Tokens.padding.largeIncreased
|
anchors.bottomMargin: Tokens.padding.largeIncreased
|
||||||
@@ -219,6 +230,7 @@ Item {
|
|||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
type: TextButton.Text
|
type: TextButton.Text
|
||||||
verticalPadding: Tokens.padding.small
|
verticalPadding: Tokens.padding.small
|
||||||
|
stateLayer.preventStealing: true
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.cancelled();
|
root.cancelled();
|
||||||
@@ -232,6 +244,7 @@ Item {
|
|||||||
isRound: true
|
isRound: true
|
||||||
text: root.acceptLabel
|
text: root.acceptLabel
|
||||||
type: TextButton.Text
|
type: TextButton.Text
|
||||||
|
stateLayer.preventStealing: true
|
||||||
verticalPadding: Tokens.padding.small
|
verticalPadding: Tokens.padding.small
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ DialogRowButton {
|
|||||||
|
|
||||||
required property var model
|
required property var model
|
||||||
property var selectedItem
|
property var selectedItem
|
||||||
|
property var initialSelect
|
||||||
|
|
||||||
function keyFor(item: var): string {
|
function keyFor(item: var): string {
|
||||||
return item.id;
|
return item.id;
|
||||||
@@ -91,7 +92,14 @@ DialogRowButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onOpenChanged: {
|
onOpenChanged: {
|
||||||
if (open)
|
if (open) {
|
||||||
selectedItem = null;
|
root.rootParent.interactive = false;
|
||||||
|
if (!initialSelect)
|
||||||
|
selectedItem = null;
|
||||||
|
else
|
||||||
|
selectedItem = initialSelect;
|
||||||
|
} else {
|
||||||
|
root.rootParent.interactive = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ColumnLayout {
|
|||||||
function findAnchor(item: Item, anchor: string): Item {
|
function findAnchor(item: Item, anchor: string): Item {
|
||||||
if (!item)
|
if (!item)
|
||||||
return null;
|
return null;
|
||||||
if (item.settingAnchor !== undefined && item.settingAnchor === anchor)
|
if (item.settingAnchor !== undefined && item.settingAnchor === anchor) // qmllint disable missing-property
|
||||||
return item;
|
return item;
|
||||||
const kids = item.children;
|
const kids = item.children;
|
||||||
for (let i = 0; i < kids.length; i++) {
|
for (let i = 0; i < kids.length; i++) {
|
||||||
@@ -43,8 +43,8 @@ ColumnLayout {
|
|||||||
|
|
||||||
function highlightAnchor(anchor: string): void {
|
function highlightAnchor(anchor: string): void {
|
||||||
const row = findAnchor(contentChild, anchor);
|
const row = findAnchor(contentChild, anchor);
|
||||||
if (row && row.flashHighlight !== undefined)
|
if (row && row.flashHighlight !== undefined) // qmllint disable missing-property
|
||||||
row.flashHighlight();
|
row.flashHighlight(); // qmllint disable missing-property
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToAnchor(anchor: string): bool {
|
function scrollToAnchor(anchor: string): bool {
|
||||||
@@ -61,8 +61,8 @@ ColumnLayout {
|
|||||||
root.animateScroll = true;
|
root.animateScroll = true;
|
||||||
flickable.contentY = target;
|
flickable.contentY = target;
|
||||||
Qt.callLater(() => root.animateScroll = false);
|
Qt.callLater(() => root.animateScroll = false);
|
||||||
if (row.flashHighlight !== undefined)
|
if (row.flashHighlight !== undefined) // qmllint disable missing-property
|
||||||
row.flashHighlight();
|
row.flashHighlight(); // qmllint disable missing-property
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,11 +154,20 @@ ColumnLayout {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: -topMargin
|
Layout.topMargin: -topMargin
|
||||||
|
|
||||||
bottomMargin: Tokens.padding.extraLarge
|
bottomMargin: Tokens.padding.extraLarge
|
||||||
|
topMargin: Tokens.padding.large
|
||||||
|
fadeAmount: 0.1
|
||||||
|
|
||||||
contentHeight: root.contentChild?.implicitHeight ?? 0
|
contentHeight: root.contentChild?.implicitHeight ?? 0
|
||||||
contentItem.children: [root.contentChild]
|
contentItem.children: [root.contentChild]
|
||||||
fadeAmount: 0.1
|
|
||||||
topMargin: Tokens.padding.large
|
rebound: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "x,y"
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on contentY {
|
Behavior on contentY {
|
||||||
enabled: root.animateScroll
|
enabled: root.animateScroll
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ ConnectedRect {
|
|||||||
property alias subtext: subLabel.text
|
property alias subtext: subLabel.text
|
||||||
property alias text: label.text
|
property alias text: label.text
|
||||||
property string trailingIcon
|
property string trailingIcon
|
||||||
|
property string activeItem
|
||||||
|
|
||||||
signal clicked(event: MouseEvent)
|
signal clicked(event: MouseEvent)
|
||||||
|
|
||||||
@@ -40,15 +41,16 @@ ConnectedRect {
|
|||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: iconLabel
|
id: iconLabel
|
||||||
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
|
visible: root.icon !== ""
|
||||||
fill: 1
|
fill: 1
|
||||||
|
text: root.icon
|
||||||
font.pointSize: Tokens.font.size.normal
|
font.pointSize: Tokens.font.size.normal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +81,26 @@ ConnectedRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.activeItem && root.subtext
|
||||||
|
asynchronous: true
|
||||||
|
visible: active
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
color: Colors.palette.m3secondaryContainer
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
implicitWidth: text.implicitWidth + Tokens.padding.medium * 2
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: text
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: Colors.palette.m3onSecondaryContainer
|
||||||
|
text: root.activeItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: root.trailingIcon
|
active: root.trailingIcon
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
DialogRowButton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var model
|
||||||
|
property var selectedItem
|
||||||
|
property var initialSelect
|
||||||
|
|
||||||
|
acceptAllowed: !!selectedItem
|
||||||
|
horizontalContentMargin: -Tokens.padding.extraSmall
|
||||||
|
separateContent: true
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
ColumnLayout {
|
||||||
|
VerticalFadeListView {
|
||||||
|
bottomMargin: Tokens.padding.large
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: ScriptModel {
|
||||||
|
values: {
|
||||||
|
const s = searchField.text;
|
||||||
|
var regex = new RegExp(s, "i");
|
||||||
|
|
||||||
|
return root.model.filter(n => regex.test(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spacing: 0
|
||||||
|
topMargin: Tokens.padding.large
|
||||||
|
|
||||||
|
delegate: CustomRect {
|
||||||
|
id: item
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
readonly property bool selected: root.selectedItem === modelData
|
||||||
|
|
||||||
|
anchors.left: ListView.view.contentItem.left
|
||||||
|
anchors.margins: 1 // Gets cut off for some reason without this
|
||||||
|
anchors.right: ListView.view.contentItem.right
|
||||||
|
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
|
||||||
|
implicitHeight: label.implicitHeight + Tokens.padding.small * 2
|
||||||
|
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
|
||||||
|
|
||||||
|
Behavior on radius {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
id: stateLayer
|
||||||
|
|
||||||
|
onClicked: root.selectedItem = item.modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: label
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Tokens.padding.large
|
||||||
|
anchors.right: item.selected ? checkIcon.left : parent.right
|
||||||
|
anchors.rightMargin: item.selected ? Tokens.spacing.medium : anchors.margins
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: item.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Tokens.font.size.smaller
|
||||||
|
text: item.modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: checkIcon
|
||||||
|
|
||||||
|
anchors.margins: Tokens.padding.large
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: Colors.palette.m3onTertiaryContainer
|
||||||
|
font.pointSize: Tokens.font.size.normal
|
||||||
|
opacity: item.selected ? 1 : 0
|
||||||
|
text: "check"
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchBar {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
bg.border.color: Colors.palette.m3outlineVariant
|
||||||
|
bg.color: Colors.tPalette.m3surfaceContainerLowest
|
||||||
|
clearIcon.font.pointSize: Tokens.font.size.large
|
||||||
|
clearIcon.padding: Tokens.padding.extraSmall
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
placeholderText: qsTr("Search...")
|
||||||
|
searchIcon.anchors.leftMargin: Tokens.padding.large
|
||||||
|
searchIcon.font.pointSize: Tokens.font.size.large
|
||||||
|
|
||||||
|
Behavior on bg.border.color {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenChanged: {
|
||||||
|
if (open) {
|
||||||
|
root.rootParent.interactive = false;
|
||||||
|
if (!initialSelect)
|
||||||
|
selectedItem = null;
|
||||||
|
else
|
||||||
|
selectedItem = initialSelect;
|
||||||
|
} else {
|
||||||
|
root.rootParent.interactive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Blobs
|
||||||
|
import qs.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool acceptAllowed: true
|
||||||
|
required property string acceptLabel
|
||||||
|
required property Component content
|
||||||
|
required property string header
|
||||||
|
property int horizontalContentMargin
|
||||||
|
property string icon
|
||||||
|
required property string label
|
||||||
|
property alias iconLabel: openButton.iconText
|
||||||
|
property alias rowButton: openButton
|
||||||
|
property alias contentItem: dialogContent.item
|
||||||
|
property string subtext: ""
|
||||||
|
property bool open
|
||||||
|
property real openHeight: Math.min(rootParent.height * 0.8, 400)
|
||||||
|
property real openWidth: Math.min(rootParent.width * 0.8, 600)
|
||||||
|
required property Item rootParent
|
||||||
|
property bool first: false
|
||||||
|
property bool last: false
|
||||||
|
required property string settingAnchor
|
||||||
|
property color openColor: Colors.palette.m3surfaceContainerHighest
|
||||||
|
|
||||||
|
signal accepted
|
||||||
|
signal cancelled
|
||||||
|
|
||||||
|
function reparentWrapper(): void {
|
||||||
|
const newParent = open ? rootParent : root;
|
||||||
|
const pos = dialogWrapper.mapToItem(newParent, 0, 0);
|
||||||
|
dialogWrapper.parent = newParent;
|
||||||
|
dialogWrapper.x = pos.x;
|
||||||
|
dialogWrapper.y = pos.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: openButton.implicitHeight
|
||||||
|
z: open || dialogTransition.running ? 2 : 0
|
||||||
|
|
||||||
|
BlobGroup {
|
||||||
|
id: blobGroup
|
||||||
|
|
||||||
|
color: root.open ? root.openColor : 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: dialogWrapper
|
||||||
|
|
||||||
|
height: openButton.implicitHeight
|
||||||
|
width: root.width
|
||||||
|
z: 1
|
||||||
|
|
||||||
|
states: State {
|
||||||
|
name: "open"
|
||||||
|
when: root.open
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
backdrop.enabled: true
|
||||||
|
dialogBg.bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||||
|
dialogBg.bottomRightRadius: Tokens.rounding.largeIncreased
|
||||||
|
dialogBg.topRightRadius: Tokens.rounding.largeIncreased
|
||||||
|
dialogBg.topLeftRadius: Tokens.rounding.largeIncreased
|
||||||
|
dialogContent.opacity: 1
|
||||||
|
dialogWrapper.height: root.openHeight
|
||||||
|
dialogWrapper.width: root.openWidth
|
||||||
|
dialogWrapper.x: (root.rootParent.width - root.openWidth) / 2
|
||||||
|
dialogWrapper.y: (root.rootParent.height - root.openHeight) / 2
|
||||||
|
elevation.opacity: 1
|
||||||
|
openButton.opacity: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transitions: Transition {
|
||||||
|
id: dialogTransition
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
ScriptAction {
|
||||||
|
script: root.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: dialogBg.bottomLeftRadius
|
||||||
|
bottomRightRadius: dialogBg.bottomRightRadius
|
||||||
|
level: 4
|
||||||
|
opacity: 0
|
||||||
|
radius: dialogBg.radius
|
||||||
|
|
||||||
|
transform: Matrix4x4 {
|
||||||
|
matrix: dialogBg.deformMatrix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobRect {
|
||||||
|
id: dialogBg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
bottomLeftRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
bottomRightRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
deformScale: 0
|
||||||
|
group: blobGroup
|
||||||
|
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeDialogToggle {
|
||||||
|
id: openButton
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
color: "transparent"
|
||||||
|
height: Math.min(implicitHeight, parent.height)
|
||||||
|
subtext: root.subtext
|
||||||
|
last: root.last
|
||||||
|
first: root.first
|
||||||
|
text: root.label
|
||||||
|
|
||||||
|
transform: Matrix4x4 {
|
||||||
|
matrix: dialogBg.deformMatrix
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: root.open = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: dialogContent
|
||||||
|
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.fill: parent
|
||||||
|
asynchronous: false
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
sourceComponent: MouseArea {
|
||||||
|
onWheel: event => event.accepted = true
|
||||||
|
preventStealing: true
|
||||||
|
clip: true
|
||||||
|
implicitWidth: innerLayout.implicitWidth + innerLayout.anchors.margins * 2
|
||||||
|
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins + innerLayout.anchors.bottomMargin
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: innerLayout
|
||||||
|
anchors.bottomMargin: Tokens.padding.largeIncreased
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Tokens.padding.extraLarge
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
font.pointSize: Tokens.font.size.large
|
||||||
|
text: root.header
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.horizontalContentMargin
|
||||||
|
Layout.topMargin: Tokens.spacing.medium
|
||||||
|
Layout.bottomMargin: Tokens.spacing.medium
|
||||||
|
Layout.rightMargin: root.horizontalContentMargin
|
||||||
|
sourceComponent: root.content
|
||||||
|
}
|
||||||
|
|
||||||
|
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.cancelled();
|
||||||
|
root.open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
enabled: root.acceptAllowed
|
||||||
|
horizontalPadding: Tokens.padding.largeIncreased
|
||||||
|
isRound: true
|
||||||
|
text: root.acceptLabel
|
||||||
|
type: TextButton.Text
|
||||||
|
stateLayer.preventStealing: true
|
||||||
|
verticalPadding: Tokens.padding.small
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.accepted();
|
||||||
|
root.open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transform: Matrix4x4 {
|
||||||
|
matrix: dialogBg.deformMatrix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
TimeDialogRow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property int start
|
||||||
|
required property int end
|
||||||
|
property int endCurrent
|
||||||
|
property int startCurrent
|
||||||
|
openColor: Colors.palette.m3surfaceContainer
|
||||||
|
openHeight: Math.min(rootParent.height * 0.8, contentItem?.implicitHeight ?? 0)
|
||||||
|
openWidth: Math.min(rootParent.width * 0.8, contentItem?.implicitWidth ?? 0)
|
||||||
|
|
||||||
|
function keyFor(item: var): string {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelFor(item: var): string {
|
||||||
|
return item.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontalContentMargin: -Tokens.padding.extraSmall
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
TimeInput {
|
||||||
|
id: timeContent
|
||||||
|
start: root.start
|
||||||
|
end: root.end
|
||||||
|
|
||||||
|
onEndEdit: value => root.endCurrent = value
|
||||||
|
onStartEdit: value => root.startCurrent = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenChanged: {
|
||||||
|
if (open) {
|
||||||
|
root.rootParent.interactive = false;
|
||||||
|
root.z = 5;
|
||||||
|
} else {
|
||||||
|
root.rootParent.interactive = true;
|
||||||
|
root.z = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-5
@@ -10,12 +10,14 @@ ConnectedRect {
|
|||||||
|
|
||||||
property alias checked: switchButton.checked
|
property alias checked: switchButton.checked
|
||||||
property int horizontalPadding: Tokens.padding.largeIncreased
|
property int horizontalPadding: Tokens.padding.largeIncreased
|
||||||
required property Component popup
|
|
||||||
property alias subtext: subtext.text
|
property alias subtext: subtext.text
|
||||||
property alias text: text.text
|
property alias text: text.text
|
||||||
|
property alias icon: icon
|
||||||
|
property string iconText: "open_in_new"
|
||||||
property int verticalPadding: Tokens.padding.small
|
property int verticalPadding: Tokens.padding.small
|
||||||
|
|
||||||
signal clicked(checked: bool)
|
signal clicked(event: MouseEvent)
|
||||||
|
signal toggled(checked: bool)
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: layout.implicitHeight + verticalPadding * 2
|
implicitHeight: layout.implicitHeight + verticalPadding * 2
|
||||||
@@ -50,11 +52,11 @@ ConnectedRect {
|
|||||||
anchors.right: switchButton.left
|
anchors.right: switchButton.left
|
||||||
anchors.rightMargin: Tokens.spacing.medium
|
anchors.rightMargin: Tokens.spacing.medium
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "open_in_new"
|
text: root.iconText
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
onClicked: PopupManager.requestOpen(root.popup)
|
onClicked: e => root.clicked(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomSwitch {
|
CustomSwitch {
|
||||||
@@ -64,6 +66,6 @@ ConnectedRect {
|
|||||||
anchors.rightMargin: root.horizontalPadding
|
anchors.rightMargin: root.horizontalPadding
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
onClicked: root.clicked(checked)
|
onClicked: root.toggled(checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,13 @@ import qs.Components
|
|||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
CustomClippingRect {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var object
|
required property int start
|
||||||
required property list<string> settings
|
required property int end
|
||||||
property bool shouldBeActive: true
|
signal endEdit(time: int)
|
||||||
|
signal startEdit(time: int)
|
||||||
signal applySettings(startTime: int, endTime: int)
|
|
||||||
signal close
|
|
||||||
|
|
||||||
function convertHour(timeValue: int): int {
|
function convertHour(timeValue: int): int {
|
||||||
return Math.floor(timeValue / 60);
|
return Math.floor(timeValue / 60);
|
||||||
@@ -28,555 +26,459 @@ CustomClippingRect {
|
|||||||
return hour * 60 + minute;
|
return hour * 60 + minute;
|
||||||
}
|
}
|
||||||
|
|
||||||
color: Colors.palette.m3surfaceContainer
|
spacing: Tokens.spacing.medium
|
||||||
implicitHeight: column.implicitHeight + column.anchors.margins * 2 + buttonRow.implicitHeight + buttonRow.anchors.topMargin
|
|
||||||
implicitWidth: column.implicitWidth + column.anchors.margins * 2
|
|
||||||
radius: Tokens.rounding.largeIncreased
|
|
||||||
|
|
||||||
ColumnLayout {
|
RowLayout {
|
||||||
id: column
|
CustomRect {
|
||||||
|
id: startHourRect
|
||||||
|
|
||||||
anchors.left: parent.left
|
Layout.preferredHeight: 72
|
||||||
anchors.margins: Tokens.padding.largeIncreased
|
Layout.preferredWidth: 96
|
||||||
anchors.right: parent.right
|
color: startHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
anchors.top: parent.top
|
implicitHeight: 72
|
||||||
spacing: Tokens.spacing.medium
|
implicitWidth: 96
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
border.color: startHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
|
border.width: startHourField.focus ? 2 : 0
|
||||||
|
radius: parent.radius - border.width
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldBase {
|
||||||
|
id: startHourField
|
||||||
|
|
||||||
|
function setConfigText(): string {
|
||||||
|
var val = root.convertHour(root.start);
|
||||||
|
if (val === 0) {
|
||||||
|
return "00";
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
clip: true
|
||||||
|
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
||||||
|
font.family: "Roboto"
|
||||||
|
font.letterSpacing: -0.25
|
||||||
|
font.pixelSize: 56
|
||||||
|
font.weight: 400
|
||||||
|
horizontalAlignment: TextInput.AlignHCenter
|
||||||
|
text: setConfigText()
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Backspace) {
|
||||||
|
event.accepted = true;
|
||||||
|
if (startHourField.text.length >= 2) {
|
||||||
|
startHourField.text = "0" + startHourField.text[0];
|
||||||
|
} else if (startHourField.text.length === 1) {
|
||||||
|
startHourField.text = "0";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
event.accepted = true;
|
||||||
|
startHourField.text = setConfigText();
|
||||||
|
startHourField.focus = false;
|
||||||
|
} else if (event.key === Qt.Key_Return) {
|
||||||
|
startHourField.focus = false;
|
||||||
|
return;
|
||||||
|
} else if (event.key === Qt.Key_Tab) {
|
||||||
|
startMinuteField.focus = true;
|
||||||
|
} else if (event.key === Qt.Key_Backtab) {
|
||||||
|
endMinuteField.focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
||||||
|
event.accepted = true;
|
||||||
|
var digit = event.text;
|
||||||
|
var textLen = startHourField.text.length;
|
||||||
|
|
||||||
|
if (textLen >= 2 && startHourField.text[0] !== '0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var val = 0;
|
||||||
|
if (textLen === 0) {
|
||||||
|
val = parseInt(digit);
|
||||||
|
} else if (textLen === 1) {
|
||||||
|
val = parseInt(startHourField.text + digit);
|
||||||
|
} else {
|
||||||
|
val = parseInt(startHourField.text[1] + digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
val = Math.max(0, Math.min(23, val));
|
||||||
|
|
||||||
|
if (textLen >= 2 && val < 10) {
|
||||||
|
startHourField.text = "0" + val;
|
||||||
|
} else {
|
||||||
|
startHourField.text = val.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
onCursorPositionChanged: cursorPosition = 2
|
||||||
|
onTextChanged: {
|
||||||
|
var mins = parseInt(startMinuteField.text);
|
||||||
|
const hours = root.convertToMinutes(parseInt(startHourField.text));
|
||||||
|
root.startEdit(hours + mins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
text: qsTr("Select time")
|
id: startSeparator
|
||||||
|
|
||||||
|
font.pointSize: Tokens.font.size.extraLarge
|
||||||
|
text: ":"
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
CustomRect {
|
||||||
CustomRect {
|
id: startMinuteRect
|
||||||
id: startHourRect
|
|
||||||
|
|
||||||
Layout.preferredHeight: 72
|
Layout.preferredHeight: 72
|
||||||
Layout.preferredWidth: 96
|
Layout.preferredWidth: 96
|
||||||
color: startHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
color: startMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
implicitHeight: 72
|
implicitHeight: 72
|
||||||
implicitWidth: 96
|
implicitWidth: 96
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
border.color: startHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
|
||||||
border.width: startHourField.focus ? 2 : 0
|
|
||||||
radius: parent.radius - border.width
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldBase {
|
|
||||||
id: startHourField
|
|
||||||
|
|
||||||
function setConfigText(setting: string): string {
|
|
||||||
var val = root.convertHour(root.object[setting]);
|
|
||||||
if (val === 0) {
|
|
||||||
return "00";
|
|
||||||
}
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
clip: true
|
|
||||||
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
|
||||||
font.family: "Roboto"
|
|
||||||
font.letterSpacing: -0.25
|
|
||||||
font.pixelSize: 56
|
|
||||||
font.weight: 400
|
|
||||||
horizontalAlignment: TextInput.AlignHCenter
|
|
||||||
text: setConfigText(root.settings[1])
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Backspace) {
|
|
||||||
event.accepted = true;
|
|
||||||
if (startHourField.text.length >= 2) {
|
|
||||||
startHourField.text = "0" + startHourField.text[0];
|
|
||||||
} else if (startHourField.text.length === 1) {
|
|
||||||
startHourField.text = "0";
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Escape) {
|
|
||||||
event.accepted = true;
|
|
||||||
startHourField.text = setConfigText(root.settings[1]);
|
|
||||||
startHourField.focus = false;
|
|
||||||
} else if (event.key === Qt.Key_Return) {
|
|
||||||
startHourField.focus = false;
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Tab) {
|
|
||||||
startMinuteField.focus = true;
|
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
|
||||||
endMinuteField.focus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
|
||||||
event.accepted = true;
|
|
||||||
var digit = event.text;
|
|
||||||
var textLen = startHourField.text.length;
|
|
||||||
|
|
||||||
if (textLen >= 2 && startHourField.text[0] !== '0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var val = 0;
|
|
||||||
if (textLen === 0) {
|
|
||||||
val = parseInt(digit);
|
|
||||||
} else if (textLen === 1) {
|
|
||||||
val = parseInt(startHourField.text + digit);
|
|
||||||
} else {
|
|
||||||
val = parseInt(startHourField.text[1] + digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
val = Math.max(0, Math.min(23, val));
|
|
||||||
|
|
||||||
if (textLen >= 2 && val < 10) {
|
|
||||||
startHourField.text = "0" + val;
|
|
||||||
} else {
|
|
||||||
startHourField.text = val.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
onCursorPositionChanged: cursorPosition = 2
|
|
||||||
onTextEdited: {
|
|
||||||
if (startHourField.text === "")
|
|
||||||
return;
|
|
||||||
var val = parseInt(startHourField.text);
|
|
||||||
if (isNaN(val))
|
|
||||||
return;
|
|
||||||
val = Math.max(0, Math.min(23, val));
|
|
||||||
var newText = val.toString();
|
|
||||||
if (newText !== startHourField.text)
|
|
||||||
startHourField.text = newText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: startSeparator
|
|
||||||
|
|
||||||
font.pointSize: Tokens.font.size.extraLarge
|
|
||||||
text: ":"
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
id: startMinuteRect
|
anchors.fill: parent
|
||||||
|
border.color: startMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
|
border.width: startMinuteField.focus ? 2 : 0
|
||||||
|
radius: parent.radius - border.width
|
||||||
|
|
||||||
Layout.preferredHeight: 72
|
Behavior on border.width {
|
||||||
Layout.preferredWidth: 96
|
Anim {}
|
||||||
color: startMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
}
|
||||||
implicitHeight: 72
|
}
|
||||||
implicitWidth: 96
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
CustomRect {
|
TextFieldBase {
|
||||||
anchors.fill: parent
|
id: startMinuteField
|
||||||
border.color: startMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
|
||||||
border.width: startMinuteField.focus ? 2 : 0
|
|
||||||
radius: parent.radius - border.width
|
|
||||||
|
|
||||||
Behavior on border.width {
|
function setConfigText(): string {
|
||||||
Anim {
|
var val = root.convertMinute(root.start);
|
||||||
}
|
if (val === 0) {
|
||||||
|
return "00";
|
||||||
}
|
}
|
||||||
|
return String(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextFieldBase {
|
anchors.left: parent.left
|
||||||
id: startMinuteField
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
function setConfigText(setting: string): string {
|
clip: true
|
||||||
var val = root.convertMinute(root.object[setting]);
|
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
||||||
if (val === 0) {
|
font.family: "Roboto"
|
||||||
return "00";
|
font.letterSpacing: -0.25
|
||||||
}
|
font.pixelSize: 56
|
||||||
return String(val);
|
font.weight: 400
|
||||||
}
|
horizontalAlignment: TextInput.AlignHCenter
|
||||||
|
text: setConfigText()
|
||||||
anchors.left: parent.left
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
clip: true
|
|
||||||
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
|
||||||
font.family: "Roboto"
|
|
||||||
font.letterSpacing: -0.25
|
|
||||||
font.pixelSize: 56
|
|
||||||
font.weight: 400
|
|
||||||
horizontalAlignment: TextInput.AlignHCenter
|
|
||||||
text: setConfigText(root.settings[1])
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Backspace) {
|
|
||||||
event.accepted = true;
|
|
||||||
if (startMinuteField.text.length >= 2) {
|
|
||||||
startMinuteField.text = "0" + startMinuteField.text[0];
|
|
||||||
} else if (startMinuteField.text.length === 1) {
|
|
||||||
startMinuteField.text = "0";
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Escape) {
|
|
||||||
event.accepted = true;
|
|
||||||
startMinuteField.text = setConfigText(root.settings[1]);
|
|
||||||
startMinuteField.focus = false;
|
|
||||||
} else if (event.key === Qt.Key_Return) {
|
|
||||||
startMinuteField.focus = false;
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Tab) {
|
|
||||||
endHourField.focus = true;
|
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
|
||||||
startHourField.focus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
|
||||||
event.accepted = true;
|
|
||||||
var digit = event.text;
|
|
||||||
var textLen = startMinuteField.text.length;
|
|
||||||
|
|
||||||
if (textLen >= 2 && startMinuteField.text[0] !== '0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var val = 0;
|
|
||||||
if (textLen === 0) {
|
|
||||||
val = parseInt(digit);
|
|
||||||
} else if (textLen === 1) {
|
|
||||||
val = parseInt(startMinuteField.text + digit);
|
|
||||||
} else {
|
|
||||||
val = parseInt(startMinuteField.text[1] + digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
val = Math.max(0, Math.min(59, val));
|
|
||||||
|
|
||||||
if (textLen >= 2 && val < 10) {
|
|
||||||
startMinuteField.text = "0" + val;
|
|
||||||
} else {
|
|
||||||
startMinuteField.text = val.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Backspace) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
if (startMinuteField.text.length >= 2) {
|
||||||
onCursorPositionChanged: cursorPosition = 2
|
startMinuteField.text = "0" + startMinuteField.text[0];
|
||||||
onTextEdited: {
|
} else if (startMinuteField.text.length === 1) {
|
||||||
if (startMinuteField.text === "")
|
startMinuteField.text = "0";
|
||||||
return;
|
|
||||||
var val = parseInt(startMinuteField.text);
|
|
||||||
if (isNaN(val))
|
|
||||||
return;
|
|
||||||
val = Math.max(0, Math.min(23, val));
|
|
||||||
var newText = val.toString();
|
|
||||||
if (newText !== startMinuteField.text)
|
|
||||||
startMinuteField.text = newText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: spacer
|
|
||||||
|
|
||||||
Layout.preferredWidth: Tokens.spacing.largeIncreased
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: endHourRect
|
|
||||||
|
|
||||||
Layout.preferredHeight: 72
|
|
||||||
Layout.preferredWidth: 96
|
|
||||||
color: endHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
|
||||||
implicitHeight: 72
|
|
||||||
implicitWidth: 96
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
border.color: endHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
|
||||||
border.width: endHourField.focus ? 2 : 0
|
|
||||||
radius: parent.radius - border.width
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
Anim {
|
|
||||||
}
|
}
|
||||||
}
|
return;
|
||||||
}
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
|
||||||
TextFieldBase {
|
|
||||||
id: endHourField
|
|
||||||
|
|
||||||
function setConfigText(setting: string): string {
|
|
||||||
var val = root.convertHour(root.object[setting]);
|
|
||||||
if (val === 0) {
|
|
||||||
return "00";
|
|
||||||
}
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
clip: true
|
|
||||||
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
|
||||||
font.family: "Roboto"
|
|
||||||
font.letterSpacing: -0.25
|
|
||||||
font.pixelSize: 56
|
|
||||||
font.weight: 400
|
|
||||||
horizontalAlignment: TextInput.AlignHCenter
|
|
||||||
text: setConfigText(root.settings[2])
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Backspace) {
|
|
||||||
event.accepted = true;
|
|
||||||
if (endHourField.text.length >= 2) {
|
|
||||||
endHourField.text = "0" + endHourField.text[0];
|
|
||||||
} else if (endHourField.text.length === 1) {
|
|
||||||
endHourField.text = "0";
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Escape) {
|
|
||||||
event.accepted = true;
|
|
||||||
endHourField.text = setConfigText(root.settings[2]);
|
|
||||||
endHourField.focus = false;
|
|
||||||
} else if (event.key === Qt.Key_Return) {
|
|
||||||
endHourField.focus = false;
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Tab) {
|
|
||||||
endMinuteField.focus = true;
|
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
|
||||||
startMinuteField.focus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
|
||||||
event.accepted = true;
|
|
||||||
var digit = event.text;
|
|
||||||
var textLen = endHourField.text.length;
|
|
||||||
|
|
||||||
if (textLen >= 2 && endHourField.text[0] !== '0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var val = 0;
|
|
||||||
if (textLen === 0) {
|
|
||||||
val = parseInt(digit);
|
|
||||||
} else if (textLen === 1) {
|
|
||||||
val = parseInt(endHourField.text + digit);
|
|
||||||
} else {
|
|
||||||
val = parseInt(endHourField.text[1] + digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
val = Math.max(0, Math.min(23, val));
|
|
||||||
|
|
||||||
if (textLen >= 2 && val < 10) {
|
|
||||||
endHourField.text = "0" + val;
|
|
||||||
} else {
|
|
||||||
endHourField.text = val.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
startMinuteField.text = setConfigText();
|
||||||
onCursorPositionChanged: cursorPosition = 2
|
startMinuteField.focus = false;
|
||||||
onTextEdited: {
|
} else if (event.key === Qt.Key_Return) {
|
||||||
if (endHourField.text === "")
|
startMinuteField.focus = false;
|
||||||
return;
|
return;
|
||||||
var val = parseInt(endHourField.text);
|
} else if (event.key === Qt.Key_Tab) {
|
||||||
if (isNaN(val))
|
endHourField.focus = true;
|
||||||
return;
|
} else if (event.key === Qt.Key_Backtab) {
|
||||||
val = Math.max(0, Math.min(23, val));
|
startHourField.focus = true;
|
||||||
var newText = val.toString();
|
|
||||||
if (newText !== endHourField.text)
|
|
||||||
endHourField.text = newText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: endSeparator
|
|
||||||
|
|
||||||
font.pointSize: Tokens.font.size.extraLarge
|
|
||||||
text: ":"
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: endMinuteRect
|
|
||||||
|
|
||||||
Layout.preferredHeight: 72
|
|
||||||
Layout.preferredWidth: 96
|
|
||||||
color: endMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
|
||||||
implicitHeight: 72
|
|
||||||
implicitWidth: 96
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
border.color: endMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
|
||||||
border.width: endMinuteField.focus ? 2 : 0
|
|
||||||
radius: parent.radius - border.width
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldBase {
|
|
||||||
id: endMinuteField
|
|
||||||
|
|
||||||
function setConfigText(setting: string): string {
|
|
||||||
var val = root.convertMinute(root.object[setting]);
|
|
||||||
if (val === 0) {
|
|
||||||
return "00";
|
|
||||||
}
|
|
||||||
return String(val);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors.left: parent.left
|
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
clip: true
|
|
||||||
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
|
||||||
font.family: "Roboto"
|
|
||||||
font.letterSpacing: -0.25
|
|
||||||
font.pixelSize: 56
|
|
||||||
font.weight: 400
|
|
||||||
horizontalAlignment: TextInput.AlignHCenter
|
|
||||||
text: setConfigText(root.settings[2])
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Backspace) {
|
|
||||||
event.accepted = true;
|
|
||||||
if (endMinuteField.text.length >= 2) {
|
|
||||||
endMinuteField.text = "0" + endMinuteField.text[0];
|
|
||||||
} else if (endMinuteField.text.length === 1) {
|
|
||||||
endMinuteField.text = "0";
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Escape) {
|
|
||||||
event.accepted = true;
|
|
||||||
endMinuteField.text = setConfigText(root.settings[2]);
|
|
||||||
endMinuteField.focus = false;
|
|
||||||
} else if (event.key === Qt.Key_Return) {
|
|
||||||
endMinuteField.focus = false;
|
|
||||||
return;
|
|
||||||
} else if (event.key === Qt.Key_Tab) {
|
|
||||||
startHourField.focus = true;
|
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
|
||||||
endHourField.focus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
|
||||||
event.accepted = true;
|
|
||||||
var digit = event.text;
|
|
||||||
var textLen = endMinuteField.text.length;
|
|
||||||
|
|
||||||
if (textLen >= 2 && endMinuteField.text[0] !== '0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var val = 0;
|
|
||||||
if (textLen === 0) {
|
|
||||||
val = parseInt(digit);
|
|
||||||
} else if (textLen === 1) {
|
|
||||||
val = parseInt(endMinuteField.text + digit);
|
|
||||||
} else {
|
|
||||||
val = parseInt(endMinuteField.text[1] + digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
val = Math.max(0, Math.min(59, val));
|
|
||||||
|
|
||||||
if (textLen >= 2 && val < 10) {
|
|
||||||
endMinuteField.text = "0" + val;
|
|
||||||
} else {
|
|
||||||
endMinuteField.text = val.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
var digit = event.text;
|
||||||
onCursorPositionChanged: cursorPosition = 2
|
var textLen = startMinuteField.text.length;
|
||||||
onTextEdited: {
|
|
||||||
if (endMinuteField.text === "")
|
if (textLen >= 2 && startMinuteField.text[0] !== '0') {
|
||||||
return;
|
return;
|
||||||
var val = parseInt(endMinuteField.text);
|
}
|
||||||
if (isNaN(val))
|
|
||||||
return;
|
var val = 0;
|
||||||
val = Math.max(0, Math.min(23, val));
|
if (textLen === 0) {
|
||||||
var newText = val.toString();
|
val = parseInt(digit);
|
||||||
if (newText !== endMinuteField.text)
|
} else if (textLen === 1) {
|
||||||
endMinuteField.text = newText;
|
val = parseInt(startMinuteField.text + digit);
|
||||||
|
} else {
|
||||||
|
val = parseInt(startMinuteField.text[1] + digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
val = Math.max(0, Math.min(59, val));
|
||||||
|
|
||||||
|
if (textLen >= 2 && val < 10) {
|
||||||
|
startMinuteField.text = "0" + val;
|
||||||
|
} else {
|
||||||
|
startMinuteField.text = val.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
onCursorPositionChanged: cursorPosition = 2
|
||||||
|
onTextChanged: {
|
||||||
|
var mins = parseInt(startMinuteField.text);
|
||||||
|
const hours = root.convertToMinutes(parseInt(startHourField.text));
|
||||||
|
root.startEdit(hours + mins);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Item {
|
||||||
CustomText {
|
id: spacer
|
||||||
Layout.preferredWidth: spacer.x + spacer.width
|
|
||||||
text: qsTr("Start")
|
Layout.preferredWidth: Tokens.spacing.largeIncreased
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: endHourRect
|
||||||
|
|
||||||
|
Layout.preferredHeight: 72
|
||||||
|
Layout.preferredWidth: 96
|
||||||
|
color: endHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
|
implicitHeight: 72
|
||||||
|
implicitWidth: 96
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
border.color: endHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
|
border.width: endHourField.focus ? 2 : 0
|
||||||
|
radius: parent.radius - border.width
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
TextFieldBase {
|
||||||
Layout.preferredWidth: endMinuteRect.width + endHourRect.width
|
id: endHourField
|
||||||
text: qsTr("End")
|
|
||||||
|
function setConfigText(): string {
|
||||||
|
var val = root.convertHour(root.end);
|
||||||
|
if (val === 0) {
|
||||||
|
return "00";
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
clip: true
|
||||||
|
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
||||||
|
font.family: "Roboto"
|
||||||
|
font.letterSpacing: -0.25
|
||||||
|
font.pixelSize: 56
|
||||||
|
font.weight: 400
|
||||||
|
horizontalAlignment: TextInput.AlignHCenter
|
||||||
|
text: setConfigText()
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Backspace) {
|
||||||
|
event.accepted = true;
|
||||||
|
if (endHourField.text.length >= 2) {
|
||||||
|
endHourField.text = "0" + endHourField.text[0];
|
||||||
|
} else if (endHourField.text.length === 1) {
|
||||||
|
endHourField.text = "0";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
event.accepted = true;
|
||||||
|
endHourField.text = setConfigText();
|
||||||
|
endHourField.focus = false;
|
||||||
|
} else if (event.key === Qt.Key_Return) {
|
||||||
|
endHourField.focus = false;
|
||||||
|
return;
|
||||||
|
} else if (event.key === Qt.Key_Tab) {
|
||||||
|
endMinuteField.focus = true;
|
||||||
|
} else if (event.key === Qt.Key_Backtab) {
|
||||||
|
startMinuteField.focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
||||||
|
event.accepted = true;
|
||||||
|
var digit = event.text;
|
||||||
|
var textLen = endHourField.text.length;
|
||||||
|
|
||||||
|
if (textLen >= 2 && endHourField.text[0] !== '0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var val = 0;
|
||||||
|
if (textLen === 0) {
|
||||||
|
val = parseInt(digit);
|
||||||
|
} else if (textLen === 1) {
|
||||||
|
val = parseInt(endHourField.text + digit);
|
||||||
|
} else {
|
||||||
|
val = parseInt(endHourField.text[1] + digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
val = Math.max(0, Math.min(23, val));
|
||||||
|
|
||||||
|
if (textLen >= 2 && val < 10) {
|
||||||
|
endHourField.text = "0" + val;
|
||||||
|
} else {
|
||||||
|
endHourField.text = val.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
onCursorPositionChanged: cursorPosition = 2
|
||||||
|
onTextChanged: {
|
||||||
|
var mins = parseInt(endMinuteField.text);
|
||||||
|
const hours = root.convertToMinutes(parseInt(endHourField.text));
|
||||||
|
root.endEdit(hours + mins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: endSeparator
|
||||||
|
|
||||||
|
font.pointSize: Tokens.font.size.extraLarge
|
||||||
|
text: ":"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: endMinuteRect
|
||||||
|
|
||||||
|
Layout.preferredHeight: 72
|
||||||
|
Layout.preferredWidth: 96
|
||||||
|
color: endMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
|
implicitHeight: 72
|
||||||
|
implicitWidth: 96
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
border.color: endMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
|
||||||
|
border.width: endMinuteField.focus ? 2 : 0
|
||||||
|
radius: parent.radius - border.width
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldBase {
|
||||||
|
id: endMinuteField
|
||||||
|
|
||||||
|
function setConfigText(): string {
|
||||||
|
var val = root.convertMinute(root.end);
|
||||||
|
if (val === 0) {
|
||||||
|
return "00";
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
clip: true
|
||||||
|
color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
|
||||||
|
font.family: "Roboto"
|
||||||
|
font.letterSpacing: -0.25
|
||||||
|
font.pixelSize: 56
|
||||||
|
font.weight: 400
|
||||||
|
horizontalAlignment: TextInput.AlignHCenter
|
||||||
|
text: setConfigText()
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Backspace) {
|
||||||
|
event.accepted = true;
|
||||||
|
if (endMinuteField.text.length >= 2) {
|
||||||
|
endMinuteField.text = "0" + endMinuteField.text[0];
|
||||||
|
} else if (endMinuteField.text.length === 1) {
|
||||||
|
endMinuteField.text = "0";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
event.accepted = true;
|
||||||
|
endMinuteField.text = setConfigText();
|
||||||
|
endMinuteField.focus = false;
|
||||||
|
} else if (event.key === Qt.Key_Return) {
|
||||||
|
endMinuteField.focus = false;
|
||||||
|
return;
|
||||||
|
} else if (event.key === Qt.Key_Tab) {
|
||||||
|
startHourField.focus = true;
|
||||||
|
} else if (event.key === Qt.Key_Backtab) {
|
||||||
|
endHourField.focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
|
||||||
|
event.accepted = true;
|
||||||
|
var digit = event.text;
|
||||||
|
var textLen = endMinuteField.text.length;
|
||||||
|
|
||||||
|
if (textLen >= 2 && endMinuteField.text[0] !== '0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var val = 0;
|
||||||
|
if (textLen === 0) {
|
||||||
|
val = parseInt(digit);
|
||||||
|
} else if (textLen === 1) {
|
||||||
|
val = parseInt(endMinuteField.text + digit);
|
||||||
|
} else {
|
||||||
|
val = parseInt(endMinuteField.text[1] + digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
val = Math.max(0, Math.min(59, val));
|
||||||
|
|
||||||
|
if (textLen >= 2 && val < 10) {
|
||||||
|
endMinuteField.text = "0" + val;
|
||||||
|
} else {
|
||||||
|
endMinuteField.text = val.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
onCursorPositionChanged: cursorPosition = 2
|
||||||
|
onTextChanged: {
|
||||||
|
var mins = parseInt(endMinuteField.text);
|
||||||
|
const hours = root.convertToMinutes(parseInt(endHourField.text));
|
||||||
|
root.endEdit(hours + mins);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: buttonRow
|
CustomText {
|
||||||
|
Layout.preferredWidth: spacer.x + spacer.width
|
||||||
anchors.bottom: parent.bottom
|
text: qsTr("Start")
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Tokens.padding.largeIncreased
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: column.bottom
|
|
||||||
anchors.topMargin: Tokens.spacing.medium
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: buttonSpacer
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonRow {
|
CustomText {
|
||||||
spacing: Tokens.spacing.medium
|
Layout.preferredWidth: endMinuteRect.width + endHourRect.width
|
||||||
|
text: qsTr("End")
|
||||||
IconTextButton {
|
|
||||||
font.pointSize: Tokens.font.size.normal
|
|
||||||
icon: "close"
|
|
||||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
|
||||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
|
||||||
isRound: true
|
|
||||||
isToggle: false
|
|
||||||
shapeMorph: true
|
|
||||||
shapeMorphExpansion: pressed ? 12 : 0
|
|
||||||
text: "Cancel"
|
|
||||||
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
IconTextButton {
|
|
||||||
font.pointSize: Tokens.font.size.normal
|
|
||||||
icon: "check"
|
|
||||||
isRound: true
|
|
||||||
isToggle: false
|
|
||||||
shapeMorph: true
|
|
||||||
shapeMorphExpansion: pressed ? 12 : 0
|
|
||||||
text: "Apply"
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
const start = root.convertToMinutes(parseInt(startHourField.text)) + parseInt(startMinuteField.text);
|
|
||||||
const end = root.convertToMinutes(parseInt(endHourField.text)) + parseInt(endMinuteField.text);
|
|
||||||
root.applySettings(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ CustomSwitch {
|
|||||||
property alias last: bg.last
|
property alias last: bg.last
|
||||||
property string settingAnchor
|
property string settingAnchor
|
||||||
property string subtext
|
property string subtext
|
||||||
|
property string iconText
|
||||||
|
|
||||||
function flashHighlight(): void {
|
function flashHighlight(): void {
|
||||||
bg.flashHighlight();
|
bg.flashHighlight();
|
||||||
@@ -63,8 +64,7 @@ CustomSwitch {
|
|||||||
text: root.text
|
text: root.text
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +81,16 @@ CustomSwitch {
|
|||||||
visible: root.subtext
|
visible: root.subtext
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: icon
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.iconText ?? ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ CustomClippingRect {
|
|||||||
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
||||||
|
|
||||||
Behavior on blobColor {
|
Behavior on blobColor {
|
||||||
CAnim {
|
CAnim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
@@ -70,8 +69,4 @@ CustomClippingRect {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
sState: root.sState
|
sState: root.sState
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupOverlay {
|
|
||||||
anchors.fill: parent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,14 +81,10 @@ VerticalFadeFlickable {
|
|||||||
topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
|
|
||||||
RadiusBehavior on bottomLeftRadius {
|
RadiusBehavior on bottomLeftRadius {}
|
||||||
}
|
RadiusBehavior on bottomRightRadius {}
|
||||||
RadiusBehavior on bottomRightRadius {
|
RadiusBehavior on topLeftRadius {}
|
||||||
}
|
RadiusBehavior on topRightRadius {}
|
||||||
RadiusBehavior on topLeftRadius {
|
|
||||||
}
|
|
||||||
RadiusBehavior on topRightRadius {
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
id: stateLayer
|
id: stateLayer
|
||||||
@@ -262,14 +258,10 @@ VerticalFadeFlickable {
|
|||||||
topRightRadius: layer.pressed ? Tokens.rounding.largeIncreased : isFirst ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
topRightRadius: layer.pressed ? Tokens.rounding.largeIncreased : isFirst ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
|
||||||
width: cardList.width
|
width: cardList.width
|
||||||
|
|
||||||
RadiusBehavior on bottomLeftRadius {
|
RadiusBehavior on bottomLeftRadius {}
|
||||||
}
|
RadiusBehavior on bottomRightRadius {}
|
||||||
RadiusBehavior on bottomRightRadius {
|
RadiusBehavior on topLeftRadius {}
|
||||||
}
|
RadiusBehavior on topRightRadius {}
|
||||||
RadiusBehavior on topLeftRadius {
|
|
||||||
}
|
|
||||||
RadiusBehavior on topRightRadius {
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: resultLayout
|
id: resultLayout
|
||||||
@@ -318,9 +310,10 @@ VerticalFadeFlickable {
|
|||||||
|
|
||||||
z: 1
|
z: 1
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.sState.jumpToSetting(result.modelData.pageIdx, result.modelData.subPath, result.modelData.anchor);
|
const target = result.modelData.targetSubPath;
|
||||||
}
|
root.sState.jumpToSetting(result.modelData.pageIdx, target.length > 0 ? target : result.modelData.subPath, target.length > 0 ? "" : result.modelData.anchor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomSwitch {
|
CustomSwitch {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import qs.Modules.Settings.Pages.Audio
|
|||||||
import qs.Modules.Settings.Pages.Apps
|
import qs.Modules.Settings.Pages.Apps
|
||||||
import qs.Modules.Settings.Pages.Panels
|
import qs.Modules.Settings.Pages.Panels
|
||||||
import qs.Modules.Settings.Pages.Panels.Bar
|
import qs.Modules.Settings.Pages.Panels.Bar
|
||||||
|
import qs.Modules.Settings.Pages.Panels.Sidebar
|
||||||
import qs.Modules.Settings.Pages.Services
|
import qs.Modules.Settings.Pages.Services
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
@@ -23,13 +24,15 @@ QtObject {
|
|||||||
// Wallpaper & style
|
// Wallpaper & style
|
||||||
StackPage {
|
StackPage {
|
||||||
Component {
|
Component {
|
||||||
WallpaperPage {
|
WallpaperPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
WallpaperSelect {
|
WallpaperSelect {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
ColorsFonts {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -37,32 +40,27 @@ QtObject {
|
|||||||
// Screenshot
|
// Screenshot
|
||||||
StackPage {
|
StackPage {
|
||||||
Component {
|
Component {
|
||||||
ScreenshotPage {
|
ScreenshotPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Connectivity
|
// Connectivity
|
||||||
Component {
|
Component {
|
||||||
PlaceholderComp {
|
PlaceholderComp {}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Component {
|
Component {
|
||||||
PlaceholderComp {
|
PlaceholderComp {}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Component {
|
Component {
|
||||||
// Audio
|
// Audio
|
||||||
StackPage {
|
StackPage {
|
||||||
Component {
|
Component {
|
||||||
AudioPage {
|
AudioPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
AppVolumes {
|
AppVolumes {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -71,49 +69,45 @@ QtObject {
|
|||||||
Component {
|
Component {
|
||||||
StackPage {
|
StackPage {
|
||||||
Component {
|
Component {
|
||||||
PanelsPage {
|
PanelsPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
BarPanel {
|
BarPanel {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
DashboardPanel {
|
DashboardPanel {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
ResourcesPanel {
|
ResourcesPanel {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
LauncherPanel {
|
LauncherPanel {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
SidebarPanel {
|
SidebarPanel {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bar sub pages
|
// Bar sub pages
|
||||||
Component {
|
Component {
|
||||||
BarTray {
|
BarTray {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
BarStatusIcons {
|
BarStatusIcons {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
BarClock {
|
BarClock {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sidebar sub pages
|
||||||
|
Component {
|
||||||
|
SidebarLlm {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -122,18 +116,15 @@ QtObject {
|
|||||||
Component {
|
Component {
|
||||||
StackPage {
|
StackPage {
|
||||||
Component {
|
Component {
|
||||||
AppsPage {
|
AppsPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
AllApps {
|
AllApps {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
AppInfo {
|
AppInfo {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -142,13 +133,11 @@ QtObject {
|
|||||||
Component {
|
Component {
|
||||||
StackPage {
|
StackPage {
|
||||||
Component {
|
Component {
|
||||||
ServicesPage {
|
ServicesPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
NotificationsPage {
|
NotificationsPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -157,15 +146,13 @@ QtObject {
|
|||||||
Component {
|
Component {
|
||||||
StackPage {
|
StackPage {
|
||||||
Component {
|
Component {
|
||||||
AboutPage {
|
AboutPage {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
readonly property Component placeholderComp: Component {
|
readonly property Component placeholderComp: Component {
|
||||||
PlaceholderComp {
|
PlaceholderComp {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component PlaceholderComp: Item {
|
component PlaceholderComp: Item {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -41,6 +42,18 @@ Item {
|
|||||||
objectName: "PageContainer"
|
objectName: "PageContainer"
|
||||||
|
|
||||||
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
|
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
|
||||||
|
|
||||||
|
// CustomRect {
|
||||||
|
// anchors.fill: parent
|
||||||
|
// color: Qt.alpha(Colors.palette.m3shadow, 0.3)
|
||||||
|
// opacity: root.sState.dimmed ? 1 : 0
|
||||||
|
// visible: opacity > 0
|
||||||
|
// z: 0
|
||||||
|
//
|
||||||
|
// Behavior on opacity {
|
||||||
|
// Anim {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ PageBase {
|
|||||||
enabled: Object.keys(model).length > 0
|
enabled: Object.keys(model).length > 0
|
||||||
header: qsTr("Add new entry")
|
header: qsTr("Add new entry")
|
||||||
icon: "add"
|
icon: "add"
|
||||||
|
settingAnchor: "panels-bar-status-icons-add-item"
|
||||||
|
onOpenChanged: {
|
||||||
|
root.sState.dimmed = open;
|
||||||
|
}
|
||||||
|
last: true
|
||||||
label: qsTr("Add entry")
|
label: qsTr("Add entry")
|
||||||
model: {
|
model: {
|
||||||
const present = new Set(Config.bar.tray.statusIcons.values.map(item => item.id));
|
const present = new Set(Config.bar.tray.statusIcons.values.map(item => item.id));
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Modules.Settings.Common
|
||||||
|
|
||||||
|
PageBase {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var schemes: [
|
||||||
|
{
|
||||||
|
id: "oneDark",
|
||||||
|
label: qsTr("One Dark")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nord",
|
||||||
|
label: qsTr("Nord")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dracula",
|
||||||
|
label: qsTr("Dracula")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "githubDark",
|
||||||
|
label: qsTr("Github Dark")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "solarizedDark",
|
||||||
|
label: qsTr("Solarized Dark")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "monokai",
|
||||||
|
label: qsTr("Monokai")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gruvboxDark",
|
||||||
|
label: qsTr("Gruvbox Dark")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catppuccinMocha",
|
||||||
|
label: qsTr("Catppuccin Mocha")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tokyoNight",
|
||||||
|
label: qsTr("Tokyo Night")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ayuDark",
|
||||||
|
label: qsTr("Ayu Dark")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "palenight",
|
||||||
|
label: qsTr("Pale Night")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
isSubPage: true
|
||||||
|
title: qsTr("Sidebar")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
spacing: Tokens.spacing.extraSmall / 2
|
||||||
|
width: root.cappedWidth
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
first: true
|
||||||
|
text: qsTr("General")
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
settingAnchor: "panels-sidebar-llm-enabled"
|
||||||
|
checked: Config.llm.enabled
|
||||||
|
text: qsTr("Enabled")
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
|
||||||
|
onToggled: Config.llm.enabled = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("Appearance")
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogSelectButton {
|
||||||
|
id: selectScheme
|
||||||
|
|
||||||
|
acceptLabel: qsTr("Confirm")
|
||||||
|
enabled: Object.keys(model).length > 0
|
||||||
|
settingAnchor: "panels-sidebar-select-scheme"
|
||||||
|
separateContent: false
|
||||||
|
header: qsTr("Select scheme")
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
icon: "format_paint"
|
||||||
|
iconLabel.font.pointSize: Tokens.font.size.large
|
||||||
|
rowButton.trailingIcon: "open_in_new"
|
||||||
|
onOpenChanged: {
|
||||||
|
root.sState.dimmed = open;
|
||||||
|
}
|
||||||
|
rowButton.activeItem: root.schemes.find(s => s.id === Config.llm.appearance.scheme).label
|
||||||
|
label: qsTr("Color scheme")
|
||||||
|
subtext: qsTr("Select the color scheme used for code blocks")
|
||||||
|
model: root.schemes
|
||||||
|
rootParent: root.flickable
|
||||||
|
|
||||||
|
initialSelect: Config.llm.appearance.scheme
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Config.llm.appearance.scheme = selectedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,12 @@ PageBase {
|
|||||||
enabled: Object.keys(model).length > 0
|
enabled: Object.keys(model).length > 0
|
||||||
header: qsTr("Add new entry")
|
header: qsTr("Add new entry")
|
||||||
icon: "add"
|
icon: "add"
|
||||||
|
onOpenChanged: {
|
||||||
|
root.sState.dimmed = open;
|
||||||
|
}
|
||||||
|
settingAnchor: "panels-sidebar-quick-toggles-add-item"
|
||||||
label: qsTr("Add entry")
|
label: qsTr("Add entry")
|
||||||
|
last: true
|
||||||
model: {
|
model: {
|
||||||
const present = new Set(Config.utilities.quickToggles.values.map(item => item.id));
|
const present = new Set(Config.utilities.quickToggles.values.map(item => item.id));
|
||||||
return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({
|
return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({
|
||||||
@@ -108,5 +113,19 @@ PageBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("AI chat")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavRow {
|
||||||
|
text: qsTr("AI chat")
|
||||||
|
settingAnchor: "panels-sidebar-llm"
|
||||||
|
icon: "robot_2"
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
|
||||||
|
onClicked: root.sState.openSubPage(9)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,12 +202,12 @@ PageBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToggleRow {
|
ToggleRow {
|
||||||
checked: Config.general.color.smart
|
checked: Config.colors.smart
|
||||||
settingAnchor: "services-smart-color-scheme"
|
settingAnchor: "services-smart-color-scheme"
|
||||||
subtext: qsTr("Derive theme mode from the wallpaper")
|
subtext: qsTr("Derive theme mode from the wallpaper")
|
||||||
text: qsTr("Smart color scheme")
|
text: qsTr("Smart color scheme")
|
||||||
|
|
||||||
onToggled: Config.general.color.smart = checked
|
onToggled: Config.colors.smart = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectRow {
|
SelectRow {
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Paths
|
||||||
|
import qs.Services
|
||||||
|
import qs.Modules.Settings.Common
|
||||||
|
|
||||||
|
PageBase {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property list<string> fonts: Qt.fontFamilies()
|
||||||
|
readonly property var schemes: [
|
||||||
|
{
|
||||||
|
id: "onedark",
|
||||||
|
label: qsTr("One Dark")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nord",
|
||||||
|
label: qsTr("Nord")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dracula",
|
||||||
|
label: qsTr("Dracula")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "darkgreen",
|
||||||
|
label: qsTr("Darkgreen")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "solarized",
|
||||||
|
label: qsTr("Solarized")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rosepine",
|
||||||
|
label: qsTr("Rosepine")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gruvbox",
|
||||||
|
label: qsTr("Gruvbox")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catppuccin",
|
||||||
|
label: qsTr("Catppuccin")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tokyonight",
|
||||||
|
label: qsTr("Tokyo Night")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "shadotheme",
|
||||||
|
label: qsTr("Shadotheme")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
isSubPage: true
|
||||||
|
title: qsTr("Colors & font")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
spacing: Tokens.spacing.extraSmall / 2
|
||||||
|
width: root.cappedWidth
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
first: true
|
||||||
|
text: qsTr("Colors")
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
text: qsTr("Dynamic colors")
|
||||||
|
first: true
|
||||||
|
subtext: qsTr("Automatic color scheme based on wallpaper is %1").arg(Config.colors.schemeGen ? "enabled" : "disabled")
|
||||||
|
checked: Config.colors.schemeGen
|
||||||
|
settingAnchor: "colors-fonts-dynamic-colors"
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
Config.colors.schemeGen = checked;
|
||||||
|
|
||||||
|
if (checked)
|
||||||
|
Colors.setMode(Config.colors.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogSelectButton {
|
||||||
|
settingAnchor: "colors-fonts-preset-scheme"
|
||||||
|
model: root.schemes
|
||||||
|
acceptLabel: qsTr("Confirm")
|
||||||
|
enabled: !Config.colors.schemeGen
|
||||||
|
header: qsTr("Select scheme")
|
||||||
|
rowButton.activeItem: root.schemes.find(s => {
|
||||||
|
var regex = new RegExp(Config.colors.presets.name, "i");
|
||||||
|
return regex.test(s.id);
|
||||||
|
}).label ?? ""
|
||||||
|
label: qsTr("Preset scheme")
|
||||||
|
subtext: qsTr("Select preset color scheme")
|
||||||
|
rootParent: root.flickable
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: view
|
||||||
|
path: `${Paths.state}/presets.json`
|
||||||
|
|
||||||
|
watchChanges: true
|
||||||
|
onFileChanged: reload()
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: adapter
|
||||||
|
|
||||||
|
property var presets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialSelect: Config.colors.presets.name.toLowerCase()
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const s = selectedItem.toLowerCase();
|
||||||
|
let v = "";
|
||||||
|
let a = "";
|
||||||
|
const existingVariant = Config.colors.presets.variant;
|
||||||
|
const existingAccent = Config.colors.presets.accent;
|
||||||
|
const variantKeys = Object.keys(adapter.presets[s].variants);
|
||||||
|
|
||||||
|
if (existingVariant !== "" && variantKeys.some(v => v === existingVariant))
|
||||||
|
v = existingVariant;
|
||||||
|
|
||||||
|
if (v !== "" && existingAccent !== "" && adapter.presets[s].variants[v]?.accents.some(a => a === existingAccent))
|
||||||
|
a = existingAccent;
|
||||||
|
|
||||||
|
if (a !== "")
|
||||||
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`, "-a", `${a}`]);
|
||||||
|
else if (v !== "")
|
||||||
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`]);
|
||||||
|
else
|
||||||
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${variantKeys[0]}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogSelectButton {
|
||||||
|
settingAnchor: "colors-fonts-preset-variant"
|
||||||
|
// qmlformat off
|
||||||
|
model: {
|
||||||
|
const v = adapter.presets[Config.colors.presets.name].variants;
|
||||||
|
const map = Object.keys(v).map(v => ({
|
||||||
|
id: v,
|
||||||
|
label: qsTr("%1%2")
|
||||||
|
.arg(v.charAt(0).toUpperCase())
|
||||||
|
.arg(v.slice(1).toLowerCase())
|
||||||
|
}));
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
// qmlformat on
|
||||||
|
acceptLabel: qsTr("Confirm")
|
||||||
|
enabled: !Config.colors.schemeGen && model.length > 1
|
||||||
|
header: qsTr("Select variant")
|
||||||
|
rowButton.activeItem: {
|
||||||
|
const v = Config.colors.presets.variant ?? "";
|
||||||
|
if (v === "")
|
||||||
|
return "";
|
||||||
|
return qsTr("%1%2").arg(v.charAt(0).toUpperCase()).arg(v.slice(1).toLowerCase());
|
||||||
|
}
|
||||||
|
label: qsTr("Variant")
|
||||||
|
subtext: qsTr("Select preset variant")
|
||||||
|
rootParent: root.flickable
|
||||||
|
|
||||||
|
initialSelect: Config.colors.presets.variant
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const v = selectedItem.toLowerCase();
|
||||||
|
const s = Config.colors.presets.name;
|
||||||
|
const existingAccent = Config.colors.presets.accent;
|
||||||
|
let a = "";
|
||||||
|
|
||||||
|
if (existingAccent !== "" && adapter.presets[s].variants[v]?.accents.some(a => a === existingAccent))
|
||||||
|
a = existingAccent;
|
||||||
|
|
||||||
|
if (a !== "")
|
||||||
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`, "-a", `${a}`]);
|
||||||
|
else
|
||||||
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogSelectButton {
|
||||||
|
settingAnchor: "colors-fonts-preset-accent"
|
||||||
|
// qmlformat off
|
||||||
|
model: {
|
||||||
|
const a = adapter.presets[Config.colors.presets.name].variants[Config.colors.presets.variant]?.accents ?? [];
|
||||||
|
|
||||||
|
if (a.length === 0)
|
||||||
|
return a;
|
||||||
|
|
||||||
|
const map = a.map(a => ({
|
||||||
|
id: a,
|
||||||
|
label: qsTr("%1%2")
|
||||||
|
.arg(a.charAt(0).toUpperCase())
|
||||||
|
.arg(a.slice(1).toLowerCase())
|
||||||
|
}));
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
// qmlformat on
|
||||||
|
acceptLabel: qsTr("Confirm")
|
||||||
|
enabled: !Config.colors.schemeGen && model.length > 1
|
||||||
|
header: qsTr("Select accent")
|
||||||
|
rowButton.activeItem: {
|
||||||
|
const a = Config.colors.presets.accent ?? "";
|
||||||
|
if (a === "")
|
||||||
|
return "";
|
||||||
|
return qsTr("%1%2").arg(a.charAt(0).toUpperCase()).arg(a.slice(1).toLowerCase());
|
||||||
|
}
|
||||||
|
label: qsTr("Accent")
|
||||||
|
last: true
|
||||||
|
subtext: qsTr("Select preset accent")
|
||||||
|
rootParent: root.flickable
|
||||||
|
|
||||||
|
initialSelect: Config.colors.presets.accent
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const a = selectedItem.toLowerCase();
|
||||||
|
const v = Config.colors.presets.variant;
|
||||||
|
const s = Config.colors.presets.name;
|
||||||
|
|
||||||
|
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`, "-a", `${a}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("Fonts")
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPopup {
|
||||||
|
id: selectSansFont
|
||||||
|
|
||||||
|
settingAnchor: "colors-fonts-sans-font"
|
||||||
|
model: root.fonts
|
||||||
|
acceptLabel: qsTr("Confirm")
|
||||||
|
enabled: Object.keys(model).length > 0
|
||||||
|
separateContent: false
|
||||||
|
header: qsTr("Select font")
|
||||||
|
first: true
|
||||||
|
rowButton.activeItem: root.fonts.find(s => {
|
||||||
|
var regex = new RegExp(Config.appearance.font.family.sans, "i");
|
||||||
|
return regex.test(s);
|
||||||
|
}) ?? ""
|
||||||
|
label: qsTr("Sans font")
|
||||||
|
subtext: qsTr("Select desired sans font")
|
||||||
|
rootParent: root.flickable
|
||||||
|
|
||||||
|
initialSelect: Config.appearance.font.family.sans
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Config.appearance.font.family.sans = selectedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPopup {
|
||||||
|
id: selectMonoFont
|
||||||
|
|
||||||
|
acceptLabel: qsTr("Confirm")
|
||||||
|
enabled: Object.keys(model).length > 0
|
||||||
|
settingAnchor: "colors-fonts-mono-font"
|
||||||
|
separateContent: false
|
||||||
|
header: qsTr("Select font")
|
||||||
|
rowButton.activeItem: root.fonts.find(s => {
|
||||||
|
var regex = new RegExp(Config.appearance.font.family.mono, "i");
|
||||||
|
return regex.test(s);
|
||||||
|
}) ?? ""
|
||||||
|
label: qsTr("Mono font")
|
||||||
|
subtext: qsTr("Select desired mono font")
|
||||||
|
model: root.fonts
|
||||||
|
rootParent: root.flickable
|
||||||
|
|
||||||
|
initialSelect: Config.appearance.font.family.mono
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Config.appearance.font.family.mono = selectedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPopup {
|
||||||
|
id: selectClockFont
|
||||||
|
|
||||||
|
acceptLabel: qsTr("Confirm")
|
||||||
|
enabled: Object.keys(model).length > 0
|
||||||
|
separateContent: false
|
||||||
|
settingAnchor: "colors-fonts-clock-font"
|
||||||
|
header: qsTr("Select font")
|
||||||
|
last: true
|
||||||
|
rowButton.activeItem: root.fonts.find(s => {
|
||||||
|
var regex = new RegExp(Config.appearance.font.family.clock, "i");
|
||||||
|
return regex.test(s);
|
||||||
|
}) ?? ""
|
||||||
|
label: qsTr("Clock font")
|
||||||
|
subtext: qsTr("Select desired clock font")
|
||||||
|
model: root.fonts
|
||||||
|
rootParent: root.flickable
|
||||||
|
|
||||||
|
initialSelect: Config.appearance.font.family.clock
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Config.appearance.font.family.clock = selectedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,18 +33,41 @@ PageBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconTextButton {
|
ButtonRow {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
enabled: Config.background.enabled
|
spacing: Tokens.spacing.small
|
||||||
horizontalPadding: Tokens.padding.extraLarge
|
|
||||||
icon: "wallpaper"
|
|
||||||
isRound: true
|
|
||||||
shapeMorph: true
|
|
||||||
text: qsTr("Wallpapers")
|
|
||||||
type: IconTextButton.Tonal
|
|
||||||
verticalPadding: Tokens.padding.small
|
|
||||||
|
|
||||||
onClicked: root.sState.openSubPage(1) // Wallpaper page
|
IconTextButton {
|
||||||
|
|
||||||
|
property string settingAnchor: "style-wallpapers"
|
||||||
|
|
||||||
|
enabled: Config.background.enabled
|
||||||
|
horizontalPadding: Tokens.padding.extraLarge
|
||||||
|
icon: "wallpaper"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
text: qsTr("Wallpapers")
|
||||||
|
type: IconTextButton.Tonal
|
||||||
|
verticalPadding: Tokens.padding.small
|
||||||
|
|
||||||
|
onClicked: root.sState.openSubPage(1) // Wallpaper page
|
||||||
|
}
|
||||||
|
|
||||||
|
IconTextButton {
|
||||||
|
|
||||||
|
property string settingAnchor: "style-colors-fonts"
|
||||||
|
|
||||||
|
enabled: Config.background.enabled
|
||||||
|
horizontalPadding: Tokens.padding.extraLarge
|
||||||
|
icon: "palette"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
text: qsTr("Colors & font")
|
||||||
|
type: IconTextButton.Tonal
|
||||||
|
verticalPadding: Tokens.padding.small
|
||||||
|
|
||||||
|
onClicked: root.sState.openSubPage(2) // Colors & font page
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleRow {
|
ToggleRow {
|
||||||
@@ -76,85 +99,116 @@ PageBase {
|
|||||||
onToggled: Colors.setMode(checked ? "dark" : "light")
|
onToggled: Colors.setMode(checked ? "dark" : "light")
|
||||||
}
|
}
|
||||||
|
|
||||||
OverlayRow {
|
TimeDialogSelect {
|
||||||
id: darkMode
|
|
||||||
|
|
||||||
readonly property string endTime: {
|
readonly property string endTime: {
|
||||||
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
||||||
d.setMinutes(Config.general.color.scheduleDarkEnd);
|
d.setMinutes(Config.colors.scheduleDarkEnd);
|
||||||
return Qt.formatTime(d, "hh:mm AP");
|
return Qt.formatTime(d, "hh:mm AP");
|
||||||
}
|
}
|
||||||
readonly property string startTime: {
|
readonly property string startTime: {
|
||||||
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
||||||
d.setMinutes(Config.general.color.scheduleDarkStart);
|
d.setMinutes(Config.colors.scheduleDarkStart);
|
||||||
return Qt.formatTime(d, "hh:mm AP");
|
return Qt.formatTime(d, "hh:mm AP");
|
||||||
}
|
}
|
||||||
|
|
||||||
checked: Config.general.color.scheduleDark
|
rowButton.checked: Config.colors.scheduleDark
|
||||||
|
rowButton.onToggled: Config.colors.scheduleDark = rowButton.checked
|
||||||
|
start: Config.colors.scheduleDarkStart
|
||||||
|
end: Config.colors.scheduleDarkEnd
|
||||||
first: true
|
first: true
|
||||||
|
last: true
|
||||||
settingAnchor: "style-schedule-dark-mode"
|
settingAnchor: "style-schedule-dark-mode"
|
||||||
|
acceptLabel: qsTr("Apply")
|
||||||
|
rootParent: root.flickable
|
||||||
|
header: qsTr("Select time")
|
||||||
subtext: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
|
subtext: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
|
||||||
text: qsTr("Schedule dark mode")
|
label: qsTr("Schedule dark mode")
|
||||||
|
|
||||||
popup: Component {
|
onOpenChanged: {
|
||||||
TimeInput {
|
root.sState.dimmed = open;
|
||||||
object: Config.general.color
|
|
||||||
settings: ["scheduleDark", "scheduleDarkStart", "scheduleDarkEnd"]
|
|
||||||
|
|
||||||
onApplySettings: (start, end) => {
|
|
||||||
Config.general.color.scheduleDarkStart = start;
|
|
||||||
Config.general.color.scheduleDarkEnd = end;
|
|
||||||
ModeScheduler.checkStartup();
|
|
||||||
PopupManager.requestClose();
|
|
||||||
}
|
|
||||||
onClose: PopupManager.requestClose()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: value => {
|
onAccepted: {
|
||||||
Config.general.color.scheduleDark = value;
|
Config.colors.scheduleDarkStart = startCurrent;
|
||||||
|
Config.colors.scheduleDarkEnd = endCurrent;
|
||||||
|
ModeScheduler.checkStartup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OverlayRow {
|
TimeDialogSelect {
|
||||||
id: hyprsunset
|
|
||||||
|
|
||||||
readonly property string endTime: {
|
readonly property string endTime: {
|
||||||
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
||||||
d.setMinutes(Config.general.color.scheduleHyprsunsetEnd);
|
d.setMinutes(Config.display.nightlight.scheduleEnd);
|
||||||
return Qt.formatTime(d, "hh:mm AP");
|
return Qt.formatTime(d, "hh:mm AP");
|
||||||
}
|
}
|
||||||
readonly property string startTime: {
|
readonly property string startTime: {
|
||||||
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
var d = new Date(0, 0, 0, 0, 0, 0, 0);
|
||||||
d.setMinutes(Config.general.color.scheduleHyprsunsetStart);
|
d.setMinutes(Config.display.nightlight.scheduleStart);
|
||||||
return Qt.formatTime(d, "hh:mm AP");
|
return Qt.formatTime(d, "hh:mm AP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rowButton.checked: Config.display.nightlight.schedule
|
||||||
|
rowButton.onToggled: Config.display.nightlight.schedule = rowButton.checked
|
||||||
|
start: Config.display.nightlight.scheduleStart
|
||||||
|
end: Config.display.nightlight.scheduleEnd
|
||||||
|
first: true
|
||||||
|
settingAnchor: "style-schedule-nightlight"
|
||||||
|
acceptLabel: qsTr("Apply")
|
||||||
|
rootParent: root.flickable
|
||||||
|
header: qsTr("Select time")
|
||||||
|
subtext: qsTr("Nightlight will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
|
||||||
|
label: qsTr("Schedule nightlight")
|
||||||
|
|
||||||
|
onOpenChanged: {
|
||||||
|
root.sState.dimmed = open;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
Config.display.nightlight.scheduleStart = startCurrent;
|
||||||
|
Config.display.nightlight.scheduleEnd = endCurrent;
|
||||||
|
Hyprsunset.checkStartup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
checked: Config.general.color.scheduleHyprsunset
|
|
||||||
|
checked: Config.display.nightlight.useNative
|
||||||
|
settingAnchor: "display-nightlight-useNative"
|
||||||
|
text: qsTr("Use native")
|
||||||
|
subtext: qsTr("Use native shader nightlight for more features")
|
||||||
|
|
||||||
|
onToggled: Config.display.nightlight.useNative = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
SpinRow {
|
||||||
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
|
|
||||||
|
from: 2000
|
||||||
|
settingAnchor: "display-nightlight-temp"
|
||||||
|
stepSize: 100
|
||||||
|
subtext: qsTr("How warm/orange the screen will be. Lower is warmer (Kelvin)")
|
||||||
|
text: qsTr("Temperature")
|
||||||
|
to: 6500
|
||||||
|
value: Config.display.nightlight.temp
|
||||||
|
|
||||||
|
onMoved: v => Config.display.nightlight.temp = v
|
||||||
|
}
|
||||||
|
|
||||||
|
SpinRow {
|
||||||
|
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||||
|
|
||||||
|
from: 0
|
||||||
|
settingAnchor: "display-nightlight-fade-duration"
|
||||||
|
enabled: Config.display.nightlight.useNative
|
||||||
last: true
|
last: true
|
||||||
settingAnchor: "style-schedule-hyprsunset"
|
stepSize: 1
|
||||||
subtext: qsTr("Hyprsunset will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
|
subtext: qsTr("How long it takes for the gradual nightlight to finish (s)")
|
||||||
text: qsTr("Schedule hyprsunset")
|
text: qsTr("Fade duration")
|
||||||
|
to: 60 * 5
|
||||||
|
value: Config.display.nightlight.fadeDuration / 1000
|
||||||
|
|
||||||
popup: Component {
|
onMoved: v => Config.display.nightlight.fadeDuration = v * 1000
|
||||||
TimeInput {
|
|
||||||
object: Config.general.color
|
|
||||||
settings: ["scheduleHyprsunset", "scheduleHyprsunsetStart", "scheduleHyprsunsetEnd"]
|
|
||||||
|
|
||||||
onApplySettings: (start, end) => {
|
|
||||||
Config.general.color.scheduleHyprsunsetStart = start;
|
|
||||||
Config.general.color.scheduleHyprsunsetEnd = end;
|
|
||||||
Hyprsunset.checkStartup();
|
|
||||||
PopupManager.requestClose();
|
|
||||||
}
|
|
||||||
onClose: PopupManager.requestClose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: value => {
|
|
||||||
Config.general.color.scheduleHyprsunset = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool closed: true
|
|
||||||
property Component currentPopup: null
|
|
||||||
|
|
||||||
function requestClose(): void {
|
|
||||||
closed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestOpen(component: Component): void {
|
|
||||||
currentPopup = component;
|
|
||||||
closed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Components
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property bool closing: PopupManager.closed
|
|
||||||
readonly property var currentPopup: PopupManager.currentPopup
|
|
||||||
property bool shouldBeVisible: loader.status === Loader.Ready
|
|
||||||
|
|
||||||
color: Qt.alpha(Colors.palette.m3shadow, 0.3)
|
|
||||||
opacity: shouldBeVisible && !closing ? 1 : 0
|
|
||||||
visible: opacity > 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: if (!visible)
|
|
||||||
PopupManager.currentPopup = null
|
|
||||||
|
|
||||||
CustomMouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
preventStealing: true
|
|
||||||
propagateComposedEvents: false
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
|
|
||||||
const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
|
|
||||||
if (insideItemHeight && insideItemWidth)
|
|
||||||
return;
|
|
||||||
|
|
||||||
PopupManager.requestClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: loader
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: root.currentPopup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +1,24 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import "../../scripts/fzf.js" as Fzf
|
import "../../scripts/fzf.js" as Fzf
|
||||||
|
import "../../scripts/settings-indexer.js" as SettingsIndexer
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import ZShell
|
import ZShell
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import qs.Paths
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var fzfFinder: null
|
property var inverted: ({})
|
||||||
|
property var ranking: ({})
|
||||||
readonly property var highlightCache: ({
|
readonly property var highlightCache: ({
|
||||||
"search": "",
|
"search": "",
|
||||||
"pattern": null
|
"pattern": null
|
||||||
})
|
})
|
||||||
property var inverted: ({})
|
property var fzfFinder: null
|
||||||
property var ranking: ({})
|
readonly property string cachePath: Paths.cache + "/settings-index.json"
|
||||||
|
|
||||||
function highlight(text: string, search: string, colour: color): string {
|
|
||||||
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
||||||
if (search.length === 0)
|
|
||||||
return escaped;
|
|
||||||
|
|
||||||
const cache = root.highlightCache;
|
|
||||||
if (search !== cache.search) {
|
|
||||||
const tokens = tokenize(search);
|
|
||||||
cache.search = search;
|
|
||||||
if (tokens.length === 0)
|
|
||||||
cache.pattern = null;
|
|
||||||
else {
|
|
||||||
const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
||||||
cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pattern = cache.pattern;
|
|
||||||
if (!pattern)
|
|
||||||
return escaped;
|
|
||||||
|
|
||||||
pattern.lastIndex = 0;
|
|
||||||
if (!pattern.test(escaped))
|
|
||||||
return escaped;
|
|
||||||
|
|
||||||
pattern.lastIndex = 0;
|
|
||||||
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lookup(token: string): var {
|
|
||||||
const result = ({});
|
|
||||||
const exact = root.inverted[token] !== undefined;
|
|
||||||
const keys = exact ? [token] : Object.keys(root.inverted).filter(k => k.startsWith(token));
|
|
||||||
for (const key of keys) {
|
|
||||||
const rank = root.ranking[key] ?? ({});
|
|
||||||
for (const id of root.inverted[key]) {
|
|
||||||
const w = rank[id] ?? 0.1;
|
|
||||||
if (result[id] === undefined || w > result[id])
|
|
||||||
result[id] = w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function query(search: string): list<QtObject> {
|
function query(search: string): list<QtObject> {
|
||||||
const tokens = root.tokenize(search);
|
const tokens = root.tokenize(search);
|
||||||
@@ -70,7 +28,7 @@ Singleton {
|
|||||||
const scores = ({});
|
const scores = ({});
|
||||||
const hitCounts = ({});
|
const hitCounts = ({});
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
const matches = root.lookup(token);
|
const matches = root.lookup(token); // { id: weight }
|
||||||
for (const id in matches) {
|
for (const id in matches) {
|
||||||
scores[id] = (scores[id] ?? 0) + matches[id];
|
scores[id] = (scores[id] ?? 0) + matches[id];
|
||||||
hitCounts[id] = (hitCounts[id] ?? 0) + 1;
|
hitCounts[id] = (hitCounts[id] ?? 0) + 1;
|
||||||
@@ -103,13 +61,76 @@ Singleton {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lookup(token: string): var {
|
||||||
|
const result = ({});
|
||||||
|
const exact = root.inverted[token] !== undefined;
|
||||||
|
const keys = exact ? [token] : Object.keys(root.inverted).filter(k => k.startsWith(token));
|
||||||
|
for (const key of keys) {
|
||||||
|
const rank = root.ranking[key] ?? ({});
|
||||||
|
for (const id of root.inverted[key]) {
|
||||||
|
const w = rank[id] ?? 0.1;
|
||||||
|
if (result[id] === undefined || w > result[id])
|
||||||
|
result[id] = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function tokenize(text: string): var {
|
function tokenize(text: string): var {
|
||||||
return text.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 0);
|
return text.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function highlight(text: string, search: string, colour: color): string {
|
||||||
|
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
if (search.length === 0)
|
||||||
|
return escaped;
|
||||||
|
|
||||||
|
const cache = root.highlightCache;
|
||||||
|
if (search !== cache.search) {
|
||||||
|
const tokens = root.tokenize(search);
|
||||||
|
cache.search = search;
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
cache.pattern = null;
|
||||||
|
} else {
|
||||||
|
const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||||||
|
cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = cache.pattern;
|
||||||
|
if (!pattern)
|
||||||
|
return escaped;
|
||||||
|
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
if (!pattern.test(escaped))
|
||||||
|
return escaped;
|
||||||
|
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadIndex(): var {
|
||||||
|
const revision = ZUtils.gitRevision();
|
||||||
|
const cached = ZUtils.readTextFile(cachePath);
|
||||||
|
if (cached) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(cached);
|
||||||
|
if (parsed.version === 3 && revision && parsed.revision === revision && parsed.locale === Qt.locale().name)
|
||||||
|
return parsed;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
const data = SettingsIndexer.buildIndex(`${Quickshell.shellDir}/Modules/Settings`, p => ZUtils.readTextFile(p), (d, s) => ZUtils.listFiles(d, s), (ctx, text) => qsTranslate(ctx, text));
|
||||||
|
data.revision = revision;
|
||||||
|
data.locale = Qt.locale().name;
|
||||||
|
ZUtils.writeTextFile(cachePath, JSON.stringify(data));
|
||||||
|
console.log(`SettingsSearcher: indexed ${data.entries.length} settings (revision ${revision || "unknown"})`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(ZUtils.settingsIndex());
|
const data = root.loadIndex();
|
||||||
entries.model = data.entries;
|
entries.model = data.entries;
|
||||||
root.inverted = data.inverted ?? {};
|
root.inverted = data.inverted ?? {};
|
||||||
root.ranking = data.ranking ?? {};
|
root.ranking = data.ranking ?? {};
|
||||||
@@ -122,6 +143,7 @@ Singleton {
|
|||||||
limit: 25
|
limit: 25
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.warn("SettingsSearcher: failed to build settings index:", e);
|
||||||
entries.model = [];
|
entries.model = [];
|
||||||
root.inverted = {};
|
root.inverted = {};
|
||||||
root.ranking = {};
|
root.ranking = {};
|
||||||
@@ -132,22 +154,25 @@ Singleton {
|
|||||||
Variants {
|
Variants {
|
||||||
id: entries
|
id: entries
|
||||||
|
|
||||||
SettingEntry {
|
SettingEntry {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component SettingEntry: QtObject {
|
component SettingEntry: QtObject {
|
||||||
readonly property string anchor: modelData.anchor ?? ""
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property int pageIdx: modelData.pageIdx
|
||||||
|
readonly property var subPath: modelData.subPath
|
||||||
|
readonly property var targetSubPath: modelData.targetSubPath ?? []
|
||||||
readonly property var crumbIcons: modelData.crumbIcons
|
readonly property var crumbIcons: modelData.crumbIcons
|
||||||
readonly property var crumbLabels: modelData.crumbLabels
|
readonly property var crumbLabels: modelData.crumbLabels
|
||||||
readonly property bool isToggle: togglePath.length > 0
|
|
||||||
required property var modelData
|
|
||||||
readonly property int pageIdx: modelData.pageIdx
|
|
||||||
readonly property string section: modelData.section ?? ""
|
|
||||||
readonly property var subPath: modelData.subPath
|
|
||||||
readonly property string subtext: modelData.subtext ?? ""
|
|
||||||
readonly property string title: modelData.title
|
readonly property string title: modelData.title
|
||||||
|
readonly property string section: modelData.section ?? ""
|
||||||
|
readonly property string subtext: modelData.subtext ?? ""
|
||||||
|
readonly property string anchor: modelData.anchor ?? ""
|
||||||
|
readonly property string icon: modelData.icon ?? ""
|
||||||
|
|
||||||
readonly property string togglePath: modelData.togglePath ?? ""
|
readonly property string togglePath: modelData.togglePath ?? ""
|
||||||
|
readonly property bool isToggle: togglePath.length > 0
|
||||||
readonly property bool toggleValue: {
|
readonly property bool toggleValue: {
|
||||||
if (!isToggle)
|
if (!isToggle)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ QtObject {
|
|||||||
property string searchText
|
property string searchText
|
||||||
property DesktopEntry selectedApp
|
property DesktopEntry selectedApp
|
||||||
property BluetoothDevice selectedBtDevice
|
property BluetoothDevice selectedBtDevice
|
||||||
|
property bool dimmed
|
||||||
property string selectedWallpaperCategory
|
property string selectedWallpaperCategory
|
||||||
property list<int> subPageIdxStack
|
property list<int> subPageIdxStack
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,24 @@
|
|||||||
|
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus CanvasPainter)
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
|
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
||||||
|
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
|
||||||
|
|
||||||
|
if(NOT Cava_FOUND)
|
||||||
|
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(cmake/sensorslib.cmake)
|
||||||
|
|
||||||
|
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||||
|
qt_standard_project_setup(REQUIRES 6.9)
|
||||||
|
|
||||||
|
include(cmake/pch.cmake)
|
||||||
|
include(cmake/qml-module.cmake)
|
||||||
|
|
||||||
|
add_library(zshell-util INTERFACE)
|
||||||
|
target_include_directories(zshell-util INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||||
|
|
||||||
add_subdirectory(ZShell)
|
add_subdirectory(ZShell)
|
||||||
|
|||||||
@@ -1,55 +1,3 @@
|
|||||||
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus CanvasPainter)
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
find_library(SENSORS_LIBRARY NAMES sensors REQUIRED)
|
|
||||||
find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED)
|
|
||||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
|
||||||
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
|
|
||||||
|
|
||||||
if(NOT Cava_FOUND)
|
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT TARGET Sensors::Sensors)
|
|
||||||
add_library(Sensors::Sensors UNKNOWN IMPORTED)
|
|
||||||
set_target_properties(Sensors::Sensors PROPERTIES
|
|
||||||
IMPORTED_LOCATION "${SENSORS_LIBRARY}"
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
|
||||||
qt_standard_project_setup(REQUIRES 6.9)
|
|
||||||
|
|
||||||
function(qml_module arg_TARGET)
|
|
||||||
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES;RESOURCES")
|
|
||||||
qt_add_qml_module(${arg_TARGET}
|
|
||||||
URI ${arg_URI}
|
|
||||||
VERSION 1.0
|
|
||||||
SOURCES ${arg_SOURCES}
|
|
||||||
RESOURCES ${arg_RESOURCES}
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_query_qml_module(${arg_TARGET}
|
|
||||||
URI module_uri
|
|
||||||
VERSION module_version
|
|
||||||
PLUGIN_TARGET module_plugin_target
|
|
||||||
TARGET_PATH module_target_path
|
|
||||||
QMLDIR module_qmldir
|
|
||||||
TYPEINFO module_typeinfo
|
|
||||||
)
|
|
||||||
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
|
|
||||||
install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
|
||||||
install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
|
||||||
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
|
|
||||||
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
|
||||||
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
set_source_files_properties("${SETTINGS_INDEX_JSON}" PROPERTIES QT_RESOURCE_ALIAS "settings-index.json")
|
|
||||||
|
|
||||||
qml_module(ZShell
|
qml_module(ZShell
|
||||||
URI ZShell
|
URI ZShell
|
||||||
SOURCES
|
SOURCES
|
||||||
@@ -60,8 +8,6 @@ qml_module(ZShell
|
|||||||
toaster.hpp toaster.cpp
|
toaster.hpp toaster.cpp
|
||||||
qalculator.hpp qalculator.cpp
|
qalculator.hpp qalculator.cpp
|
||||||
zutils.hpp zutils.cpp
|
zutils.hpp zutils.cpp
|
||||||
RESOURCES
|
|
||||||
"${SETTINGS_INDEX_JSON}"
|
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
@@ -69,9 +15,13 @@ qml_module(ZShell
|
|||||||
Qt::Sql
|
Qt::Sql
|
||||||
Qt::DBus
|
Qt::DBus
|
||||||
PkgConfig::Qalculate
|
PkgConfig::Qalculate
|
||||||
|
zshell-util
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(ZShell PRIVATE ZSHELL_VERSION="${VERSION}")
|
target_compile_definitions(ZShell PRIVATE
|
||||||
|
ZSHELL_VERSION="${VERSION}"
|
||||||
|
GIT_REVISION="${GIT_REVISION}"
|
||||||
|
)
|
||||||
|
|
||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
add_subdirectory(Internal)
|
add_subdirectory(Internal)
|
||||||
@@ -79,3 +29,12 @@ add_subdirectory(Services)
|
|||||||
add_subdirectory(Components)
|
add_subdirectory(Components)
|
||||||
add_subdirectory(Blobs)
|
add_subdirectory(Blobs)
|
||||||
add_subdirectory(Config)
|
add_subdirectory(Config)
|
||||||
|
add_subdirectory(Llm)
|
||||||
|
|
||||||
|
pkg_check_modules(HYPRLAND_PROBE hyprland)
|
||||||
|
if(HYPRLAND_PROBE_FOUND)
|
||||||
|
add_subdirectory(HyprPlugins)
|
||||||
|
else()
|
||||||
|
message(STATUS
|
||||||
|
"hyprland development headers not found; skipping zshell-nightlight plugin")
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
qml_module(ZShell-components
|
qml_module(ZShell-components
|
||||||
URI ZShell.Components
|
URI ZShell.Components
|
||||||
SOURCES
|
SOURCES
|
||||||
lazylistview.hpp lazylistview.cpp
|
lazylistview.hpp lazylistview.cpp
|
||||||
wavyline.hpp wavyline.cpp
|
wavyline.hpp wavyline.cpp
|
||||||
buttonrow.hpp buttonrow.cpp
|
buttonrow.hpp buttonrow.cpp
|
||||||
carouselview.hpp carouselview.cpp
|
carouselview.hpp carouselview.cpp
|
||||||
LIBRARIES
|
wheelinverter.hpp
|
||||||
Qt::Quick
|
LIBRARIES
|
||||||
|
Qt::Quick
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
namespace ZShell::components {
|
||||||
|
|
||||||
|
class WheelInverter : public QQuickItem {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(
|
||||||
|
QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
using QQuickItem::QQuickItem;
|
||||||
|
|
||||||
|
QQuickItem* target() const { return m_target; }
|
||||||
|
|
||||||
|
void setTarget(QQuickItem* target) {
|
||||||
|
if (m_target == target) return;
|
||||||
|
|
||||||
|
if (m_target) m_target->removeEventFilter(this);
|
||||||
|
|
||||||
|
m_target = target;
|
||||||
|
|
||||||
|
if (m_target) m_target->installEventFilter(this);
|
||||||
|
|
||||||
|
emit targetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
class InvertedWheelEvent : public QWheelEvent {
|
||||||
|
public:
|
||||||
|
using QWheelEvent::QWheelEvent;
|
||||||
|
|
||||||
|
bool invertedByWheelInverter = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool eventFilter(QObject* watched, QEvent* event) override {
|
||||||
|
if (watched != m_target || event->type() != QEvent::Wheel)
|
||||||
|
return QQuickItem::eventFilter(watched, event);
|
||||||
|
|
||||||
|
auto* wheel = static_cast<QWheelEvent*>(event);
|
||||||
|
|
||||||
|
const bool inverted = dynamic_cast<InvertedWheelEvent*>(wheel) !=
|
||||||
|
nullptr;
|
||||||
|
|
||||||
|
if (inverted) return false;
|
||||||
|
|
||||||
|
auto* window = m_target ? m_target->window() : nullptr;
|
||||||
|
if (!window) {
|
||||||
|
qInfo() << "[WheelInverter] no window";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPointF windowPos =
|
||||||
|
window->mapFromGlobal(wheel->globalPosition());
|
||||||
|
|
||||||
|
auto* invertedEvent = new InvertedWheelEvent(
|
||||||
|
windowPos,
|
||||||
|
wheel->globalPosition(),
|
||||||
|
-wheel->pixelDelta(),
|
||||||
|
-wheel->angleDelta(),
|
||||||
|
wheel->buttons(),
|
||||||
|
wheel->modifiers(),
|
||||||
|
wheel->phase(),
|
||||||
|
wheel->inverted(),
|
||||||
|
wheel->source(),
|
||||||
|
wheel->pointingDevice());
|
||||||
|
|
||||||
|
invertedEvent->setTimestamp(wheel->timestamp());
|
||||||
|
|
||||||
|
QCoreApplication::postEvent(window, invertedEvent);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void targetChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<QQuickItem> m_target;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::components
|
||||||
@@ -5,6 +5,7 @@ qml_module(ZShell-config
|
|||||||
configobject.hpp configobject.cpp
|
configobject.hpp configobject.cpp
|
||||||
configlist.hpp configlist.cpp
|
configlist.hpp configlist.cpp
|
||||||
config.hpp config.cpp
|
config.hpp config.cpp
|
||||||
|
migration.hpp migration.cpp
|
||||||
anim.hpp anim.cpp
|
anim.hpp anim.cpp
|
||||||
tokens.hpp
|
tokens.hpp
|
||||||
appearance.hpp
|
appearance.hpp
|
||||||
@@ -13,9 +14,11 @@ qml_module(ZShell-config
|
|||||||
clipboard.hpp
|
clipboard.hpp
|
||||||
colors.hpp
|
colors.hpp
|
||||||
dashboard.hpp
|
dashboard.hpp
|
||||||
|
display.hpp
|
||||||
dock.hpp
|
dock.hpp
|
||||||
general.hpp
|
general.hpp
|
||||||
launcher.hpp
|
launcher.hpp
|
||||||
|
llm.hpp
|
||||||
lock.hpp
|
lock.hpp
|
||||||
notifs.hpp
|
notifs.hpp
|
||||||
osd.hpp
|
osd.hpp
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ class Colors : public ConfigObject {
|
|||||||
|
|
||||||
CONFIG_SUBOBJECT(Presets, presets)
|
CONFIG_SUBOBJECT(Presets, presets)
|
||||||
CFG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
|
CFG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
|
||||||
|
CFG_PROPERTY(bool, scheduleDark, false)
|
||||||
|
CFG_PROPERTY(int, scheduleDarkEnd, 600)
|
||||||
|
CFG_PROPERTY(int, scheduleDarkStart, 1140)
|
||||||
|
CFG_PROPERTY(bool, schemeGen, true)
|
||||||
|
CFG_PROPERTY(bool, neovimColors, false)
|
||||||
|
CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
|
||||||
|
CFG_PROPERTY(bool, smart, false)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Colors(QObject* parent = nullptr)
|
explicit Colors(QObject* parent = nullptr)
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
#include "clipboard.hpp"
|
#include "clipboard.hpp"
|
||||||
#include "colors.hpp"
|
#include "colors.hpp"
|
||||||
#include "dashboard.hpp"
|
#include "dashboard.hpp"
|
||||||
|
#include "display.hpp"
|
||||||
#include "dock.hpp"
|
#include "dock.hpp"
|
||||||
#include "general.hpp"
|
#include "general.hpp"
|
||||||
#include "launcher.hpp"
|
#include "launcher.hpp"
|
||||||
|
#include "llm.hpp"
|
||||||
#include "lock.hpp"
|
#include "lock.hpp"
|
||||||
|
#include "migration.hpp"
|
||||||
#include "notifs.hpp"
|
#include "notifs.hpp"
|
||||||
#include "osd.hpp"
|
#include "osd.hpp"
|
||||||
#include "screenshot.hpp"
|
#include "screenshot.hpp"
|
||||||
@@ -33,6 +36,8 @@
|
|||||||
|
|
||||||
namespace ZShell::config {
|
namespace ZShell::config {
|
||||||
|
|
||||||
|
Config* Config::s_instance = nullptr;
|
||||||
|
|
||||||
Config::Config(QObject* parent)
|
Config::Config(QObject* parent)
|
||||||
: ConfigObject(parent)
|
: ConfigObject(parent)
|
||||||
, m_appearance(new Appearance(this))
|
, m_appearance(new Appearance(this))
|
||||||
@@ -41,9 +46,11 @@ Config::Config(QObject* parent)
|
|||||||
, m_clipboard(new Clipboard(this))
|
, m_clipboard(new Clipboard(this))
|
||||||
, m_colors(new Colors(this))
|
, m_colors(new Colors(this))
|
||||||
, m_dashboard(new Dashboard(this))
|
, m_dashboard(new Dashboard(this))
|
||||||
|
, m_display(new Display(this))
|
||||||
, m_dock(new Dock(this))
|
, m_dock(new Dock(this))
|
||||||
, m_general(new General(this))
|
, m_general(new General(this))
|
||||||
, m_launcher(new Launcher(this))
|
, m_launcher(new Launcher(this))
|
||||||
|
, m_llm(new Llm(this))
|
||||||
, m_lock(new Lock(this))
|
, m_lock(new Lock(this))
|
||||||
, m_notifs(new Notifs(this))
|
, m_notifs(new Notifs(this))
|
||||||
, m_osd(new Osd(this))
|
, m_osd(new Osd(this))
|
||||||
@@ -51,6 +58,7 @@ Config::Config(QObject* parent)
|
|||||||
, m_services(new Services(this))
|
, m_services(new Services(this))
|
||||||
, m_sidebar(new Sidebar(this))
|
, m_sidebar(new Sidebar(this))
|
||||||
, m_utilities(new Utilities(this)) {
|
, m_utilities(new Utilities(this)) {
|
||||||
|
s_instance = this;
|
||||||
connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave);
|
connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave);
|
||||||
|
|
||||||
m_saveTimer.setSingleShot(true);
|
m_saveTimer.setSingleShot(true);
|
||||||
@@ -81,8 +89,13 @@ Config::Config(QObject* parent)
|
|||||||
m_firstLoadDone = true;
|
m_firstLoadDone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Config* Config::instance() {
|
||||||
|
return s_instance;
|
||||||
|
}
|
||||||
|
|
||||||
Config* Config::create(QQmlEngine*, QJSEngine*) {
|
Config* Config::create(QQmlEngine*, QJSEngine*) {
|
||||||
return new Config();
|
if (!s_instance) s_instance = new Config();
|
||||||
|
return s_instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Config::filePath() const {
|
QString Config::filePath() const {
|
||||||
@@ -100,11 +113,14 @@ void Config::loadSync() {
|
|||||||
QJsonObject before;
|
QJsonObject before;
|
||||||
|
|
||||||
bool existed = false;
|
bool existed = false;
|
||||||
|
bool migrated = false;
|
||||||
|
|
||||||
if (f.open(QIODevice::ReadOnly)) {
|
if (f.open(QIODevice::ReadOnly)) {
|
||||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
const auto doc = QJsonDocument::fromJson(f.readAll());
|
||||||
if (doc.isObject()) {
|
if (doc.isObject()) {
|
||||||
before = doc.object();
|
const QJsonObject raw = doc.object();
|
||||||
|
before = ConfigMigrations::apply(raw);
|
||||||
|
migrated = before != raw;
|
||||||
existed = true;
|
existed = true;
|
||||||
} else {
|
} else {
|
||||||
qInfo() << "Config: existing config at" << filePath()
|
qInfo() << "Config: existing config at" << filePath()
|
||||||
@@ -119,7 +135,7 @@ void Config::loadSync() {
|
|||||||
m_loading = false;
|
m_loading = false;
|
||||||
|
|
||||||
const auto after = toJson().toObject();
|
const auto after = toJson().toObject();
|
||||||
if (!existed || after != before) saveNow();
|
if (!existed || migrated || after != before) saveNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::updateWatch() {
|
void Config::updateWatch() {
|
||||||
@@ -165,16 +181,18 @@ void Config::loadAsync() {
|
|||||||
if (f.open(QIODevice::ReadOnly)) {
|
if (f.open(QIODevice::ReadOnly)) {
|
||||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
const auto doc = QJsonDocument::fromJson(f.readAll());
|
||||||
const bool valid = doc.isObject();
|
const bool valid = doc.isObject();
|
||||||
const QJsonObject before = valid ? doc.object() : QJsonObject();
|
const QJsonObject raw = valid ? doc.object() : QJsonObject();
|
||||||
|
const QJsonObject before = ConfigMigrations::apply(raw);
|
||||||
|
const bool migrated = valid && before != raw;
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, valid, before]() {
|
[this, valid, migrated, before]() {
|
||||||
loadFromJson(QJsonValue(before));
|
loadFromJson(QJsonValue(before));
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
|
|
||||||
const auto after = toJson().toObject();
|
const auto after = toJson().toObject();
|
||||||
if (!valid || after != before) saveNow();
|
if (!valid || migrated || after != before) saveNow();
|
||||||
|
|
||||||
if (m_reloadPending) m_reloadTimer.start();
|
if (m_reloadPending) m_reloadTimer.start();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ class Bar;
|
|||||||
class Clipboard;
|
class Clipboard;
|
||||||
class Colors;
|
class Colors;
|
||||||
class Dashboard;
|
class Dashboard;
|
||||||
|
class Display;
|
||||||
class Dock;
|
class Dock;
|
||||||
class General;
|
class General;
|
||||||
class Launcher;
|
class Launcher;
|
||||||
|
class Llm;
|
||||||
class Lock;
|
class Lock;
|
||||||
class Notifs;
|
class Notifs;
|
||||||
class Osd;
|
class Osd;
|
||||||
@@ -43,9 +45,11 @@ class Config : public ConfigObject {
|
|||||||
Q_MOC_INCLUDE("clipboard.hpp")
|
Q_MOC_INCLUDE("clipboard.hpp")
|
||||||
Q_MOC_INCLUDE("colors.hpp")
|
Q_MOC_INCLUDE("colors.hpp")
|
||||||
Q_MOC_INCLUDE("dashboard.hpp")
|
Q_MOC_INCLUDE("dashboard.hpp")
|
||||||
|
Q_MOC_INCLUDE("display.hpp")
|
||||||
Q_MOC_INCLUDE("dock.hpp")
|
Q_MOC_INCLUDE("dock.hpp")
|
||||||
Q_MOC_INCLUDE("general.hpp")
|
Q_MOC_INCLUDE("general.hpp")
|
||||||
Q_MOC_INCLUDE("launcher.hpp")
|
Q_MOC_INCLUDE("launcher.hpp")
|
||||||
|
Q_MOC_INCLUDE("llm.hpp")
|
||||||
Q_MOC_INCLUDE("lock.hpp")
|
Q_MOC_INCLUDE("lock.hpp")
|
||||||
Q_MOC_INCLUDE("notifs.hpp")
|
Q_MOC_INCLUDE("notifs.hpp")
|
||||||
Q_MOC_INCLUDE("osd.hpp")
|
Q_MOC_INCLUDE("osd.hpp")
|
||||||
@@ -60,9 +64,11 @@ class Config : public ConfigObject {
|
|||||||
CONFIG_SUBOBJECT(Clipboard, clipboard)
|
CONFIG_SUBOBJECT(Clipboard, clipboard)
|
||||||
CONFIG_SUBOBJECT(Colors, colors)
|
CONFIG_SUBOBJECT(Colors, colors)
|
||||||
CONFIG_SUBOBJECT(Dashboard, dashboard)
|
CONFIG_SUBOBJECT(Dashboard, dashboard)
|
||||||
|
CONFIG_SUBOBJECT(Display, display)
|
||||||
CONFIG_SUBOBJECT(Dock, dock)
|
CONFIG_SUBOBJECT(Dock, dock)
|
||||||
CONFIG_SUBOBJECT(General, general)
|
CONFIG_SUBOBJECT(General, general)
|
||||||
CONFIG_SUBOBJECT(Launcher, launcher)
|
CONFIG_SUBOBJECT(Launcher, launcher)
|
||||||
|
CONFIG_SUBOBJECT(Llm, llm)
|
||||||
CONFIG_SUBOBJECT(Lock, lock)
|
CONFIG_SUBOBJECT(Lock, lock)
|
||||||
CONFIG_SUBOBJECT(Notifs, notifs)
|
CONFIG_SUBOBJECT(Notifs, notifs)
|
||||||
CONFIG_SUBOBJECT(Osd, osd)
|
CONFIG_SUBOBJECT(Osd, osd)
|
||||||
@@ -74,6 +80,7 @@ class Config : public ConfigObject {
|
|||||||
public:
|
public:
|
||||||
explicit Config(QObject* parent = nullptr);
|
explicit Config(QObject* parent = nullptr);
|
||||||
static Config* create(QQmlEngine*, QJSEngine*);
|
static Config* create(QQmlEngine*, QJSEngine*);
|
||||||
|
[[nodiscard]] static Config* instance();
|
||||||
|
|
||||||
Q_INVOKABLE void load();
|
Q_INVOKABLE void load();
|
||||||
Q_INVOKABLE void saveNow();
|
Q_INVOKABLE void saveNow();
|
||||||
@@ -103,6 +110,8 @@ class Config : public ConfigObject {
|
|||||||
bool m_loading = false;
|
bool m_loading = false;
|
||||||
bool m_firstLoadDone = false;
|
bool m_firstLoadDone = false;
|
||||||
QFuture<void> m_loadFuture;
|
QFuture<void> m_loadFuture;
|
||||||
|
|
||||||
|
static Config* s_instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::config
|
} // namespace ZShell::config
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "configobject.hpp"
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
namespace ZShell::config {
|
||||||
|
|
||||||
|
class Nightlight : public ConfigObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ANONYMOUS
|
||||||
|
|
||||||
|
CFG_PROPERTY(bool, schedule, true)
|
||||||
|
CFG_PROPERTY(int, scheduleStart, 1200)
|
||||||
|
CFG_PROPERTY(int, scheduleEnd, 570)
|
||||||
|
CFG_PROPERTY(bool, useNative, true)
|
||||||
|
CFG_PROPERTY(int, fadeDuration, 2000)
|
||||||
|
CFG_PROPERTY(int, temp, 2600)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Nightlight(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Display : public ConfigObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ANONYMOUS
|
||||||
|
|
||||||
|
CONFIG_SUBOBJECT(Nightlight, nightlight)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Display(QObject* parent = nullptr)
|
||||||
|
: ConfigObject(parent), m_nightlight(new Nightlight(this)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::config
|
||||||
@@ -91,26 +91,6 @@ class Battery : public ConfigObject {
|
|||||||
explicit Battery(QObject* parent = nullptr) : ConfigObject(parent) {}
|
explicit Battery(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class ColorSettings : public ConfigObject {
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ANONYMOUS
|
|
||||||
|
|
||||||
CFG_PROPERTY(int, hyprsunsetTemp, 2600)
|
|
||||||
CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
|
|
||||||
CFG_PROPERTY(bool, neovimColors, false)
|
|
||||||
CFG_PROPERTY(bool, scheduleDark, false)
|
|
||||||
CFG_PROPERTY(int, scheduleDarkEnd, 600)
|
|
||||||
CFG_PROPERTY(int, scheduleDarkStart, 1140)
|
|
||||||
CFG_PROPERTY(bool, scheduleHyprsunset, true)
|
|
||||||
CFG_PROPERTY(int, scheduleHyprsunsetEnd, 570)
|
|
||||||
CFG_PROPERTY(int, scheduleHyprsunsetStart, 1200)
|
|
||||||
CFG_PROPERTY(bool, schemeGeneration, true)
|
|
||||||
CFG_PROPERTY(bool, smart, false)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ColorSettings(QObject* parent = nullptr) : ConfigObject(parent) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class Idle : public ConfigObject {
|
class Idle : public ConfigObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ANONYMOUS
|
QML_ANONYMOUS
|
||||||
@@ -135,7 +115,6 @@ class General : public ConfigObject {
|
|||||||
|
|
||||||
CONFIG_SUBOBJECT(Apps, apps)
|
CONFIG_SUBOBJECT(Apps, apps)
|
||||||
CONFIG_SUBOBJECT(Battery, battery)
|
CONFIG_SUBOBJECT(Battery, battery)
|
||||||
CONFIG_SUBOBJECT(ColorSettings, color)
|
|
||||||
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
|
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
|
||||||
CFG_PROPERTY(bool, desktopIcons, true)
|
CFG_PROPERTY(bool, desktopIcons, true)
|
||||||
CONFIG_SUBOBJECT(Idle, idle)
|
CONFIG_SUBOBJECT(Idle, idle)
|
||||||
@@ -151,7 +130,6 @@ class General : public ConfigObject {
|
|||||||
: ConfigObject(parent)
|
: ConfigObject(parent)
|
||||||
, m_apps(new Apps(this))
|
, m_apps(new Apps(this))
|
||||||
, m_battery(new Battery(this))
|
, m_battery(new Battery(this))
|
||||||
, m_color(new ColorSettings(this))
|
|
||||||
, m_idle(new Idle(this)) {}
|
, m_idle(new Idle(this)) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "configobject.hpp"
|
||||||
|
#include <qhashfunctions.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
namespace ZShell::config {
|
||||||
|
|
||||||
|
class LlmAppearance : public ConfigObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ANONYMOUS
|
||||||
|
|
||||||
|
CFG_PROPERTY(QString, scheme, QStringLiteral("tokyoNight"))
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LlmAppearance(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Llm : public ConfigObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ANONYMOUS
|
||||||
|
|
||||||
|
CFG_PROPERTY(QString, endpoint, "http://localhost:8080")
|
||||||
|
CFG_PROPERTY(QString, model, "")
|
||||||
|
CFG_PROPERTY(double, temperature, 0.7)
|
||||||
|
CFG_PROPERTY(bool, tools, true)
|
||||||
|
CFG_PROPERTY(bool, enabled, true)
|
||||||
|
CONFIG_SUBOBJECT(LlmAppearance, appearance)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Llm(QObject* parent = nullptr)
|
||||||
|
: ConfigObject(parent), m_appearance(new LlmAppearance(this)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::config
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#include "migration.hpp"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
namespace ZShell::config {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// A stored value is "missing" if the path does not resolve to an entry.
|
||||||
|
// Note: default-constructed QJsonValue is Null, not Undefined.
|
||||||
|
bool isMissing(const QJsonValue& v) {
|
||||||
|
return v.isUndefined() || v.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue valueAt(const QJsonObject& obj, const QStringList& path) {
|
||||||
|
QJsonObject cur = obj;
|
||||||
|
|
||||||
|
for (int i = 0; i < path.size() - 1; ++i) {
|
||||||
|
if (!cur.value(path.at(i)).isObject()) return QJsonValue::Undefined;
|
||||||
|
cur = cur.value(path.at(i)).toObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString key = path.last();
|
||||||
|
if (!cur.contains(key)) return QJsonValue::Undefined;
|
||||||
|
return cur.value(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject placeAt(QJsonObject obj, QStringList path, const QJsonValue& value) {
|
||||||
|
const QString head = path.takeFirst();
|
||||||
|
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
obj[head] = value;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject child = obj.value(head).isObject() ? obj.value(head).toObject()
|
||||||
|
: QJsonObject();
|
||||||
|
obj[head] = placeAt(child, path, value);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes `path` from `obj`. Returns true if a value was removed.
|
||||||
|
// Parent objects left empty are removed as well.
|
||||||
|
bool removeAt(QJsonObject& obj, const QStringList& path) {
|
||||||
|
const QString head = path.first();
|
||||||
|
|
||||||
|
if (path.size() == 1) {
|
||||||
|
if (!obj.contains(head)) return false;
|
||||||
|
obj.remove(head);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj.value(head).isObject()) return false;
|
||||||
|
|
||||||
|
QJsonObject child = obj.value(head).toObject();
|
||||||
|
const bool removed = removeAt(child, path.mid(1));
|
||||||
|
|
||||||
|
if (child.isEmpty())
|
||||||
|
obj.remove(head);
|
||||||
|
else
|
||||||
|
obj[head] = child;
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const QList<ConfigMigrationRule>& ConfigMigrations::rules() {
|
||||||
|
static const QList<ConfigMigrationRule> s_rules = {
|
||||||
|
migrate(
|
||||||
|
"general.color.scheduleHyprsunset", "display.nightlight.schedule"),
|
||||||
|
migrate(
|
||||||
|
"general.color.scheduleHyprsunsetStart",
|
||||||
|
"display.nightlight.scheduleStart"),
|
||||||
|
migrate(
|
||||||
|
"general.color.scheduleHyprsunsetEnd",
|
||||||
|
"display.nightlight.scheduleEnd"),
|
||||||
|
migrate("general.color.scheduleDark", "colors.scheduleDark"),
|
||||||
|
migrate("general.color.scheduleDarkStart", "colors.scheduleDarkStart"),
|
||||||
|
migrate("general.color.scheduleDarkEnd", "colors.scheduleDarkEnd"),
|
||||||
|
migrate("general.color.schemeGeneration", "colors.schemeGen"),
|
||||||
|
migrate(
|
||||||
|
"general.color.useNativeNightlight",
|
||||||
|
"display.nightlight.useNative"),
|
||||||
|
migrate(
|
||||||
|
"general.color.nativeFadeDuration",
|
||||||
|
"display.nightlight.fadeDuration"),
|
||||||
|
migrate("general.color.hyprsunsetTemp", "display.nightlight.temp"),
|
||||||
|
migrate("general.color.neovimColors", "colors.neovimColors"),
|
||||||
|
migrate("general.color.mode", "colors.mode"),
|
||||||
|
migrate("general.color.smart", "colors.smart"),
|
||||||
|
};
|
||||||
|
return s_rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject ConfigMigrations::apply(const QJsonObject& json) {
|
||||||
|
QJsonObject obj = json;
|
||||||
|
|
||||||
|
for (const auto& rule : rules()) {
|
||||||
|
const QStringList from =
|
||||||
|
rule.from.split(QLatin1Char('.'), Qt::SkipEmptyParts);
|
||||||
|
const QStringList to =
|
||||||
|
rule.to.split(QLatin1Char('.'), Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
if (from.isEmpty() || to.isEmpty()) continue;
|
||||||
|
|
||||||
|
const QJsonValue oldValue = valueAt(obj, from);
|
||||||
|
if (isMissing(oldValue)) continue;
|
||||||
|
|
||||||
|
const bool targetExists = !isMissing(valueAt(obj, to));
|
||||||
|
if (!targetExists) obj = placeAt(obj, to, oldValue);
|
||||||
|
|
||||||
|
if (removeAt(obj, from)) {
|
||||||
|
qInfo() << "Config migration:" << rule.from << "->" << rule.to
|
||||||
|
<< (targetExists ? "(target already set, old value dropped)"
|
||||||
|
: "(value moved)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::config
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace ZShell::config {
|
||||||
|
|
||||||
|
// A single migration rule: move the value stored at the dotted `from` path
|
||||||
|
// to the dotted `to` path, then delete the old key (and any parent objects
|
||||||
|
// left empty).
|
||||||
|
//
|
||||||
|
// Rules are applied to the raw JSON file on every load, before the schema
|
||||||
|
// loads it. They are idempotent: once the old key is gone the rule never
|
||||||
|
// fires again, so no version stamping is needed. Old rules can stay in the
|
||||||
|
// table forever.
|
||||||
|
//
|
||||||
|
// WARNING: the target path must exist in the schema (i.e. a ConfigObject
|
||||||
|
// property somewhere in the tree). If it does not, the migrated value is
|
||||||
|
// loaded as an unknown key and silently dropped on the next save.
|
||||||
|
struct ConfigMigrationRule {
|
||||||
|
QString from;
|
||||||
|
QString to;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper for writing rules: migrate("general.color.foo", "display.bar.foo")
|
||||||
|
inline ConfigMigrationRule migrate(const QString& from, const QString& to) {
|
||||||
|
return {from, to};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigMigrations {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] static const QList<ConfigMigrationRule>& rules();
|
||||||
|
|
||||||
|
// Applies all rules to `json`, returning the migrated object.
|
||||||
|
[[nodiscard]] static QJsonObject apply(const QJsonObject& json);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::config
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
pkg_check_modules(HYPRLAND REQUIRED hyprland)
|
||||||
|
|
||||||
|
add_library(zshell-nightlight SHARED main.cpp)
|
||||||
|
|
||||||
|
target_compile_features(zshell-nightlight PRIVATE cxx_std_23)
|
||||||
|
target_include_directories(zshell-nightlight PRIVATE ${HYPRLAND_INCLUDE_DIRS})
|
||||||
|
target_link_directories(zshell-nightlight PRIVATE ${HYPRLAND_LIBRARY_DIRS})
|
||||||
|
target_compile_options(zshell-nightlight PRIVATE ${HYPRLAND_CFLAGS_OTHER})
|
||||||
|
|
||||||
|
target_compile_options(zshell-nightlight PRIVATE -fno-gnu-unique)
|
||||||
|
|
||||||
|
install(TARGETS zshell-nightlight LIBRARY DESTINATION "${INSTALL_LIBDIR}/plugins")
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace ZShell::services {
|
||||||
|
|
||||||
|
inline std::array<float, 3> kelvinToGain(int kelvin) {
|
||||||
|
const double t = std::clamp(kelvin, 1000, 40000) / 100.0;
|
||||||
|
double r, g, b;
|
||||||
|
|
||||||
|
r = t <= 66.0
|
||||||
|
? 1.0
|
||||||
|
: std::clamp(
|
||||||
|
1.29293618606 * std::pow(t - 60.0, -0.1332047592), 0.0, 1.0);
|
||||||
|
g = t <= 66.0
|
||||||
|
? std::clamp(0.39008157876 * std::log(t) - 0.63184144378, 0.0, 1.0)
|
||||||
|
: std::clamp(
|
||||||
|
1.12989086089 * std::pow(t - 60.0, -0.0755148492), 0.0, 1.0);
|
||||||
|
b = t >= 66.0
|
||||||
|
? 1.0
|
||||||
|
: (t <= 19.0
|
||||||
|
? 0.0
|
||||||
|
: std::clamp(
|
||||||
|
0.54320678911 * std::log(t - 10.0) - 1.19625408914,
|
||||||
|
0.0,
|
||||||
|
1.0));
|
||||||
|
|
||||||
|
return {static_cast<float>(r), static_cast<float>(g), static_cast<float>(b)};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::services
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
#include <any>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define private public
|
||||||
|
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||||
|
#include <hyprland/src/render/OpenGL.hpp>
|
||||||
|
#include <hyprland/src/render/Renderer.hpp>
|
||||||
|
#undef private
|
||||||
|
|
||||||
|
#include <hyprland/src/managers/EventManager.hpp>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <lua.h>
|
||||||
|
#include <lauxlib.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "colortemp.hpp"
|
||||||
|
|
||||||
|
using ZShell::services::kelvinToGain;
|
||||||
|
|
||||||
|
using Render::IFramebuffer;
|
||||||
|
using Render::GL::CHyprOpenGLImpl;
|
||||||
|
using Render::GL::g_pHyprOpenGL;
|
||||||
|
|
||||||
|
inline HANDLE PHANDLE = nullptr;
|
||||||
|
|
||||||
|
static std::filesystem::path g_runtimeDir{"/tmp"};
|
||||||
|
static std::filesystem::path g_cacheBase{"/tmp/.cache"};
|
||||||
|
|
||||||
|
static void initEnvPaths() {
|
||||||
|
if (const char* env = getenv("XDG_RUNTIME_DIR")) g_runtimeDir = env;
|
||||||
|
|
||||||
|
if (const char* env = getenv("XDG_CACHE_HOME")) {
|
||||||
|
if (*env) g_cacheBase = env;
|
||||||
|
} else if (const char* home = getenv("HOME")) {
|
||||||
|
g_cacheBase = std::filesystem::path(home) / ".cache";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::ofstream& debugLog() {
|
||||||
|
static std::ofstream log(
|
||||||
|
g_runtimeDir / "zshell-nightlight-debug.log", std::ios::app);
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::mutex g_logMutex;
|
||||||
|
static std::atomic<uint64_t> g_logSeq{0};
|
||||||
|
|
||||||
|
static void dlog(const std::string& msg) {
|
||||||
|
std::lock_guard<std::mutex> lk(g_logMutex);
|
||||||
|
auto& log = debugLog();
|
||||||
|
log << "[" << g_logSeq++ << "][tid=" << std::this_thread::get_id() << "] "
|
||||||
|
<< msg << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int BAKED_STEPS = 24;
|
||||||
|
|
||||||
|
static std::string glslFloat(float v) {
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << std::fixed << std::setprecision(6) << v;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::filesystem::path cacheDir() {
|
||||||
|
return g_cacheBase / "zshell" / "night-light";
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::filesystem::path shaderPath() {
|
||||||
|
return cacheDir() / "nightlight.frag";
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::filesystem::path bakedShaderPath(const std::array<float, 3>& gain) {
|
||||||
|
return cacheDir() /
|
||||||
|
("nightlight-" + glslFloat(gain[0]) + "-" + glslFloat(gain[1]) +
|
||||||
|
"-" + glslFloat(gain[2]) + ".frag");
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string defaultShaderSource() {
|
||||||
|
return "#version 100\n"
|
||||||
|
"precision highp float;\n"
|
||||||
|
"varying vec2 v_texcoord;\n"
|
||||||
|
"uniform sampler2D tex;\n"
|
||||||
|
"uniform vec3 tint;\n"
|
||||||
|
"void main() {\n"
|
||||||
|
" vec4 pixColor = texture2D(tex, v_texcoord);\n"
|
||||||
|
" gl_FragColor = vec4(pixColor.rgb * tint, pixColor.a);\n"
|
||||||
|
"}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string bakedShaderSource(const std::array<float, 3>& gain) {
|
||||||
|
std::ostringstream src;
|
||||||
|
src << "#version 100\n"
|
||||||
|
"precision highp float;\n"
|
||||||
|
"varying vec2 v_texcoord;\n"
|
||||||
|
"uniform sampler2D tex;\n"
|
||||||
|
"const vec3 GAIN = vec3("
|
||||||
|
<< glslFloat(gain[0]) << "," << glslFloat(gain[1]) << ","
|
||||||
|
<< glslFloat(gain[2])
|
||||||
|
<< ");\n"
|
||||||
|
"void main() {\n"
|
||||||
|
" vec4 pixColor = texture2D(tex, v_texcoord);\n"
|
||||||
|
" gl_FragColor = vec4(pixColor.rgb * GAIN, pixColor.a);\n"
|
||||||
|
"}\n";
|
||||||
|
return src.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ensureShaderFile(
|
||||||
|
const std::filesystem::path& path, const std::string& source) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::is_regular_file(path, ec)) return;
|
||||||
|
std::filesystem::create_directories(path.parent_path(), ec);
|
||||||
|
std::ofstream out(path, std::ios::trunc);
|
||||||
|
out << source;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool g_active = false;
|
||||||
|
static bool g_uniformMode = false;
|
||||||
|
static bool g_modeDecided = false;
|
||||||
|
static std::array<float, 3> g_currentGain{1.f, 1.f, 1.f};
|
||||||
|
|
||||||
|
static int g_currentKelvin = 6500;
|
||||||
|
static int g_lastKelvin = 6500;
|
||||||
|
|
||||||
|
struct STransition {
|
||||||
|
bool active = false;
|
||||||
|
std::chrono::steady_clock::time_point start;
|
||||||
|
float durationSec = 0.4f;
|
||||||
|
std::array<float, 3> fromGain{1.f, 1.f, 1.f};
|
||||||
|
std::array<float, 3> toGain{1.f, 1.f, 1.f};
|
||||||
|
int lastBakedStep = -1;
|
||||||
|
};
|
||||||
|
static STransition g_transition;
|
||||||
|
|
||||||
|
static void postStateEvent(bool enabled, bool animating);
|
||||||
|
|
||||||
|
static bool gainIsNeutral(const std::array<float, 3>& gain) {
|
||||||
|
for (float v : gain)
|
||||||
|
if (std::abs(v - 1.f) > 0.001f) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CFunctionHook* g_pBeginHook = nullptr;
|
||||||
|
using origBegin = void (*)(
|
||||||
|
CHyprOpenGLImpl*,
|
||||||
|
PHLMONITOR,
|
||||||
|
const CRegion&,
|
||||||
|
SP<IFramebuffer>,
|
||||||
|
std::optional<CRegion>);
|
||||||
|
|
||||||
|
static CFunctionHook* g_pSaveMirrorHook = nullptr;
|
||||||
|
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
|
||||||
|
|
||||||
|
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
|
||||||
|
const bool saved = thisptr->m_applyFinalShader;
|
||||||
|
thisptr->m_applyFinalShader = false;
|
||||||
|
const bool result = (*reinterpret_cast<origSaveMirror>(
|
||||||
|
g_pSaveMirrorHook->m_original))(thisptr, box);
|
||||||
|
thisptr->m_applyFinalShader = saved;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void installShader() {
|
||||||
|
auto* gl = g_pHyprOpenGL.get();
|
||||||
|
if (!gl) return;
|
||||||
|
|
||||||
|
ensureShaderFile(shaderPath(), defaultShaderSource());
|
||||||
|
gl->applyScreenShader(shaderPath().string());
|
||||||
|
|
||||||
|
auto* shader = gl->m_finalScreenShader.get();
|
||||||
|
if (shader && shader->program() >= 1 &&
|
||||||
|
shader->getUniformLocation(SHADER_TINT) != -1) {
|
||||||
|
g_uniformMode = true;
|
||||||
|
} else {
|
||||||
|
g_uniformMode = false;
|
||||||
|
if (!g_modeDecided)
|
||||||
|
dlog(
|
||||||
|
"shader has no 'tint' uniform, falling back to "
|
||||||
|
"per-step recompiled shaders");
|
||||||
|
}
|
||||||
|
g_modeDecided = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setUniformGain(const std::array<float, 3>& gain) {
|
||||||
|
auto* gl = g_pHyprOpenGL.get();
|
||||||
|
auto* shader = gl ? gl->m_finalScreenShader.get() : nullptr;
|
||||||
|
if (!gl || !shader) return;
|
||||||
|
|
||||||
|
gl->useShader(gl->m_finalScreenShader);
|
||||||
|
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hkBegin(
|
||||||
|
CHyprOpenGLImpl* thisptr,
|
||||||
|
PHLMONITOR mon,
|
||||||
|
const CRegion& damage,
|
||||||
|
SP<IFramebuffer> fb,
|
||||||
|
std::optional<CRegion> finalDamage) {
|
||||||
|
(*reinterpret_cast<origBegin>(
|
||||||
|
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
|
||||||
|
|
||||||
|
if (fb) {
|
||||||
|
auto* renderer = g_pHyprRenderer.get();
|
||||||
|
if (renderer) renderer->m_renderData.blockScreenShader = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_active) return;
|
||||||
|
|
||||||
|
auto* gl = g_pHyprOpenGL.get();
|
||||||
|
if (!gl) return;
|
||||||
|
|
||||||
|
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
|
||||||
|
installShader();
|
||||||
|
if (!gl->m_finalScreenShader.get() ||
|
||||||
|
gl->m_finalScreenShader->program() < 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (g_transition.active) {
|
||||||
|
auto* monPtr = mon.get();
|
||||||
|
if (!monPtr) return;
|
||||||
|
const float elapsed =
|
||||||
|
std::chrono::duration<float>(
|
||||||
|
std::chrono::steady_clock::now() - g_transition.start)
|
||||||
|
.count();
|
||||||
|
const float t =
|
||||||
|
std::clamp(elapsed / g_transition.durationSec, 0.f, 1.f);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 3; ++i)
|
||||||
|
g_currentGain[i] =
|
||||||
|
g_transition.fromGain[i] +
|
||||||
|
(g_transition.toGain[i] - g_transition.fromGain[i]) * t;
|
||||||
|
|
||||||
|
if (g_uniformMode) {
|
||||||
|
setUniformGain(g_currentGain);
|
||||||
|
} else {
|
||||||
|
const int step = static_cast<int>(t * BAKED_STEPS);
|
||||||
|
if (step != g_transition.lastBakedStep) {
|
||||||
|
g_transition.lastBakedStep = step;
|
||||||
|
ensureShaderFile(
|
||||||
|
bakedShaderPath(g_currentGain),
|
||||||
|
bakedShaderSource(g_currentGain));
|
||||||
|
gl->applyScreenShader(bakedShaderPath(g_currentGain).string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t >= 1.f) {
|
||||||
|
g_transition.active = false;
|
||||||
|
if (gainIsNeutral(g_currentGain)) {
|
||||||
|
g_active = false;
|
||||||
|
gl->applyScreenShader("");
|
||||||
|
}
|
||||||
|
postStateEvent(g_active, false);
|
||||||
|
} else {
|
||||||
|
monPtr->m_forceFullFrames = 2;
|
||||||
|
monPtr->scheduleFrame();
|
||||||
|
}
|
||||||
|
} else if (g_uniformMode) {
|
||||||
|
setUniformGain(g_currentGain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int KELVIN_MIN = 2000;
|
||||||
|
static constexpr int KELVIN_MAX = 6500;
|
||||||
|
|
||||||
|
static int luaNightlightSet(lua_State* L) {
|
||||||
|
const int rawKelvin = static_cast<int>(luaL_checknumber(L, 1));
|
||||||
|
const float durationSec =
|
||||||
|
lua_gettop(L) >= 2 ? static_cast<float>(luaL_checknumber(L, 2)) : 0.4f;
|
||||||
|
|
||||||
|
const int targetKelvin = std::clamp(rawKelvin, KELVIN_MIN, KELVIN_MAX);
|
||||||
|
if (targetKelvin != rawKelvin)
|
||||||
|
dlog(
|
||||||
|
"clamped " + std::to_string(rawKelvin) + "K to " +
|
||||||
|
std::to_string(targetKelvin) + "K");
|
||||||
|
|
||||||
|
g_transition.fromGain = g_currentGain;
|
||||||
|
g_transition.toGain = kelvinToGain(targetKelvin);
|
||||||
|
g_transition.active = true;
|
||||||
|
g_transition.start = std::chrono::steady_clock::now();
|
||||||
|
g_transition.durationSec = durationSec;
|
||||||
|
g_transition.lastBakedStep = -1;
|
||||||
|
g_active = true;
|
||||||
|
|
||||||
|
g_lastKelvin = g_currentKelvin;
|
||||||
|
g_currentKelvin = targetKelvin;
|
||||||
|
|
||||||
|
postStateEvent(true, true);
|
||||||
|
|
||||||
|
lua_pushinteger(L, targetKelvin);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int luaNightlightDisable(lua_State* L) {
|
||||||
|
if (!g_active) return 0;
|
||||||
|
|
||||||
|
const float durationSec =
|
||||||
|
lua_gettop(L) >= 1 ? static_cast<float>(luaL_checknumber(L, 1)) : 0.4f;
|
||||||
|
|
||||||
|
g_transition.fromGain = g_currentGain;
|
||||||
|
g_transition.toGain = {1.f, 1.f, 1.f};
|
||||||
|
g_transition.active = true;
|
||||||
|
g_transition.start = std::chrono::steady_clock::now();
|
||||||
|
g_transition.durationSec = durationSec;
|
||||||
|
g_transition.lastBakedStep = -1;
|
||||||
|
|
||||||
|
g_lastKelvin = g_currentKelvin;
|
||||||
|
g_currentKelvin = 6500;
|
||||||
|
|
||||||
|
postStateEvent(false, true);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int luaNightlightState(lua_State* L) {
|
||||||
|
lua_pushstring(
|
||||||
|
L,
|
||||||
|
std::format(
|
||||||
|
"{{\"enabled\":{},\"animating\":{},\"last\":{},\"current\":{}}}",
|
||||||
|
g_active ? "true" : "false",
|
||||||
|
g_transition.active ? "true" : "false",
|
||||||
|
g_lastKelvin,
|
||||||
|
g_currentKelvin)
|
||||||
|
.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void postStateEvent(bool enabled, bool animating) {
|
||||||
|
if (!g_pEventManager) return;
|
||||||
|
g_pEventManager->postEvent(
|
||||||
|
SHyprIPCEvent{
|
||||||
|
.event = "zshell-nightlight",
|
||||||
|
.data = std::format(
|
||||||
|
"{},{},{},{}",
|
||||||
|
enabled ? 1 : 0,
|
||||||
|
animating ? 1 : 0,
|
||||||
|
g_lastKelvin,
|
||||||
|
g_currentKelvin)});
|
||||||
|
}
|
||||||
|
|
||||||
|
static const SFunctionMatch* findExactMethod(
|
||||||
|
const std::vector<SFunctionMatch>& methods,
|
||||||
|
const std::string& classAndMethod) {
|
||||||
|
for (const auto& m : methods) {
|
||||||
|
if (m.signature.find(classAndMethod) != std::string::npos) return &m;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string manglePrefix(
|
||||||
|
const std::string& className, const std::string& methodName) {
|
||||||
|
return std::to_string(className.size()) + className +
|
||||||
|
std::to_string(methodName.size()) + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
|
||||||
|
PHANDLE = handle;
|
||||||
|
initEnvPaths();
|
||||||
|
dlog("=== PLUGIN_INIT, build timestamp " __DATE__ " " __TIME__ " ===");
|
||||||
|
|
||||||
|
const std::string serverHash = __hyprland_api_get_hash();
|
||||||
|
const std::string clientHash = __hyprland_api_get_client_hash();
|
||||||
|
|
||||||
|
if (serverHash != clientHash) {
|
||||||
|
HyprlandAPI::addNotification(
|
||||||
|
PHANDLE,
|
||||||
|
"[zshell-nightlight] Mismatched headers! Can't proceed.",
|
||||||
|
CHyprColor{1.0f, 0.2f, 0.2f, 1.0f},
|
||||||
|
5000);
|
||||||
|
throw std::runtime_error("[zshell-nightlight] version mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
static const auto SAVEMIRROR_METHODS =
|
||||||
|
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
|
||||||
|
const SFunctionMatch* saveMirrorTarget = findExactMethod(
|
||||||
|
SAVEMIRROR_METHODS,
|
||||||
|
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
|
||||||
|
|
||||||
|
if (saveMirrorTarget) {
|
||||||
|
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
|
||||||
|
PHANDLE,
|
||||||
|
saveMirrorTarget->address,
|
||||||
|
reinterpret_cast<void*>(&hkSaveBufferForMirror));
|
||||||
|
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
|
||||||
|
dlog(
|
||||||
|
"saveBufferForMirror hook installed: " +
|
||||||
|
saveMirrorTarget->signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_pSaveMirrorHook)
|
||||||
|
HyprlandAPI::addNotification(
|
||||||
|
PHANDLE,
|
||||||
|
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
|
||||||
|
"captures may still show the nightlight",
|
||||||
|
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
|
||||||
|
8000);
|
||||||
|
|
||||||
|
static const auto METHODS =
|
||||||
|
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
|
||||||
|
const SFunctionMatch* target =
|
||||||
|
findExactMethod(METHODS, manglePrefix("CHyprOpenGLImpl", "begin"));
|
||||||
|
if (!target)
|
||||||
|
throw std::runtime_error(
|
||||||
|
"[zshell-nightlight] couldn't uniquely resolve "
|
||||||
|
"CHyprOpenGLImpl::begin");
|
||||||
|
|
||||||
|
g_pBeginHook = HyprlandAPI::createFunctionHook(
|
||||||
|
PHANDLE, target->address, reinterpret_cast<void*>(&hkBegin));
|
||||||
|
if (!g_pBeginHook || !g_pBeginHook->hook())
|
||||||
|
throw std::runtime_error("[zshell-nightlight] failed to hook begin()");
|
||||||
|
|
||||||
|
if (!HyprlandAPI::addLuaFunction(
|
||||||
|
PHANDLE, "zshell", "nlSet", luaNightlightSet))
|
||||||
|
throw std::runtime_error(
|
||||||
|
"[zshell-nightlight] failed to register Lua function");
|
||||||
|
|
||||||
|
if (!HyprlandAPI::addLuaFunction(
|
||||||
|
PHANDLE, "zshell", "nlDisable", luaNightlightDisable))
|
||||||
|
throw std::runtime_error(
|
||||||
|
"[zshell-nightlight] failed to register Lua function");
|
||||||
|
|
||||||
|
if (!HyprlandAPI::addLuaFunction(
|
||||||
|
PHANDLE, "zshell", "nlState", luaNightlightState))
|
||||||
|
throw std::runtime_error(
|
||||||
|
"[zshell-nightlight] failed to register Lua function");
|
||||||
|
|
||||||
|
return {
|
||||||
|
"zshell-nightlight",
|
||||||
|
"Animated nightlight, excluded from screencopy",
|
||||||
|
"ZShell",
|
||||||
|
"1.2"};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
APICALL EXPORT std::string PLUGIN_API_VERSION() {
|
||||||
|
return HYPRLAND_API_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
APICALL EXPORT void PLUGIN_EXIT() {}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# cmark-gfm: markdown -> block structure (pkg-config; no CMake config
|
||||||
|
# package is installed).
|
||||||
|
pkg_check_modules(CMARK_GFM REQUIRED IMPORTED_TARGET libcmark-gfm)
|
||||||
|
|
||||||
|
# tree-sitter runtime for code block highlighting (grammars are
|
||||||
|
# dlopen()'d at runtime and optional).
|
||||||
|
pkg_check_modules(TREE_SITTER REQUIRED IMPORTED_TARGET tree-sitter)
|
||||||
|
|
||||||
|
# JKQTMathText (JKQtPlotter) for LaTeX rendering. The config files live
|
||||||
|
# in a shared JKQTPlotter6 directory, not one named after the package.
|
||||||
|
find_path(JKQTPlotter6_CMAKE_DIR
|
||||||
|
NAMES JKQTMathText6Config.cmake
|
||||||
|
HINTS /usr/lib/cmake/JKQTPlotter6 /usr/local/lib/cmake/JKQTPlotter6
|
||||||
|
DOC "Directory containing the JKQtPlotter cmake package files")
|
||||||
|
find_package(JKQTMathText6 REQUIRED PATHS "${JKQTPlotter6_CMAKE_DIR}")
|
||||||
|
|
||||||
|
# --- tree-sitter grammar discovery (configure time) ---
|
||||||
|
#
|
||||||
|
# Discover installed tree-sitter grammars — system packages
|
||||||
|
# (libtree-sitter-<lang>.so) and the parsers Neovim's nvim-treesitter
|
||||||
|
# installs (~/.local/share/nvim/site/parser/*.so) — and pair each with
|
||||||
|
# highlight queries. Query sources, in priority order:
|
||||||
|
# 1. vendored highlight-queries/*.scm (version-pinned; see the
|
||||||
|
# per-file source headers, MIT),
|
||||||
|
# 2. Neovim's own queries (version-matched to its parsers),
|
||||||
|
# 3. tree-sitter/highlighting from GitHub (cached in the build dir).
|
||||||
|
# The result is embedded as highlight-queries.hpp. Re-run cmake to pick
|
||||||
|
# up grammars installed later.
|
||||||
|
#
|
||||||
|
# Candidate entry points are read from the .so with `nm` rather than
|
||||||
|
# assumed to be tree_sitter_<id>; a missing entry point just makes that
|
||||||
|
# candidate fail at runtime.
|
||||||
|
function(_ts_entry_point out file)
|
||||||
|
# OUTPUT_VARIABLE + OUTPUT_QUIET loses the output on CMake 4, so
|
||||||
|
# capture through a temp file.
|
||||||
|
get_filename_component(_ts_nm_base "${file}" NAME)
|
||||||
|
set(_ts_nm_file "${CMAKE_CURRENT_BINARY_DIR}/ts-entry-${_ts_nm_base}")
|
||||||
|
execute_process(
|
||||||
|
COMMAND nm -D --defined-only "${file}"
|
||||||
|
RESULT_VARIABLE _ts_nm_rc
|
||||||
|
OUTPUT_FILE "${_ts_nm_file}"
|
||||||
|
ERROR_FILE "${_ts_nm_file}.err")
|
||||||
|
set(_ts_sym "tree_sitter_missing")
|
||||||
|
if(_ts_nm_rc EQUAL 0 AND EXISTS "${_ts_nm_file}")
|
||||||
|
file(READ "${_ts_nm_file}" _ts_nm_out)
|
||||||
|
file(REMOVE "${_ts_nm_file}" "${_ts_nm_file}.err")
|
||||||
|
# The library also exports the external scanner functions; the
|
||||||
|
# entry point is the one without the _external suffix.
|
||||||
|
string(REGEX MATCHALL " T tree_sitter_[A-Za-z0-9_]+" _ts_syms "${_ts_nm_out}")
|
||||||
|
foreach(_ts_s IN LISTS _ts_syms)
|
||||||
|
string(REPLACE " T " "" _ts_s "${_ts_s}")
|
||||||
|
if(NOT _ts_s MATCHES "_external")
|
||||||
|
set(_ts_sym "${_ts_s}")
|
||||||
|
break()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
set(${out} "${_ts_sym}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Append a query text to <files> (PARENT_SCOPE) unless it duplicates a
|
||||||
|
# hash in <hashes>. The text is written to the build tree right away:
|
||||||
|
# the s-expression `;` comments would split CMake list items and the
|
||||||
|
# query `(` `)` break bracket arguments, so query text only ever lives
|
||||||
|
# in variables and files, never in lists.
|
||||||
|
# Expand Neovim's "; inherits:" directives by appending the inherited
|
||||||
|
# query set's highlights file (recursively; a visited set prevents
|
||||||
|
# cycles). Several languages are stubs that inherit the real query
|
||||||
|
# (html inherits html_tags, qmljs inherits ecma, ...).
|
||||||
|
function(_ts_expand_inherits query_dir text outVar)
|
||||||
|
set(result "${text}")
|
||||||
|
set(_visited "")
|
||||||
|
set(_depth 0)
|
||||||
|
while(_depth LESS 8)
|
||||||
|
# Do not match the leading `;`: a MATCHALL result that itself
|
||||||
|
# contains a semicolon is re-split into list items.
|
||||||
|
string(REGEX MATCHALL "inherits:[ \t]*[A-Za-z0-9_]+" _inh "${result}")
|
||||||
|
if(NOT _inh)
|
||||||
|
break()
|
||||||
|
endif()
|
||||||
|
set(_added FALSE)
|
||||||
|
foreach(_entry IN LISTS _inh)
|
||||||
|
string(REGEX REPLACE "^inherits:[ \t]*" "" _name "${_entry}")
|
||||||
|
set(_file "${query_dir}/${_name}/highlights.scm")
|
||||||
|
if(NOT EXISTS "${_file}" OR _file IN_LIST _visited)
|
||||||
|
continue()
|
||||||
|
endif()
|
||||||
|
list(APPEND _visited "${_file}")
|
||||||
|
file(READ "${_file}" _inh_text)
|
||||||
|
string(APPEND result "\n${_inh_text}")
|
||||||
|
set(_added TRUE)
|
||||||
|
endforeach()
|
||||||
|
if(NOT _added)
|
||||||
|
break()
|
||||||
|
endif()
|
||||||
|
math(EXPR _depth "${_depth} + 1")
|
||||||
|
endwhile()
|
||||||
|
set(${outVar} "${result}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(_ts_add_query sid text filesVar hashesVar)
|
||||||
|
# filesVar/hashesVar hold the caller's variable names.
|
||||||
|
set(files "${${filesVar}}")
|
||||||
|
set(hashes "${${hashesVar}}")
|
||||||
|
string(SHA256 _ts_qh "${text}")
|
||||||
|
if(_ts_qh IN_LIST hashes)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
list(APPEND hashes "${_ts_qh}")
|
||||||
|
list(LENGTH files _ts_qi)
|
||||||
|
set(_ts_qfile "${CMAKE_CURRENT_BINARY_DIR}/ts-queries/${sid}_${_ts_qi}.scm")
|
||||||
|
file(WRITE "${_ts_qfile}" "${text}")
|
||||||
|
list(APPEND files "${_ts_qfile}")
|
||||||
|
set(${filesVar} "${files}" PARENT_SCOPE)
|
||||||
|
set(${hashesVar} "${hashes}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
set(_ts_candidates "") # entries: <id>|<cmake-safe id>|<lib>|<symbol>
|
||||||
|
file(GLOB _ts_sys_files
|
||||||
|
"/usr/lib/libtree-sitter-*.so" "/usr/local/lib/libtree-sitter-*.so")
|
||||||
|
foreach(_ts_file IN LISTS _ts_sys_files)
|
||||||
|
get_filename_component(_ts_name "${_ts_file}" NAME)
|
||||||
|
string(REGEX REPLACE "^libtree-sitter-(.+)\.so$" "\\1" _ts_id "${_ts_name}")
|
||||||
|
string(REPLACE "-" "_" _ts_sid "${_ts_id}")
|
||||||
|
_ts_entry_point(_ts_sym "${_ts_file}")
|
||||||
|
list(APPEND _ts_candidates
|
||||||
|
"${_ts_id}|${_ts_sid}|libtree-sitter-${_ts_id}.so|${_ts_sym}")
|
||||||
|
endforeach()
|
||||||
|
set(_ts_nvim_parser_dirs
|
||||||
|
"$ENV{HOME}/.local/share/nvim/site/parser"
|
||||||
|
"$ENV{HOME}/.local/share/nvim/runtime/parser"
|
||||||
|
"$ENV{HOME}/.local/share/nvim/lazy/nvim-treesitter/parser")
|
||||||
|
set(_ts_nvim_query_dirs
|
||||||
|
"$ENV{HOME}/.local/share/nvim/site/queries"
|
||||||
|
"$ENV{HOME}/.local/share/nvim/lazy/nvim-treesitter/runtime/queries")
|
||||||
|
foreach(_ts_dir IN LISTS _ts_nvim_parser_dirs)
|
||||||
|
file(GLOB _ts_dir_files "${_ts_dir}/*.so")
|
||||||
|
foreach(_ts_file IN LISTS _ts_dir_files)
|
||||||
|
get_filename_component(_ts_name "${_ts_file}" NAME)
|
||||||
|
string(REGEX REPLACE "\\.so$" "" _ts_id "${_ts_name}")
|
||||||
|
string(REPLACE "-" "_" _ts_sid "${_ts_id}")
|
||||||
|
_ts_entry_point(_ts_sym "${_ts_file}")
|
||||||
|
list(APPEND _ts_candidates
|
||||||
|
"${_ts_id}|${_ts_sid}|${_ts_file}|${_ts_sym}")
|
||||||
|
endforeach()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(_ts_ids "")
|
||||||
|
foreach(_ts_c IN LISTS _ts_candidates)
|
||||||
|
string(REPLACE "|" ";" _ts_parts "${_ts_c}")
|
||||||
|
list(GET _ts_parts 0 _ts_id)
|
||||||
|
if(NOT _ts_id IN_LIST _ts_ids)
|
||||||
|
list(APPEND _ts_ids "${_ts_id}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
list(SORT _ts_ids)
|
||||||
|
|
||||||
|
set(HIGHLIGHT_QUERIES_HPP "${CMAKE_CURRENT_BINARY_DIR}/highlight-queries.hpp")
|
||||||
|
set(_hl_header
|
||||||
|
"#pragma once\n\n// Generated by CMake. Discoverd tree-sitter grammars and their\n// highlight queries: vendored highlight-queries/*.scm (MIT), Neovim\n// nvim-treesitter queries, and tree-sitter/highlighting (MIT).\nnamespace ZShell::llm::hq {\nstruct Candidate { const char* lib; const char* symbol; };\nstruct Grammar { const char* id; int nCandidates; const Candidate* candidates; int nQueries; const char* const* queries; };\n")
|
||||||
|
set(_ts_grammar_rows "")
|
||||||
|
set(_ts_vendored
|
||||||
|
c cpp python javascript typescript tsx bash json rust go yaml toml sql)
|
||||||
|
foreach(_ts_id IN LISTS _ts_ids)
|
||||||
|
string(REPLACE "-" "_" _ts_sid "${_ts_id}")
|
||||||
|
|
||||||
|
# Query candidates in priority order (deduped; identical texts are
|
||||||
|
# skipped, the nvim site and plugin copies are usually the same
|
||||||
|
# file).
|
||||||
|
set(_ts_qfiles "")
|
||||||
|
set(_ts_qhashes "")
|
||||||
|
if(_ts_id STREQUAL "cpp")
|
||||||
|
# The C++ grammar is a superset of C and its query only covers
|
||||||
|
# the C++ delta; base C coverage comes from the C query.
|
||||||
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/c.scm" _ts_qa)
|
||||||
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/cpp.scm" _ts_qb)
|
||||||
|
_ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
|
||||||
|
elseif(_ts_id STREQUAL "typescript" OR _ts_id STREQUAL "tsx")
|
||||||
|
# The TS grammars reuse the JS node names; the JS query usually
|
||||||
|
# compiles against them and gives full coverage.
|
||||||
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/javascript.scm" _ts_qa)
|
||||||
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/typescript.scm" _ts_qb)
|
||||||
|
_ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
|
||||||
|
_ts_add_query(${_ts_sid} "${_ts_qb}" _ts_qfiles _ts_qhashes)
|
||||||
|
elseif(_ts_id IN_LIST _ts_vendored)
|
||||||
|
file(READ
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/${_ts_id}.scm" _ts_q)
|
||||||
|
_ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
|
||||||
|
endif()
|
||||||
|
foreach(_ts_qd IN LISTS _ts_nvim_query_dirs)
|
||||||
|
if(EXISTS "${_ts_qd}/${_ts_id}/highlights.scm")
|
||||||
|
file(READ "${_ts_qd}/${_ts_id}/highlights.scm" _ts_q)
|
||||||
|
_ts_expand_inherits("${_ts_qd}" "${_ts_q}" _ts_q)
|
||||||
|
_ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
if(NOT _ts_qfiles)
|
||||||
|
# Last resort: fetch from the tree-sitter/highlighting repo.
|
||||||
|
# Version-skewed against locally installed grammars, so only
|
||||||
|
# used when nothing local exists. Cached; offline builds simply
|
||||||
|
# drop the language.
|
||||||
|
set(_ts_dl "${CMAKE_CURRENT_BINARY_DIR}/ts-query-downloads/${_ts_sid}.scm")
|
||||||
|
if(NOT EXISTS "${_ts_dl}")
|
||||||
|
file(DOWNLOAD
|
||||||
|
"https://raw.githubusercontent.com/tree-sitter/highlighting/main/queries/${_ts_id}/highlight.scm"
|
||||||
|
"${_ts_dl}" STATUS _ts_dl_status TIMEOUT 30)
|
||||||
|
list(GET _ts_dl_status 0 _ts_dl_rc)
|
||||||
|
if(NOT _ts_dl_rc EQUAL 0)
|
||||||
|
file(REMOVE "${_ts_dl}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
if(EXISTS "${_ts_dl}")
|
||||||
|
file(READ "${_ts_dl}" _ts_q)
|
||||||
|
_ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
list(LENGTH _ts_qfiles _ts_nq)
|
||||||
|
if(_ts_nq EQUAL 0)
|
||||||
|
continue()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Emit query sources.
|
||||||
|
set(_ts_qn 0)
|
||||||
|
set(_ts_q_ptrs "")
|
||||||
|
foreach(_ts_qfile IN LISTS _ts_qfiles)
|
||||||
|
file(READ "${_ts_qfile}" _ts_q)
|
||||||
|
string(APPEND _hl_header
|
||||||
|
"inline constexpr const char* q_${_ts_sid}_${_ts_qn} = R\"ZSQUERY(${_ts_q})ZSQUERY\";\n")
|
||||||
|
string(APPEND _ts_q_ptrs "q_${_ts_sid}_${_ts_qn}, ")
|
||||||
|
math(EXPR _ts_qn "${_ts_qn} + 1")
|
||||||
|
endforeach()
|
||||||
|
string(APPEND _hl_header
|
||||||
|
"inline constexpr const char* const q_${_ts_sid}[] = { ${_ts_q_ptrs} };\n")
|
||||||
|
|
||||||
|
# Emit library candidates (system package first, then Neovim).
|
||||||
|
string(APPEND _hl_header "inline constexpr Candidate cand_${_ts_sid}[] = {\n")
|
||||||
|
set(_ts_nc 0)
|
||||||
|
foreach(_ts_c IN LISTS _ts_candidates)
|
||||||
|
string(REPLACE "|" ";" _ts_parts "${_ts_c}")
|
||||||
|
list(GET _ts_parts 0 _ts_cid)
|
||||||
|
if(_ts_cid STREQUAL _ts_id)
|
||||||
|
list(GET _ts_parts 2 _ts_lib)
|
||||||
|
list(GET _ts_parts 3 _ts_sym)
|
||||||
|
string(APPEND _hl_header
|
||||||
|
" { R\"ZSLIB(${_ts_lib})ZSLIB\", R\"ZSSYM(${_ts_sym})ZSSYM\" },\n")
|
||||||
|
math(EXPR _ts_nc "${_ts_nc} + 1")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
string(APPEND _hl_header "};\n")
|
||||||
|
if(_ts_nc EQUAL 0)
|
||||||
|
# Queries but no library: pointless, drop the language.
|
||||||
|
string(APPEND _hl_header "") # (arrays stay; grammar row is skipped)
|
||||||
|
continue()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
list(APPEND _ts_grammar_rows
|
||||||
|
"{ \"${_ts_id}\", ${_ts_nc}, cand_${_ts_sid}, ${_ts_nq}, q_${_ts_sid} },")
|
||||||
|
endforeach()
|
||||||
|
string(APPEND _hl_header "inline constexpr Grammar grammars[] = {\n")
|
||||||
|
foreach(_ts_row IN LISTS _ts_grammar_rows)
|
||||||
|
string(APPEND _hl_header " ${_ts_row}\n")
|
||||||
|
endforeach()
|
||||||
|
string(APPEND _hl_header "};\n}\n")
|
||||||
|
file(WRITE "${HIGHLIGHT_QUERIES_HPP}" "${_hl_header}")
|
||||||
|
|
||||||
|
# Embed the vendored Latin Modern fonts (GUST Font License; provenance
|
||||||
|
# in fonts/latinmodern/GUST-FONT-LICENSE.txt) as byte arrays.
|
||||||
|
set(LM_FONTS
|
||||||
|
lmroman10-regular
|
||||||
|
lmroman10-italic
|
||||||
|
lmroman10-bold
|
||||||
|
lmroman10-bolditalic
|
||||||
|
latinmodern-math)
|
||||||
|
set(LM_FONTS_HPP "${CMAKE_CURRENT_BINARY_DIR}/latinmodern-fonts.hpp")
|
||||||
|
set(_lm_header "#pragma once\n\n// Vendored Latin Modern fonts (GUST Font License; see\n// fonts/latinmodern/GUST-FONT-LICENSE.txt).\nnamespace ZShell::llm::lmfont {\n")
|
||||||
|
foreach(_lm_font IN LISTS LM_FONTS)
|
||||||
|
string(REPLACE "-" "_" _lm_sym "${_lm_font}")
|
||||||
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/fonts/latinmodern/${_lm_font}.otf" _lm_hex HEX)
|
||||||
|
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _lm_bytes "${_lm_hex}")
|
||||||
|
string(APPEND _lm_header "inline const unsigned char ${_lm_sym}[] = { ${_lm_bytes} };\n")
|
||||||
|
endforeach()
|
||||||
|
string(APPEND _lm_header "}\n")
|
||||||
|
file(WRITE "${LM_FONTS_HPP}" "${_lm_header}")
|
||||||
|
|
||||||
|
qml_module(ZShell-llm
|
||||||
|
URI ZShell.Llm
|
||||||
|
SOURCES
|
||||||
|
chat.hpp chat.cpp
|
||||||
|
chatstore.hpp chatstore.cpp
|
||||||
|
codehighlighter.hpp codehighlighter.cpp
|
||||||
|
filetool.hpp filetool.cpp
|
||||||
|
generation.hpp generation.cpp
|
||||||
|
llmclient.hpp llmclient.cpp
|
||||||
|
markdownblock.hpp
|
||||||
|
markdownparser.hpp markdownparser.cpp
|
||||||
|
mathtext.hpp mathtext.cpp
|
||||||
|
message.hpp message.cpp
|
||||||
|
messagemodel.hpp messagemodel.cpp
|
||||||
|
segment.hpp segment.cpp
|
||||||
|
session.hpp session.cpp
|
||||||
|
tool.hpp tool.cpp
|
||||||
|
webfetchtool.hpp webfetchtool.cpp
|
||||||
|
LIBRARIES
|
||||||
|
Qt::Network
|
||||||
|
Qt::Sql
|
||||||
|
Qt::Gui
|
||||||
|
Qt::Widgets
|
||||||
|
ZShell-config
|
||||||
|
JKQTPlotter::JKQTMathText
|
||||||
|
PkgConfig::CMARK_GFM
|
||||||
|
PkgConfig::TREE_SITTER
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(ZShell-llm PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_include_directories(ZShell-llm PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Config)
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
#include "chat.hpp"
|
||||||
|
|
||||||
|
#include "config.hpp"
|
||||||
|
#include "filetool.hpp"
|
||||||
|
#include "llm.hpp"
|
||||||
|
#include "llmclient.hpp"
|
||||||
|
#include "webfetchtool.hpp"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
namespace ZShell::llm {
|
||||||
|
|
||||||
|
Chat::Chat(QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_store(new ChatStore(this))
|
||||||
|
, m_client(new LlmClient(this)) {
|
||||||
|
if (!config::Config::instance()) new config::Config();
|
||||||
|
|
||||||
|
m_store->setLlmClient(m_client);
|
||||||
|
m_client->tools()->registerTool(new WebFetchTool(m_client->tools()));
|
||||||
|
m_client->tools()->registerTool(new FileReadTool(m_client->tools()));
|
||||||
|
|
||||||
|
const auto* llm = config::Config::instance()->llm();
|
||||||
|
m_client->setEndpoint(llm->endpoint());
|
||||||
|
m_client->setModel(llm->model());
|
||||||
|
m_client->setTemperature(llm->temperature());
|
||||||
|
m_client->setToolsEnabled(llm->tools());
|
||||||
|
|
||||||
|
connect(llm, &config::Llm::endpointChanged, this, [this, llm]() {
|
||||||
|
m_client->setEndpoint(llm->endpoint());
|
||||||
|
});
|
||||||
|
connect(llm, &config::Llm::modelChanged, this, [this, llm]() {
|
||||||
|
m_client->setModel(llm->model());
|
||||||
|
});
|
||||||
|
connect(llm, &config::Llm::temperatureChanged, this, [this, llm]() {
|
||||||
|
m_client->setTemperature(llm->temperature());
|
||||||
|
});
|
||||||
|
connect(llm, &config::Llm::toolsChanged, this, [this, llm]() {
|
||||||
|
m_client->setToolsEnabled(llm->tools());
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_client, &LlmClient::busyChanged, this, [this]() {
|
||||||
|
if (m_client->busy() && !m_lastError.isEmpty()) {
|
||||||
|
m_lastError.clear();
|
||||||
|
Q_EMIT lastErrorChanged();
|
||||||
|
}
|
||||||
|
Q_EMIT busyChanged();
|
||||||
|
});
|
||||||
|
connect(m_client, &LlmClient::endpointChanged, this, &Chat::endpointChanged);
|
||||||
|
connect(m_client, &LlmClient::modelChanged, this, &Chat::modelChanged);
|
||||||
|
connect(
|
||||||
|
m_client,
|
||||||
|
&LlmClient::availableModelsChanged,
|
||||||
|
this,
|
||||||
|
&Chat::availableModelsChanged);
|
||||||
|
connect(
|
||||||
|
m_client,
|
||||||
|
&LlmClient::contextSizeChanged,
|
||||||
|
this,
|
||||||
|
&Chat::contextSizeChanged);
|
||||||
|
connect(
|
||||||
|
m_client,
|
||||||
|
&LlmClient::streamingChatIdChanged,
|
||||||
|
this,
|
||||||
|
&Chat::streamingChatIdChanged);
|
||||||
|
connect(
|
||||||
|
m_client,
|
||||||
|
&LlmClient::toolsEnabledChanged,
|
||||||
|
this,
|
||||||
|
&Chat::toolsEnabledChanged);
|
||||||
|
connect(
|
||||||
|
m_client,
|
||||||
|
&LlmClient::errorOccurred,
|
||||||
|
this,
|
||||||
|
[this](const QString& message) {
|
||||||
|
m_lastError = message;
|
||||||
|
Q_EMIT lastErrorChanged();
|
||||||
|
Q_EMIT errorOccurred(message);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
m_store,
|
||||||
|
&ChatStore::sessionRemoved,
|
||||||
|
this,
|
||||||
|
[this](ChatSession* session) { m_client->sessionRemoved(session); });
|
||||||
|
connect(
|
||||||
|
m_client,
|
||||||
|
&LlmClient::titleSuggested,
|
||||||
|
this,
|
||||||
|
[this](ChatSession* session, const QString& title) {
|
||||||
|
qInfo() << "Chat: applying generated title" << session->id()
|
||||||
|
<< title << "(was" << session->title() << ")";
|
||||||
|
session->setTitle(title);
|
||||||
|
m_store->saveMeta(session);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
m_client,
|
||||||
|
&LlmClient::iconSuggested,
|
||||||
|
this,
|
||||||
|
[this](ChatSession* session, const QString& icon) {
|
||||||
|
qInfo() << "Chat: applying generated icon" << session->id() << icon
|
||||||
|
<< "(was" << session->icon() << ")";
|
||||||
|
session->setIcon(icon);
|
||||||
|
m_store->saveMeta(session);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Chat::busy() const {
|
||||||
|
return m_client->busy();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Chat::endpoint() const {
|
||||||
|
return m_client->endpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Chat::model() const {
|
||||||
|
return m_client->model();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Chat::availableModels() const {
|
||||||
|
return m_client->availableModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Chat::contextSize() const {
|
||||||
|
return m_client->contextSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Chat::toolsEnabled() const {
|
||||||
|
return m_client->toolsEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chat::setToolsEnabled(bool value) {
|
||||||
|
m_client->setToolsEnabled(value);
|
||||||
|
if (auto* config = config::Config::instance())
|
||||||
|
config->llm()->set_tools(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Chat::streamingChatId() const {
|
||||||
|
return m_client->streamingChatId();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Chat* Chat::s_instance = nullptr;
|
||||||
|
|
||||||
|
Chat* Chat::create(QQmlEngine*, QJSEngine*) {
|
||||||
|
if (!s_instance) s_instance = new Chat();
|
||||||
|
return s_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chat::stop() {
|
||||||
|
m_client->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chat::dismissError() {
|
||||||
|
if (m_lastError.isEmpty()) return;
|
||||||
|
m_lastError.clear();
|
||||||
|
Q_EMIT lastErrorChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chat::refreshModels() {
|
||||||
|
m_client->refreshModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chat::refreshFromServer() {
|
||||||
|
m_client->refreshFromServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chat::selectModel(const QString& id) {
|
||||||
|
if (id.isEmpty()) return;
|
||||||
|
m_client->setModel(id);
|
||||||
|
if (auto* config = config::Config::instance()) config->llm()->set_model(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::llm
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QtQml>
|
||||||
|
|
||||||
|
#include "chatstore.hpp"
|
||||||
|
|
||||||
|
class QQmlEngine;
|
||||||
|
class QJSEngine;
|
||||||
|
|
||||||
|
namespace ZShell::llm {
|
||||||
|
|
||||||
|
class LlmClient;
|
||||||
|
|
||||||
|
class Chat : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||||
|
Q_PROPERTY(QString endpoint READ endpoint NOTIFY endpointChanged)
|
||||||
|
Q_PROPERTY(QString model READ model NOTIFY modelChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QStringList availableModels READ availableModels NOTIFY
|
||||||
|
availableModelsChanged)
|
||||||
|
Q_PROPERTY(int contextSize READ contextSize NOTIFY contextSizeChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool toolsEnabled READ toolsEnabled WRITE setToolsEnabled NOTIFY
|
||||||
|
toolsEnabledChanged)
|
||||||
|
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||||
|
Q_PROPERTY(ZShell::llm::ChatStore* chats READ chats CONSTANT)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QString streamingChatId READ streamingChatId NOTIFY
|
||||||
|
streamingChatIdChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Chat(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] bool busy() const;
|
||||||
|
[[nodiscard]] QString endpoint() const;
|
||||||
|
[[nodiscard]] QString model() const;
|
||||||
|
[[nodiscard]] QStringList availableModels() const;
|
||||||
|
[[nodiscard]] int contextSize() const;
|
||||||
|
[[nodiscard]] bool toolsEnabled() const;
|
||||||
|
void setToolsEnabled(bool value);
|
||||||
|
[[nodiscard]] QString lastError() const { return m_lastError; }
|
||||||
|
[[nodiscard]] ChatStore* chats() const { return m_store; }
|
||||||
|
[[nodiscard]] QString streamingChatId() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void stop();
|
||||||
|
Q_INVOKABLE void dismissError();
|
||||||
|
Q_INVOKABLE void refreshModels();
|
||||||
|
Q_INVOKABLE void refreshFromServer();
|
||||||
|
Q_INVOKABLE void selectModel(const QString& id);
|
||||||
|
|
||||||
|
static Chat* create(QQmlEngine*, QJSEngine*);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void busyChanged();
|
||||||
|
void endpointChanged();
|
||||||
|
void modelChanged();
|
||||||
|
void availableModelsChanged();
|
||||||
|
void contextSizeChanged();
|
||||||
|
void toolsEnabledChanged();
|
||||||
|
void errorOccurred(const QString& message);
|
||||||
|
void lastErrorChanged();
|
||||||
|
void streamingChatIdChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ChatStore* m_store = nullptr;
|
||||||
|
LlmClient* m_client = nullptr;
|
||||||
|
QString m_lastError;
|
||||||
|
|
||||||
|
static Chat* s_instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::llm
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
#include "chatstore.hpp"
|
||||||
|
|
||||||
|
#include "llmclient.hpp"
|
||||||
|
#include "message.hpp"
|
||||||
|
#include "segment.hpp"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QSqlDatabase>
|
||||||
|
#include <QSqlError>
|
||||||
|
#include <QSqlQuery>
|
||||||
|
#include <QThreadPool>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace ZShell::llm {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString segmentTypeName(LlmSegment::Type type) {
|
||||||
|
switch (type) {
|
||||||
|
case LlmSegment::Type::Reasoning:
|
||||||
|
return QStringLiteral("reasoning");
|
||||||
|
case LlmSegment::Type::ToolCall:
|
||||||
|
return QStringLiteral("tool_call");
|
||||||
|
case LlmSegment::Type::Content:
|
||||||
|
return QStringLiteral("content");
|
||||||
|
}
|
||||||
|
return QStringLiteral("reasoning");
|
||||||
|
}
|
||||||
|
|
||||||
|
LlmSegment::Type segmentTypeFromName(const QString& name) {
|
||||||
|
if (name == QLatin1String("tool_call")) return LlmSegment::Type::ToolCall;
|
||||||
|
if (name == QLatin1String("content")) return LlmSegment::Type::Content;
|
||||||
|
return LlmSegment::Type::Reasoning;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString sqlText(const QString& value) {
|
||||||
|
if (value.isNull()) return QStringLiteral("");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SegmentRow {
|
||||||
|
QString type;
|
||||||
|
QString text;
|
||||||
|
QString name;
|
||||||
|
QString toolCallId;
|
||||||
|
QString arguments;
|
||||||
|
QString result;
|
||||||
|
int status = 0;
|
||||||
|
qint64 elapsedMs = 0;
|
||||||
|
qint64 timestamp = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GenerationRow {
|
||||||
|
qint64 timestamp = 0;
|
||||||
|
bool active = false;
|
||||||
|
QVector<SegmentRow> segments;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MessageRow {
|
||||||
|
bool user = false;
|
||||||
|
qint64 timestamp = 0;
|
||||||
|
QVector<GenerationRow> generations;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ChatStore::ChatStore(QObject* parent)
|
||||||
|
: QObject(parent), m_connectionName(QUuid::createUuid().toString()) {
|
||||||
|
openDb();
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatStore::~ChatStore() {
|
||||||
|
if (m_connectionName.isEmpty()) return;
|
||||||
|
QSqlDatabase db = QSqlDatabase::database(m_connectionName, false);
|
||||||
|
db.close();
|
||||||
|
QSqlDatabase::removeDatabase(m_connectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlDatabase ChatStore::db() const {
|
||||||
|
return QSqlDatabase::database(m_connectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::openDb() {
|
||||||
|
m_dbPath =
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation) +
|
||||||
|
QStringLiteral("/zshell/chats.sqlite");
|
||||||
|
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
|
||||||
|
|
||||||
|
QSqlDatabase db =
|
||||||
|
QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
|
||||||
|
db.setDatabaseName(m_dbPath);
|
||||||
|
if (!db.open()) {
|
||||||
|
qWarning() << "ChatStore: failed to open database" << m_dbPath << ":"
|
||||||
|
<< db.lastError().text();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery pragma(db);
|
||||||
|
pragma.exec(QStringLiteral("PRAGMA foreign_keys = ON"));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec(QStringLiteral(
|
||||||
|
"CREATE TABLE IF NOT EXISTS sessions (\n"
|
||||||
|
" id TEXT PRIMARY KEY,\n"
|
||||||
|
" title TEXT NOT NULL DEFAULT '',\n"
|
||||||
|
" icon TEXT NOT NULL DEFAULT '',\n"
|
||||||
|
" created_at INTEGER NOT NULL,\n"
|
||||||
|
" updated_at INTEGER NOT NULL,\n"
|
||||||
|
" pinned INTEGER NOT NULL DEFAULT 0\n"
|
||||||
|
")"));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec(QStringLiteral(
|
||||||
|
"CREATE TABLE IF NOT EXISTS messages (\n"
|
||||||
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||||
|
" session_id TEXT NOT NULL REFERENCES sessions (id) "
|
||||||
|
"ON DELETE CASCADE,\n"
|
||||||
|
" role TEXT NOT NULL,\n"
|
||||||
|
" timestamp INTEGER NOT NULL\n"
|
||||||
|
")"));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec(QStringLiteral(
|
||||||
|
"CREATE TABLE IF NOT EXISTS generations (\n"
|
||||||
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||||
|
" message_id INTEGER NOT NULL REFERENCES messages "
|
||||||
|
"(id) ON DELETE CASCADE,\n"
|
||||||
|
" timestamp INTEGER NOT NULL,\n"
|
||||||
|
" is_active INTEGER NOT NULL DEFAULT 1\n"
|
||||||
|
")"));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec(QStringLiteral(
|
||||||
|
"CREATE TABLE IF NOT EXISTS segments (\n"
|
||||||
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||||
|
" generation_id INTEGER NOT NULL REFERENCES generations "
|
||||||
|
"(id) ON DELETE CASCADE,\n"
|
||||||
|
" type TEXT NOT NULL,\n"
|
||||||
|
" text TEXT NOT NULL DEFAULT '',\n"
|
||||||
|
" name TEXT NOT NULL DEFAULT '',\n"
|
||||||
|
" tool_call_id TEXT NOT NULL DEFAULT '',\n"
|
||||||
|
" arguments TEXT NOT NULL DEFAULT '',\n"
|
||||||
|
" result TEXT NOT NULL DEFAULT '',\n"
|
||||||
|
" status INTEGER NOT NULL DEFAULT 0,\n"
|
||||||
|
" elapsed_ms INTEGER NOT NULL DEFAULT 0,\n"
|
||||||
|
" timestamp INTEGER NOT NULL\n"
|
||||||
|
")"));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec(QStringLiteral(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_messages_session "
|
||||||
|
"ON messages (session_id)"));
|
||||||
|
query.exec(QStringLiteral(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_generations_message "
|
||||||
|
"ON generations (message_id)"));
|
||||||
|
query.exec(QStringLiteral(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_segments_generation "
|
||||||
|
"ON segments (generation_id)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatStore::count() const {
|
||||||
|
return static_cast<int>(m_sessions.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList ChatStore::values() const {
|
||||||
|
QVariantList vals;
|
||||||
|
vals.reserve(m_sessions.size());
|
||||||
|
for (const auto* session : m_sessions)
|
||||||
|
vals.append(QVariant::fromValue(session));
|
||||||
|
return vals;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatSession* ChatStore::at(int index) const {
|
||||||
|
if (index < 0 || index >= m_sessions.size()) return nullptr;
|
||||||
|
return m_sessions.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatSession* ChatStore::insert(int index) {
|
||||||
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
const QString id = QUuid::createUuid().toString();
|
||||||
|
{
|
||||||
|
QSqlQuery query(db());
|
||||||
|
query.prepare(
|
||||||
|
"INSERT INTO sessions (id, title, created_at, updated_at) "
|
||||||
|
"VALUES (:id, '', :created_at, :updated_at)");
|
||||||
|
query.bindValue(":id", id);
|
||||||
|
query.bindValue(":created_at", now);
|
||||||
|
query.bindValue(":updated_at", now);
|
||||||
|
if (!query.exec())
|
||||||
|
qWarning() << "ChatStore: failed to insert session" << id << ":"
|
||||||
|
<< query.lastError().text();
|
||||||
|
}
|
||||||
|
auto* session = new ChatSession(id, this);
|
||||||
|
session->setMeta(QString(), now, now, 0);
|
||||||
|
const int pos = index >= 0 && index <= m_sessions.size() ? index : 0;
|
||||||
|
m_sessions.insert(pos, session);
|
||||||
|
Q_EMIT countChanged();
|
||||||
|
Q_EMIT valuesChanged();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::remove(int index) {
|
||||||
|
removeSession(at(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::remove(ChatSession* chat) {
|
||||||
|
removeSession(chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::removeSession(ChatSession* session) {
|
||||||
|
if (!session || !m_sessions.contains(session)) return;
|
||||||
|
const QList<ChatSession*> before = m_sessions;
|
||||||
|
Q_EMIT sessionRemoved(session);
|
||||||
|
{
|
||||||
|
QSqlQuery query(db());
|
||||||
|
query.prepare("DELETE FROM sessions WHERE id = :id");
|
||||||
|
query.bindValue(":id", session->id());
|
||||||
|
query.exec();
|
||||||
|
}
|
||||||
|
m_sessions.removeOne(session);
|
||||||
|
session->deleteLater();
|
||||||
|
notify(before);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::move(int from, int to) {
|
||||||
|
if (from < 0 || from >= m_sessions.size() || to < 0 ||
|
||||||
|
to >= m_sessions.size() || from == to)
|
||||||
|
return;
|
||||||
|
m_sessions.move(from, to);
|
||||||
|
Q_EMIT valuesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::clear() {
|
||||||
|
const QList<ChatSession*> sessions = m_sessions;
|
||||||
|
for (ChatSession* session : sessions)
|
||||||
|
removeSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatSession* ChatStore::sessionById(const QString& id) {
|
||||||
|
for (auto* session : m_sessions)
|
||||||
|
if (session->id() == id) return session;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::setLlmClient(LlmClient* client) {
|
||||||
|
m_llmClient = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::persist(ChatSession* session) {
|
||||||
|
if (!session || !m_sessions.contains(session)) return;
|
||||||
|
if (!session->isLoaded()) {
|
||||||
|
m_pendingPersists.insert(session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session->setUpdatedAt(QDateTime::currentMSecsSinceEpoch());
|
||||||
|
if (!saveSession(session)) return;
|
||||||
|
sortAndNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::saveMeta(ChatSession* session) {
|
||||||
|
if (!session || !m_sessions.contains(session)) return;
|
||||||
|
QSqlQuery query(db());
|
||||||
|
query.prepare(
|
||||||
|
"UPDATE sessions SET title = :title, icon = :icon "
|
||||||
|
"WHERE id = :id");
|
||||||
|
query.bindValue(":title", sqlText(session->title()));
|
||||||
|
query.bindValue(":icon", sqlText(session->icon()));
|
||||||
|
query.bindValue(":id", session->id());
|
||||||
|
if (!query.exec())
|
||||||
|
qWarning() << "ChatStore: failed to save meta for" << session->id()
|
||||||
|
<< ":" << query.lastError().text();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatStore::saveSession(ChatSession* session) {
|
||||||
|
const QString id = session->id();
|
||||||
|
QSqlDatabase handle = db();
|
||||||
|
if (!handle.transaction()) {
|
||||||
|
qWarning() << "ChatStore: failed to begin transaction:"
|
||||||
|
<< handle.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool ok = true;
|
||||||
|
{
|
||||||
|
QSqlQuery query(handle);
|
||||||
|
query.prepare(
|
||||||
|
"UPDATE sessions SET title = :title, updated_at = :updated_at "
|
||||||
|
"WHERE id = :id");
|
||||||
|
query.bindValue(":title", sqlText(session->title()));
|
||||||
|
query.bindValue(":updated_at", session->updatedAtMs());
|
||||||
|
query.bindValue(":id", session->id());
|
||||||
|
ok = query.exec();
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
QSqlQuery query(handle);
|
||||||
|
query.prepare("DELETE FROM messages WHERE session_id = :id");
|
||||||
|
query.bindValue(":id", session->id());
|
||||||
|
ok = query.exec();
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
QSqlQuery messageInsert(handle);
|
||||||
|
ok = messageInsert.prepare(
|
||||||
|
"INSERT INTO messages (session_id, role, timestamp) "
|
||||||
|
"VALUES (:id, :role, :timestamp)");
|
||||||
|
QSqlQuery generationInsert(handle);
|
||||||
|
ok = ok &&
|
||||||
|
generationInsert.prepare(
|
||||||
|
"INSERT INTO generations (message_id, timestamp, is_active) "
|
||||||
|
"VALUES (:mid, :timestamp, :is_active)");
|
||||||
|
QSqlQuery segmentInsert(handle);
|
||||||
|
ok = ok &&
|
||||||
|
segmentInsert.prepare(
|
||||||
|
"INSERT INTO segments (generation_id, type, text, name, "
|
||||||
|
"tool_call_id, arguments, result, status, elapsed_ms, "
|
||||||
|
"timestamp) VALUES (:gid, :type, :text, :name, "
|
||||||
|
":tool_call_id, :arguments, :result, :status, :elapsed_ms, "
|
||||||
|
":timestamp)");
|
||||||
|
const auto* model = session->messagesModel();
|
||||||
|
for (int row = model->rowCount() - 1; ok && row >= 0; --row) {
|
||||||
|
const auto* message = model->at(row);
|
||||||
|
messageInsert.bindValue(":id", session->id());
|
||||||
|
messageInsert.bindValue(
|
||||||
|
":role",
|
||||||
|
message->role() == ChatMessage::Role::User
|
||||||
|
? QStringLiteral("user")
|
||||||
|
: QStringLiteral("assistant"));
|
||||||
|
messageInsert.bindValue(":timestamp", message->timestamp());
|
||||||
|
if (!messageInsert.exec()) {
|
||||||
|
ok = false;
|
||||||
|
qWarning() << "ChatStore: saveSession" << id
|
||||||
|
<< "message insert failed:"
|
||||||
|
<< messageInsert.lastError().text();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int messageId = messageInsert.lastInsertId().toInt();
|
||||||
|
for (int i = 0; ok && i < message->generationCount(); ++i) {
|
||||||
|
const auto* generation = message->generation(i);
|
||||||
|
generationInsert.bindValue(":mid", messageId);
|
||||||
|
generationInsert.bindValue(
|
||||||
|
":timestamp", generation->timestamp());
|
||||||
|
generationInsert.bindValue(
|
||||||
|
":is_active",
|
||||||
|
i == message->activeGenerationIndex() ? 1 : 0);
|
||||||
|
if (!generationInsert.exec()) {
|
||||||
|
ok = false;
|
||||||
|
qWarning() << "ChatStore: saveSession" << id
|
||||||
|
<< "generation insert failed:"
|
||||||
|
<< generationInsert.lastError().text();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int generationId =
|
||||||
|
generationInsert.lastInsertId().toInt();
|
||||||
|
for (const auto* segment : generation->segments()) {
|
||||||
|
segmentInsert.bindValue(":gid", generationId);
|
||||||
|
segmentInsert.bindValue(
|
||||||
|
":type", segmentTypeName(segment->type()));
|
||||||
|
segmentInsert.bindValue(":text", sqlText(segment->text()));
|
||||||
|
segmentInsert.bindValue(":name", sqlText(segment->name()));
|
||||||
|
segmentInsert.bindValue(
|
||||||
|
":tool_call_id", sqlText(segment->toolCallId()));
|
||||||
|
segmentInsert.bindValue(
|
||||||
|
":arguments", sqlText(segment->arguments()));
|
||||||
|
segmentInsert.bindValue(
|
||||||
|
":result", sqlText(segment->result()));
|
||||||
|
segmentInsert.bindValue(
|
||||||
|
":status", static_cast<int>(segment->status()));
|
||||||
|
segmentInsert.bindValue(":elapsed_ms", segment->elapsedMs());
|
||||||
|
segmentInsert.bindValue(":timestamp", segment->timestamp());
|
||||||
|
if (!segmentInsert.exec()) {
|
||||||
|
ok = false;
|
||||||
|
qWarning() << "ChatStore: saveSession" << id
|
||||||
|
<< "segment insert failed:"
|
||||||
|
<< segmentInsert.lastError().text();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok || !handle.commit()) {
|
||||||
|
qWarning() << "ChatStore: saveSession" << id
|
||||||
|
<< "commit failed, rolling back";
|
||||||
|
handle.rollback();
|
||||||
|
ok = false;
|
||||||
|
qWarning() << "ChatStore: failed to save session" << session->id()
|
||||||
|
<< ":" << handle.lastError().text();
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||||
|
if (!session) return;
|
||||||
|
const QString sessionId = session->id();
|
||||||
|
const QString path = m_dbPath;
|
||||||
|
|
||||||
|
QThreadPool::globalInstance()->start([store = QPointer<ChatStore>(this),
|
||||||
|
session =
|
||||||
|
QPointer<ChatSession>(session),
|
||||||
|
sessionId,
|
||||||
|
path]() {
|
||||||
|
QList<MessageRow> rows;
|
||||||
|
const QString connName = QUuid::createUuid().toString();
|
||||||
|
{
|
||||||
|
QSqlDatabase db =
|
||||||
|
QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connName);
|
||||||
|
db.setDatabaseName(path);
|
||||||
|
if (db.open()) {
|
||||||
|
QSqlQuery busy(db);
|
||||||
|
busy.exec(QStringLiteral("PRAGMA busy_timeout = 3000"));
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"SELECT id, role, timestamp FROM messages "
|
||||||
|
"WHERE session_id = :id ORDER BY rowid DESC");
|
||||||
|
query.bindValue(":id", sessionId);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "ChatStore: failed to load messages for"
|
||||||
|
<< sessionId << ":" << query.lastError().text();
|
||||||
|
} else {
|
||||||
|
while (query.next()) {
|
||||||
|
const int messageId = query.value(0).toInt();
|
||||||
|
MessageRow message;
|
||||||
|
message.user = query.value(1).toString() ==
|
||||||
|
QLatin1String("user");
|
||||||
|
message.timestamp = query.value(2).toLongLong();
|
||||||
|
QSqlQuery generationQuery(db);
|
||||||
|
generationQuery.prepare(
|
||||||
|
"SELECT id, timestamp, is_active FROM "
|
||||||
|
"generations WHERE message_id = :mid "
|
||||||
|
"ORDER BY rowid");
|
||||||
|
generationQuery.bindValue(":mid", messageId);
|
||||||
|
if (generationQuery.exec()) {
|
||||||
|
while (generationQuery.next()) {
|
||||||
|
GenerationRow generation;
|
||||||
|
generation.timestamp =
|
||||||
|
generationQuery.value(1).toLongLong();
|
||||||
|
generation.active =
|
||||||
|
generationQuery.value(2).toInt() != 0;
|
||||||
|
QSqlQuery segmentQuery(db);
|
||||||
|
segmentQuery.prepare(
|
||||||
|
"SELECT type, text, name, tool_call_id, "
|
||||||
|
"arguments, result, status, elapsed_ms, "
|
||||||
|
"timestamp FROM segments WHERE "
|
||||||
|
"generation_id = :gid ORDER BY rowid");
|
||||||
|
segmentQuery.bindValue(
|
||||||
|
":gid", generationQuery.value(0).toInt());
|
||||||
|
if (segmentQuery.exec()) {
|
||||||
|
while (segmentQuery.next()) {
|
||||||
|
SegmentRow segment;
|
||||||
|
segment.type =
|
||||||
|
segmentQuery.value(0).toString();
|
||||||
|
segment.text =
|
||||||
|
segmentQuery.value(1).toString();
|
||||||
|
segment.name =
|
||||||
|
segmentQuery.value(2).toString();
|
||||||
|
segment.toolCallId =
|
||||||
|
segmentQuery.value(3).toString();
|
||||||
|
segment.arguments =
|
||||||
|
segmentQuery.value(4).toString();
|
||||||
|
segment.result =
|
||||||
|
segmentQuery.value(5).toString();
|
||||||
|
segment.status =
|
||||||
|
segmentQuery.value(6).toInt();
|
||||||
|
segment.elapsedMs =
|
||||||
|
segmentQuery.value(7).toLongLong();
|
||||||
|
segment.timestamp =
|
||||||
|
segmentQuery.value(8).toLongLong();
|
||||||
|
generation.segments.append(segment);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning()
|
||||||
|
<< "ChatStore: failed to load "
|
||||||
|
"segments for generation"
|
||||||
|
<< generationQuery.value(0).toInt()
|
||||||
|
<< ":"
|
||||||
|
<< segmentQuery.lastError().text();
|
||||||
|
}
|
||||||
|
message.generations.append(generation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning()
|
||||||
|
<< "ChatStore: failed to load generations "
|
||||||
|
"for message"
|
||||||
|
<< messageId << ":"
|
||||||
|
<< generationQuery.lastError().text();
|
||||||
|
}
|
||||||
|
rows.append(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
} else {
|
||||||
|
qWarning() << "ChatStore: failed to open database for load:"
|
||||||
|
<< db.lastError().text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(connName);
|
||||||
|
|
||||||
|
QMetaObject::invokeMethod(
|
||||||
|
QCoreApplication::instance(),
|
||||||
|
[store, session, rows = std::move(rows)]() mutable {
|
||||||
|
ChatStore* st = store;
|
||||||
|
ChatSession* s = session;
|
||||||
|
if (!st || !s) return;
|
||||||
|
|
||||||
|
auto* model = s->model();
|
||||||
|
if (!model) return;
|
||||||
|
QList<ChatMessage*> messages;
|
||||||
|
for (const MessageRow& row : rows) {
|
||||||
|
auto* message = model->createMessage(
|
||||||
|
row.user ? ChatMessage::Role::User
|
||||||
|
: ChatMessage::Role::Assistant,
|
||||||
|
row.timestamp);
|
||||||
|
int activeIndex = 0;
|
||||||
|
for (int i = 0; i < row.generations.size(); ++i) {
|
||||||
|
const GenerationRow& generationRow =
|
||||||
|
row.generations.at(i);
|
||||||
|
auto* generation =
|
||||||
|
message->addGeneration(generationRow.timestamp);
|
||||||
|
for (const SegmentRow& segmentRow :
|
||||||
|
generationRow.segments) {
|
||||||
|
auto* segment = new LlmSegment(
|
||||||
|
segmentTypeFromName(segmentRow.type),
|
||||||
|
segmentRow.timestamp,
|
||||||
|
generation);
|
||||||
|
segment->setText(segmentRow.text);
|
||||||
|
segment->setName(segmentRow.name);
|
||||||
|
segment->setToolCallId(segmentRow.toolCallId);
|
||||||
|
segment->appendArguments(segmentRow.arguments);
|
||||||
|
segment->setResult(segmentRow.result);
|
||||||
|
segment->setStatus(
|
||||||
|
static_cast<LlmSegment::Status>(
|
||||||
|
segmentRow.status));
|
||||||
|
segment->restore(segmentRow.elapsedMs);
|
||||||
|
generation->addSegment(segment);
|
||||||
|
}
|
||||||
|
if (generationRow.active) activeIndex = i;
|
||||||
|
}
|
||||||
|
message->setActiveGeneration(activeIndex);
|
||||||
|
messages.append(message);
|
||||||
|
}
|
||||||
|
if (model->rowCount() > 0) {
|
||||||
|
QList<ChatMessage*> live = messages;
|
||||||
|
for (int r = 0; r < model->rowCount(); ++r)
|
||||||
|
live.prepend(model->at(r));
|
||||||
|
messages = live;
|
||||||
|
}
|
||||||
|
if (!messages.isEmpty() || model->rowCount() > 0)
|
||||||
|
s->adoptMessages(messages);
|
||||||
|
|
||||||
|
s->markLoaded();
|
||||||
|
|
||||||
|
if (s->takeClearPending()) {
|
||||||
|
s->clear();
|
||||||
|
} else if (st->m_pendingPersists.remove(s)) {
|
||||||
|
st->persist(s);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::load() {
|
||||||
|
QSqlQuery query(db());
|
||||||
|
query.exec(
|
||||||
|
"SELECT s.id, s.title, s.icon, s.created_at, s.updated_at, s.pinned, "
|
||||||
|
"(SELECT COUNT(*) FROM messages m WHERE m.session_id = s.id) AS cnt "
|
||||||
|
"FROM sessions s ORDER BY s.pinned DESC, s.updated_at DESC");
|
||||||
|
while (query.next()) {
|
||||||
|
auto* session = new ChatSession(query.value(0).toString(), this);
|
||||||
|
session->setMeta(
|
||||||
|
query.value(1).toString(),
|
||||||
|
query.value(3).toLongLong(),
|
||||||
|
query.value(4).toLongLong(),
|
||||||
|
query.value(6).toInt());
|
||||||
|
session->setIcon(query.value(2).toString());
|
||||||
|
session->setPinned(query.value(5).toInt() != 0);
|
||||||
|
m_sessions.append(session);
|
||||||
|
}
|
||||||
|
sortAndNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::sortAndNotify() {
|
||||||
|
const QList<ChatSession*> before = m_sessions;
|
||||||
|
std::stable_sort(
|
||||||
|
m_sessions.begin(),
|
||||||
|
m_sessions.end(),
|
||||||
|
[](const ChatSession* a, const ChatSession* b) {
|
||||||
|
if (a->pinned() != b->pinned()) return a->pinned() > b->pinned();
|
||||||
|
return a->updatedAtMs() > b->updatedAtMs();
|
||||||
|
});
|
||||||
|
notify(before);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::notify(const QList<ChatSession*>& before) {
|
||||||
|
if (before.size() != m_sessions.size()) Q_EMIT countChanged();
|
||||||
|
bool same = before.size() == m_sessions.size();
|
||||||
|
for (int i = 0; same && i < m_sessions.size(); ++i)
|
||||||
|
if (before.at(i) != m_sessions.at(i)) same = false;
|
||||||
|
if (!same) Q_EMIT valuesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::llm
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user