Compare commits
37
Commits
fc81be922f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8101b2807 | ||
|
|
8271e5d013 | ||
|
|
90184bff58 | ||
|
|
abdde2450d | ||
|
|
fd05b3d4c4 | ||
|
|
9d8bde1413 | ||
|
|
118b8f482a | ||
|
|
b58b4fdc9c | ||
|
|
a56afd7fac | ||
|
|
b0cb43f75c | ||
|
|
e655ec5bc7 | ||
|
|
dc0f222ace | ||
|
|
de19fd6ac4 | ||
|
|
9edbcbd610 | ||
|
|
cd44d27b74 | ||
|
|
0d0faecb1c | ||
|
|
200a6a5d54 | ||
|
|
2d2231745c | ||
|
|
187e97a081 | ||
|
|
0f54d93675 | ||
|
|
5401cfbc0a | ||
|
|
e35c87b3f2 | ||
|
|
cbd4e6caa7 | ||
|
|
36cddc96c3 | ||
|
|
6781078467 | ||
|
|
d3345906b9 | ||
|
|
08d42e1f8a | ||
|
|
45aa6c9005 | ||
|
|
1763b03287 | ||
|
|
72ff6ad42a | ||
|
|
2a45740020 | ||
|
|
0a503bc27c | ||
|
|
1fe8a3a959 | ||
|
|
ae25f33fc8
|
||
|
|
02267ec9a6
|
||
|
|
4b8d74a7e5
|
||
|
|
1fa3c84a56
|
@@ -0,0 +1,3 @@
|
||||
|
||||
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
|
||||
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
|
||||
@@ -0,0 +1,5 @@
|
||||
CompileFlags:
|
||||
Remove:
|
||||
# GCC-only flags from the system's default spec that clang/clangd cannot
|
||||
# parse. They appear in compile_commands.json but are not set by the project.
|
||||
- -mno-direct-extern-access
|
||||
@@ -38,7 +38,6 @@ body:
|
||||
attributes:
|
||||
label: OS
|
||||
options:
|
||||
- NixOS
|
||||
- Arch
|
||||
- Debian / Ubuntu
|
||||
- Fedora
|
||||
|
||||
@@ -30,6 +30,7 @@ body:
|
||||
- Refactor / style
|
||||
- CI / build / tooling
|
||||
- Documentation
|
||||
- Other (Describe in description box with header "Issue type")
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
+12
-2
@@ -50,6 +50,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(QT_NO_PRIVATE_MODULE_WARNING ON)
|
||||
set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
|
||||
|
||||
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
|
||||
@@ -61,10 +62,12 @@ set(CMAKE_INSTALL_MESSAGE NEVER)
|
||||
|
||||
add_compile_options(
|
||||
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
||||
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
||||
-Wformat=2 -Wfloat-equal -Woverloaded-virtual
|
||||
-Wnull-dereference -Wdouble-promotion
|
||||
-Wformat=2 -Wfloat-equal
|
||||
-Wsign-conversion -Wredundant-decls -Wswitch
|
||||
-Wunreachable-code
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-Wold-style-cast>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
|
||||
)
|
||||
|
||||
|
||||
@@ -188,4 +191,11 @@ if("m3shapes" IN_LIST ENABLE_MODULES)
|
||||
if(TARGET m3shapesplugin)
|
||||
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
|
||||
endif()
|
||||
|
||||
# Silence warnings that come from m3shapes files we do not manage.
|
||||
foreach(m3shapes_target m3shapes_core m3shapes_morph m3shapes_shapes m3shapes m3shapesplugin)
|
||||
if(TARGET ${m3shapes_target})
|
||||
target_compile_options(${m3shapes_target} PRIVATE -Wno-shadow -Wno-float-equal)
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real radius
|
||||
property real topLeftRadius: radius
|
||||
property real topRightRadius: radius
|
||||
property real bottomRightRadius: radius
|
||||
property real bottomLeftRadius: radius
|
||||
property color color: "transparent"
|
||||
default property alias contentData: contentItem.data
|
||||
|
||||
Item {
|
||||
id: contentItem
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
ShaderEffectSource {
|
||||
id: src
|
||||
|
||||
hideSource: true
|
||||
sourceItem: contentItem
|
||||
}
|
||||
|
||||
RoundedMask {
|
||||
anchors.fill: parent
|
||||
source: src
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
color: root.color
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
@@ -2,55 +2,57 @@ import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
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;
|
||||
}
|
||||
ClipRect {
|
||||
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
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
implicitHeight: handle.implicitHeight
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
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
|
||||
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
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,23 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Flickable {
|
||||
id: root
|
||||
|
||||
interactive: !ShellState.forActive().isDrawing
|
||||
|
||||
rebound: Transition {
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
}
|
||||
|
||||
readonly property KineticWheelScroller _scroller: KineticWheelScroller {
|
||||
id: wheelScroller
|
||||
anchors.fill: parent
|
||||
parent: root
|
||||
z: 12
|
||||
flickable: root
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
import QtQuick
|
||||
|
||||
// Taken from hyprfm: https://github.com/soyeb-jim285/hyprfm/blob/main/src/qml/views/KineticWheelScroller.qml
|
||||
// Adapted to fit this project where needed.
|
||||
|
||||
// Deliberately not a MouseArea: a MouseArea claims the arrow cursor for its
|
||||
// whole area, which silently killed every cursorShape in the views this
|
||||
// overlays (column resize grips, pointer hands). A WheelHandler on a plain
|
||||
// Item takes wheel events without touching the cursor.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var flickable
|
||||
|
||||
property real wheelStep: 56
|
||||
property real mouseWheelMultiplier: 1.0
|
||||
property real touchpadMultiplier: 1.35
|
||||
property real minVelocity: 480
|
||||
property real maxVelocity: 5200
|
||||
property real kineticGain: 1.35
|
||||
property real smoothing: 0.72
|
||||
property real edgeEpsilon: 1.0
|
||||
// Browser-style overscroll: larger visible rubber-band, softer
|
||||
// resistance, and a springy settle on release.
|
||||
property real overshootLimit: 140
|
||||
property real overshootResistance: 0.38
|
||||
property real maxOvershootInput: 520
|
||||
// Bounce magnitude per unit of impact velocity. Keep this deliberately
|
||||
// small: slow edge hits should settle, not fire a visibly faster bounce.
|
||||
property real kineticBounceGain: 0.018
|
||||
property real maxKineticBounceInput: 90
|
||||
// Minimum impact velocity that actually produces a visible bounce.
|
||||
// Very slow landings skip bounce entirely — matches what browsers do
|
||||
// when a fling decays to near-zero before reaching the edge.
|
||||
property real minBounceVelocity: 260
|
||||
|
||||
property real velocity: 0
|
||||
property double lastWheelTimestamp: 0
|
||||
property bool kineticCandidate: false
|
||||
property real overshootInput: 0
|
||||
property real overshootReleaseVelocity: 0
|
||||
property real pendingKineticBounceVelocity: 0
|
||||
// Set by runKineticGlide when the glide will clamp against an edge.
|
||||
// It is the glide velocity at the instant of impact, which drives the
|
||||
// elastic bounce amount and duration — browser-style: slow impact
|
||||
// makes a small gentle bounce; fast impact makes a big springy one.
|
||||
property real pendingImpactVelocity: 0
|
||||
|
||||
// Browser-style exponential-feeling glide replaces the linear
|
||||
// flickDeceleration decay. Tail uses OutQuart — soft landing.
|
||||
property real kineticTau: 0.42
|
||||
|
||||
property real mouseGlideTime: 0.09 // decay time constant (s); higher = longer coast
|
||||
property real mouseAccelWindow: 140 // ms between notches that counts as "continuous"
|
||||
property real mouseAccelStep: 0.15 // extra impulse multiplier per consecutive notch
|
||||
property real mouseMaxAccel: 2.5
|
||||
property real mouseSnap: 0.2 // fraction of each notch applied instantly (0 = fully glided, 1 = no smoothing)
|
||||
|
||||
property real mouseVelocity: 0
|
||||
property int mouseDir: 0
|
||||
property int mouseStreak: 0
|
||||
property double mouseLastTime: 0
|
||||
|
||||
// Escape hatch if auto-detection misjudges a device
|
||||
property bool forceMouseWheel: false
|
||||
|
||||
readonly property real visualOvershoot: overshootInput < 0 ? rubberDistance(-overshootInput) : -rubberDistance(overshootInput)
|
||||
|
||||
signal scrollStarted
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
// `blocking` decides whether the event continues to the Flickable
|
||||
// underneath; handleWheel() flips it per event (the old MouseArea
|
||||
// code toggled wheel.accepted for the same purpose).
|
||||
onWheel: wheel => root.handleWheel(wheel)
|
||||
}
|
||||
|
||||
function isMouseWheel(wheel) {
|
||||
if (forceMouseWheel)
|
||||
return true;
|
||||
var ay = wheel.angleDelta.y;
|
||||
if (ay === 0)
|
||||
return false;
|
||||
return Math.abs(ay) % 120 === 0;
|
||||
}
|
||||
|
||||
function minimumContentY() {
|
||||
if (!flickable)
|
||||
return 0;
|
||||
|
||||
return flickable.originY;
|
||||
}
|
||||
|
||||
function maximumContentY() {
|
||||
if (!flickable)
|
||||
return 0;
|
||||
|
||||
return Math.max(minimumContentY(), minimumContentY() + flickable.contentHeight - flickable.height);
|
||||
}
|
||||
|
||||
function boundedContentY(value) {
|
||||
return Math.max(minimumContentY(), Math.min(maximumContentY(), value));
|
||||
}
|
||||
|
||||
function rubberDistance(distance) {
|
||||
if (distance <= 0)
|
||||
return 0;
|
||||
|
||||
var scaled = (distance * overshootResistance) / overshootLimit;
|
||||
return overshootLimit * (1 - 1 / (1 + scaled));
|
||||
}
|
||||
|
||||
function setOvershootInput(value) {
|
||||
overshootInput = Math.max(-maxOvershootInput, Math.min(maxOvershootInput, value));
|
||||
}
|
||||
|
||||
function hasOvershoot() {
|
||||
return Math.abs(overshootInput) > edgeEpsilon;
|
||||
}
|
||||
|
||||
function absorbOvershoot(delta) {
|
||||
if (!hasOvershoot())
|
||||
return delta;
|
||||
|
||||
var sameDirection = (overshootInput < 0 && delta < 0) || (overshootInput > 0 && delta > 0);
|
||||
|
||||
if (sameDirection) {
|
||||
setOvershootInput(overshootInput + delta);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var combined = overshootInput + delta;
|
||||
if ((overshootInput < 0 && combined < 0) || (overshootInput > 0 && combined > 0)) {
|
||||
setOvershootInput(combined);
|
||||
return 0;
|
||||
}
|
||||
|
||||
setOvershootInput(0);
|
||||
return combined;
|
||||
}
|
||||
|
||||
function deltaFor(wheel) {
|
||||
var rawDelta;
|
||||
if (isMouseWheel(wheel))
|
||||
rawDelta = (wheel.angleDelta.y / 120.0) * wheelStep * mouseWheelMultiplier;
|
||||
else
|
||||
rawDelta = wheel.pixelDelta.y !== 0 ? wheel.pixelDelta.y * touchpadMultiplier : (wheel.angleDelta.y / 120.0) * wheelStep * mouseWheelMultiplier;
|
||||
|
||||
return wheel.inverted ? rawDelta : -rawDelta;
|
||||
}
|
||||
|
||||
function shouldUseKinetic(wheel, gap) {
|
||||
return wheel.pixelDelta.y !== 0 || (wheel.phase !== Qt.NoScrollPhase && wheel.angleDelta.y === 0);
|
||||
}
|
||||
|
||||
function scrollBy(delta) {
|
||||
if (!flickable || Math.abs(delta) < 0.01)
|
||||
return;
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
|
||||
var remainingDelta = absorbOvershoot(delta);
|
||||
if (Math.abs(remainingDelta) < 0.01)
|
||||
return;
|
||||
var minY = minimumContentY();
|
||||
var maxY = maximumContentY();
|
||||
var next = flickable.contentY + remainingDelta;
|
||||
|
||||
if (next < minY) {
|
||||
flickable.contentY = minY;
|
||||
setOvershootInput(overshootInput + next - minY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (next > maxY) {
|
||||
flickable.contentY = maxY;
|
||||
setOvershootInput(overshootInput + next - maxY);
|
||||
return;
|
||||
}
|
||||
|
||||
flickable.contentY = next;
|
||||
}
|
||||
|
||||
function remainingDistanceFor(velocityValue) {
|
||||
if (!flickable)
|
||||
return 0;
|
||||
|
||||
if (velocityValue > 0)
|
||||
return Math.max(0, maximumContentY() - flickable.contentY);
|
||||
|
||||
if (velocityValue < 0)
|
||||
return Math.max(0, flickable.contentY - minimumContentY());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function clampedContentVelocity(velocityValue) {
|
||||
if (!flickable || velocityValue === 0)
|
||||
return 0;
|
||||
|
||||
var remaining = remainingDistanceFor(velocityValue);
|
||||
if (remaining <= edgeEpsilon)
|
||||
return 0;
|
||||
|
||||
var maxAllowed = Math.sqrt(2 * flickable.flickDeceleration * remaining);
|
||||
var magnitude = Math.min(Math.abs(velocityValue), maxAllowed);
|
||||
return velocityValue < 0 ? -magnitude : magnitude;
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
overshootReleaseVelocity = 0;
|
||||
finishTimer.stop();
|
||||
kineticAnim.stop();
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
stopMouseGlide();
|
||||
}
|
||||
|
||||
function bounceBack() {
|
||||
if (!hasOvershoot()) {
|
||||
setOvershootInput(0);
|
||||
overshootReleaseVelocity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var mag = Math.min(1.0, Math.abs(overshootInput) / maxOvershootInput);
|
||||
var speed = Math.abs(overshootReleaseVelocity);
|
||||
var duration = speed >= minBounceVelocity ? Math.round(Math.max(280, Math.min(620, Math.abs(overshootInput) / speed * 1600))) : Math.round(380 + mag * 260);
|
||||
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
bounceBackAnimation.duration = duration;
|
||||
bounceBackAnimation.from = overshootInput;
|
||||
bounceBackAnimation.to = 0;
|
||||
overshootReleaseVelocity = 0;
|
||||
bounceBackAnimation.start();
|
||||
}
|
||||
|
||||
function triggerKineticBounce(contentVelocity) {
|
||||
if (!flickable || contentVelocity === 0)
|
||||
return;
|
||||
var atTop = flickable.contentY <= minimumContentY() + edgeEpsilon;
|
||||
var atBottom = flickable.contentY >= maximumContentY() - edgeEpsilon;
|
||||
if ((contentVelocity < 0 && !atTop) || (contentVelocity > 0 && !atBottom))
|
||||
return;
|
||||
var speed = Math.abs(contentVelocity);
|
||||
var effectiveVelocity = contentVelocity;
|
||||
if (speed < minBounceVelocity)
|
||||
effectiveVelocity = (contentVelocity < 0 ? -1 : 1) * Math.max(90, speed * 0.6);
|
||||
|
||||
var target = Math.max(-maxKineticBounceInput, Math.min(maxKineticBounceInput, effectiveVelocity * kineticBounceGain));
|
||||
if (Math.abs(target) < 2.5)
|
||||
target = contentVelocity < 0 ? -2.5 : 2.5;
|
||||
|
||||
var mag = Math.min(1.0, Math.abs(target) / maxKineticBounceInput);
|
||||
var inDur = Math.round(150 + mag * 120);
|
||||
var outDur = Math.round(320 + mag * 300);
|
||||
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
overshootReleaseVelocity = 0;
|
||||
overshootInput = 0;
|
||||
kineticBounceIn.from = 0;
|
||||
kineticBounceIn.to = target;
|
||||
kineticBounceIn.duration = inDur;
|
||||
kineticBounceOut.from = target;
|
||||
kineticBounceOut.to = 0;
|
||||
kineticBounceOut.duration = outDur;
|
||||
kineticBounceSequence.start();
|
||||
}
|
||||
|
||||
function stopAndSettle() {
|
||||
kineticAnim.stop();
|
||||
if (flickable && flickable.flicking)
|
||||
flickable.cancelFlick();
|
||||
|
||||
resetState();
|
||||
bounceBack();
|
||||
}
|
||||
|
||||
function runKineticGlide(contentVelocity) {
|
||||
// contentVelocity > 0 means the content should scroll downward
|
||||
// (contentY increases), matching deltaFor()'s sign convention.
|
||||
if (!flickable || contentVelocity === 0) {
|
||||
pendingImpactVelocity = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var current = flickable.contentY;
|
||||
var unclampedTarget = current + contentVelocity * kineticTau;
|
||||
var target = boundedContentY(unclampedTarget);
|
||||
var distUnclamped = Math.abs(unclampedTarget - current);
|
||||
var distActual = Math.abs(target - current);
|
||||
if (distActual < 4) {
|
||||
pendingImpactVelocity = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the glide is clipped by an edge, compute the velocity at the
|
||||
// moment of impact using the OutQuart velocity profile. That is
|
||||
// the velocity the bounce should feed on — gentle impacts get
|
||||
// gentle bounces, hard impacts get big springy ones.
|
||||
var hitsEdge = distActual + 0.5 < distUnclamped;
|
||||
if (hitsEdge && distUnclamped > 0) {
|
||||
var f = distActual / distUnclamped; // fraction covered
|
||||
// OutQuart: y = 1 - (1-x)^4, so velocity fraction at distance
|
||||
// fraction f equals (1-f)^(3/4) (times the constant peak).
|
||||
var velFraction = Math.pow(Math.max(0, 1 - f), 0.75);
|
||||
pendingImpactVelocity = contentVelocity * velFraction;
|
||||
} else {
|
||||
pendingImpactVelocity = 0;
|
||||
}
|
||||
|
||||
kineticAnim.stop();
|
||||
kineticAnim.from = current;
|
||||
kineticAnim.to = target;
|
||||
// Duration from distance and velocity; clamped so short flicks
|
||||
// stay snappy and long flicks don't run forever.
|
||||
kineticAnim.duration = Math.round(Math.min(1400, Math.max(180, distActual / Math.abs(contentVelocity) * 1800)));
|
||||
kineticAnim.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
function atEdgeFor(contentVelocity) {
|
||||
if (!flickable)
|
||||
return false;
|
||||
if (contentVelocity > 0)
|
||||
return flickable.contentY >= maximumContentY() - edgeEpsilon;
|
||||
if (contentVelocity < 0)
|
||||
return flickable.contentY <= minimumContentY() + edgeEpsilon;
|
||||
return false;
|
||||
}
|
||||
|
||||
function finishScroll() {
|
||||
if (!flickable)
|
||||
return;
|
||||
finishTimer.stop();
|
||||
|
||||
if (hasOvershoot()) {
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
kineticAnim.stop();
|
||||
bounceBack();
|
||||
return;
|
||||
}
|
||||
|
||||
var rawContentVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, velocity * kineticGain));
|
||||
var contentVelocity = clampedContentVelocity(rawContentVelocity);
|
||||
var useKinetic = kineticCandidate && Math.abs(rawContentVelocity) >= minVelocity;
|
||||
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
|
||||
if (useKinetic) {
|
||||
if (atEdgeFor(rawContentVelocity)) {
|
||||
// Already at the edge — fling bounces immediately.
|
||||
pendingKineticBounceVelocity = 0;
|
||||
triggerKineticBounce(rawContentVelocity);
|
||||
} else if (Math.abs(contentVelocity) >= minVelocity && runKineticGlide(contentVelocity)) {
|
||||
// Glide started — bounce fires on kineticAnim.onFinished
|
||||
// if it lands at an edge.
|
||||
pendingKineticBounceVelocity = contentVelocity;
|
||||
} else {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
flickable.returnToBounds();
|
||||
}
|
||||
} else {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
flickable.returnToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(wheel) {
|
||||
wheelHandler.blocking = true;
|
||||
if (!enabled || !visible || !flickable || !flickable.visible || !flickable.interactive) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wheel.modifiers !== Qt.NoModifier) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (maximumContentY() <= minimumContentY()) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = deltaFor(wheel);
|
||||
if (delta === 0) {
|
||||
wheelHandler.blocking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any in-flight glide — user input always wins, position
|
||||
// stays where it is, then new delta is applied from there.
|
||||
if (kineticAnim.running) {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
kineticAnim.stop();
|
||||
}
|
||||
kineticBounceSequence.stop();
|
||||
bounceBackAnimation.stop();
|
||||
if (flickable.flicking) {
|
||||
pendingKineticBounceVelocity = 0;
|
||||
pendingImpactVelocity = 0;
|
||||
flickable.cancelFlick();
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var isTouchpad = !isMouseWheel(wheel) && (wheel.pixelDelta.y !== 0 || (wheel.phase !== Qt.NoScrollPhase && wheel.angleDelta.y === 0));
|
||||
console.log("wheel", wheel.angleDelta.y, wheel.pixelDelta.y, wheel.phase);
|
||||
if (!isTouchpad) {
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
kineticCandidate = false;
|
||||
scrollStarted();
|
||||
handleMouseDelta(delta, now);
|
||||
return;
|
||||
}
|
||||
|
||||
// Touchpad path (unchanged below), but cancel any mouse glide first
|
||||
stopMouseGlide();
|
||||
|
||||
var gap = lastWheelTimestamp > 0 ? now - lastWheelTimestamp : 16;
|
||||
var dt = Math.max(8, Math.min(24, gap));
|
||||
var instantVelocity = (delta / dt) * 1000;
|
||||
|
||||
kineticCandidate = kineticCandidate || shouldUseKinetic(wheel, gap);
|
||||
scrollStarted();
|
||||
scrollBy(delta);
|
||||
|
||||
if (hasOvershoot()) {
|
||||
overshootReleaseVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, instantVelocity));
|
||||
velocity = 0;
|
||||
kineticCandidate = false;
|
||||
lastWheelTimestamp = now;
|
||||
finishTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
velocity = lastWheelTimestamp <= 0 ? instantVelocity : velocity * (1.0 - smoothing) + instantVelocity * smoothing;
|
||||
lastWheelTimestamp = now;
|
||||
finishTimer.restart();
|
||||
|
||||
if (wheel.phase === Qt.ScrollEnd)
|
||||
finishScroll();
|
||||
}
|
||||
|
||||
function stopMouseGlide() {
|
||||
mouseGlide.stop();
|
||||
mouseVelocity = 0;
|
||||
mouseDir = 0;
|
||||
mouseStreak = 0;
|
||||
mouseLastTime = 0;
|
||||
}
|
||||
|
||||
function handleMouseDelta(delta, now) {
|
||||
var dir = delta > 0 ? 1 : -1;
|
||||
|
||||
// Edge or active overshoot: existing rubber-band path
|
||||
if (hasOvershoot() || atEdgeFor(dir)) {
|
||||
stopMouseGlide();
|
||||
scrollBy(delta);
|
||||
overshootReleaseVelocity = 0;
|
||||
finishTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
var continuous = dir === mouseDir && (now - mouseLastTime) <= mouseAccelWindow;
|
||||
if (!continuous)
|
||||
mouseVelocity = 0; // new gesture or reversal: start clean
|
||||
mouseStreak = continuous ? mouseStreak + 1 : 0;
|
||||
|
||||
// Compute the gap BEFORE overwriting mouseLastTime
|
||||
var gap = mouseLastTime > 0 ? Math.max(12, Math.min(mouseAccelWindow, now - mouseLastTime)) : 0;
|
||||
|
||||
mouseDir = dir;
|
||||
mouseLastTime = now;
|
||||
|
||||
var accel = Math.min(mouseMaxAccel, 1 + mouseStreak * mouseAccelStep);
|
||||
|
||||
// --- velocity update (this is the part that changed) ---
|
||||
|
||||
// Part of the notch lands immediately
|
||||
var instant = delta * mouseSnap;
|
||||
flickable.contentY = boundedContentY(flickable.contentY + instant);
|
||||
|
||||
// The rest becomes glide velocity, stacked onto the current velocity
|
||||
var glide = delta * (1 - mouseSnap) * accel;
|
||||
var v = mouseVelocity + glide / mouseGlideTime;
|
||||
|
||||
// While notches keep arriving, don't fall below the rate they imply
|
||||
if (continuous && gap > 0) {
|
||||
var rate = glide / (gap / 1000);
|
||||
if (Math.abs(rate) > Math.abs(v))
|
||||
v = rate;
|
||||
}
|
||||
mouseVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, v));
|
||||
|
||||
// --- end velocity update ---
|
||||
|
||||
if (!mouseGlide.running)
|
||||
mouseGlide.start();
|
||||
}
|
||||
|
||||
function stepMouseGlide(dt) {
|
||||
if (!flickable || flickable.dragging) {
|
||||
stopMouseGlide();
|
||||
return;
|
||||
}
|
||||
|
||||
dt = Math.min(dt, 0.05);
|
||||
var decay = Math.exp(-dt / mouseGlideTime);
|
||||
// Exact integral of v0 * e^(-t/tau) over dt, so it's frame-rate independent
|
||||
var step = mouseVelocity * mouseGlideTime * (1 - decay);
|
||||
var next = flickable.contentY + step;
|
||||
var minY = minimumContentY();
|
||||
var maxY = maximumContentY();
|
||||
|
||||
if (next <= minY || next >= maxY) {
|
||||
var impact = mouseVelocity;
|
||||
flickable.contentY = next <= minY ? minY : maxY;
|
||||
mouseGlide.stop();
|
||||
mouseVelocity = 0;
|
||||
mouseDir = 0;
|
||||
mouseStreak = 0;
|
||||
triggerKineticBounce(impact); // bounce scales with the real impact speed
|
||||
return;
|
||||
}
|
||||
|
||||
flickable.contentY = next;
|
||||
mouseVelocity *= decay;
|
||||
|
||||
if (Math.abs(mouseVelocity) < 40) // was 8
|
||||
stopMouseGlide();
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
id: mouseGlide
|
||||
onTriggered: root.stepMouseGlide(frameTime)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: finishTimer
|
||||
interval: 45
|
||||
repeat: false
|
||||
onTriggered: root.finishScroll()
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: kineticAnim
|
||||
target: root.flickable
|
||||
property: "contentY"
|
||||
easing.type: Easing.OutQuart
|
||||
onFinished: {
|
||||
// Prefer the impact-time velocity (computed in runKineticGlide)
|
||||
// so the bounce matches the residual energy at the moment the
|
||||
// glide hit the edge — slow landings → soft bounce, fast
|
||||
// landings → big springy bounce.
|
||||
var bv = Math.abs(root.pendingImpactVelocity) > root.edgeEpsilon ? root.pendingImpactVelocity : root.pendingKineticBounceVelocity;
|
||||
root.pendingImpactVelocity = 0;
|
||||
root.pendingKineticBounceVelocity = 0;
|
||||
if (Math.abs(bv) > root.edgeEpsilon)
|
||||
root.triggerKineticBounce(bv);
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: bounceBackAnimation
|
||||
target: root
|
||||
property: "overshootInput"
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: kineticBounceSequence
|
||||
|
||||
NumberAnimation {
|
||||
id: kineticBounceIn
|
||||
target: root
|
||||
property: "overshootInput"
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: kineticBounceOut
|
||||
target: root
|
||||
property: "overshootInput"
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Translate {
|
||||
id: overshootTranslate
|
||||
y: root.visualOvershoot
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.flickable
|
||||
|
||||
function onInteractiveChanged() {
|
||||
if (!root.flickable.interactive)
|
||||
root.stopAndSettle();
|
||||
}
|
||||
|
||||
function onContentHeightChanged() {
|
||||
if (!root.flickable.moving) {
|
||||
root.flickable.contentY = root.boundedContentY(root.flickable.contentY);
|
||||
root.bounceBack();
|
||||
}
|
||||
}
|
||||
|
||||
function onHeightChanged() {
|
||||
if (!root.flickable.moving) {
|
||||
root.flickable.contentY = root.boundedContentY(root.flickable.contentY);
|
||||
root.bounceBack();
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibleChanged() {
|
||||
if (!root.flickable.visible)
|
||||
root.stopAndSettle();
|
||||
}
|
||||
|
||||
function onMovementEnded() {
|
||||
if (Math.abs(root.pendingKineticBounceVelocity) > root.edgeEpsilon) {
|
||||
var bounceVelocity = root.pendingKineticBounceVelocity;
|
||||
root.pendingKineticBounceVelocity = 0;
|
||||
root.triggerKineticBounce(bounceVelocity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!flickable)
|
||||
return;
|
||||
flickable.flickDeceleration = 2800;
|
||||
flickable.maximumFlickVelocity = Math.max(flickable.maximumFlickVelocity, maxVelocity);
|
||||
flickable.contentItem.transform = [overshootTranslate];
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -34,7 +34,7 @@ MouseArea {
|
||||
cursorShape: expanded ? Qt.ArrowCursor : undefined
|
||||
enabled: expanded
|
||||
hoverEnabled: expanded
|
||||
layer.enabled: opacity < 1
|
||||
layer.enabled: false
|
||||
opacity: expanded ? 1 : 0
|
||||
parent: {
|
||||
const win = QsWindow.window;
|
||||
@@ -101,8 +101,7 @@ MouseArea {
|
||||
yScale: root.expanded ? 1 : 0.1
|
||||
|
||||
Behavior on yScale {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,8 +136,7 @@ MouseArea {
|
||||
radius: menu.radius - column.anchors.margins
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
|
||||
@@ -12,7 +12,7 @@ TextField {
|
||||
implicitHeight: contentHeight + topPadding + bottomPadding
|
||||
implicitWidth: contentWidth + leftPadding + rightPadding
|
||||
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
|
||||
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
|
||||
renderType: TextField.QtRendering
|
||||
selectedTextColor: color
|
||||
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Effects
|
||||
|
||||
CustomFlickable {
|
||||
@@ -9,6 +10,7 @@ CustomFlickable {
|
||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real fadeAmount: 0.1
|
||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
readonly property real dpr: Math.ceil((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
|
||||
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts
|
||||
@@ -28,6 +30,7 @@ CustomFlickable {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
layer.textureSize: Qt.size(width * dpr, height * dpr)
|
||||
layer.effect: Mask {
|
||||
maskSource: mask
|
||||
|
||||
|
||||
@@ -2,55 +2,57 @@ import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
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
|
||||
|
||||
ClipRect {
|
||||
anchors.bottom: parent.bottom
|
||||
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;
|
||||
}
|
||||
anchors.top: parent.top
|
||||
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
|
||||
implicitWidth: handle.implicitWidth
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
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
|
||||
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-261
@@ -4,14 +4,11 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import ZShell
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
import qs.Paths
|
||||
import qs.Services
|
||||
import ZShell.Config
|
||||
|
||||
Singleton {
|
||||
@@ -19,11 +16,9 @@ Singleton {
|
||||
|
||||
readonly property var appCooldownMap: new Map()
|
||||
property alias dnd: props.dnd
|
||||
property list<Notif> list: []
|
||||
property bool loaded
|
||||
readonly property list<Notif> notClosed: list.filter(n => !n.closed)
|
||||
readonly property list<Notif> popups: list.filter(n => n.popup)
|
||||
property alias server: server
|
||||
property alias list: store.rows
|
||||
readonly property list<NotificationRow> notClosed: store.rows.filter(n => !n.closed)
|
||||
readonly property list<NotificationRow> popups: store.rows.filter(n => n.popup)
|
||||
|
||||
function shouldThrottle(appName: string): bool {
|
||||
if (props.dnd || ShellState.anySidebarOpen())
|
||||
@@ -46,33 +41,6 @@ Singleton {
|
||||
return true;
|
||||
}
|
||||
|
||||
onListChanged: {
|
||||
if (loaded) {
|
||||
saveTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
interval: 1000
|
||||
|
||||
onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({
|
||||
time: n.time,
|
||||
id: n.id,
|
||||
summary: n.summary,
|
||||
body: n.body,
|
||||
appIcon: n.appIcon,
|
||||
appName: n.appName,
|
||||
image: n.image,
|
||||
expireTimeout: n.expireTimeout,
|
||||
urgency: n.urgency,
|
||||
resident: n.resident,
|
||||
hasActionIcons: n.hasActionIcons,
|
||||
actions: n.actions
|
||||
}))))
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: props
|
||||
|
||||
@@ -81,6 +49,12 @@ Singleton {
|
||||
reloadableId: "notifs"
|
||||
}
|
||||
|
||||
NotificationStore {
|
||||
id: store
|
||||
|
||||
path: `${Paths.state}/notifs.sqlite`
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
@@ -89,40 +63,12 @@ Singleton {
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
keepOnReload: false
|
||||
keepOnReload: true
|
||||
persistenceSupported: true
|
||||
|
||||
onNotification: notif => {
|
||||
notif.tracked = true;
|
||||
|
||||
const is_popup = root.shouldThrottle(notif.appName);
|
||||
|
||||
const comp = notifComp.createObject(root, {
|
||||
popup: is_popup,
|
||||
notification: notif
|
||||
});
|
||||
root.list = [comp, ...root.list];
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: storage
|
||||
|
||||
path: `${Paths.state}/notifs.json`
|
||||
printErrors: false
|
||||
|
||||
onLoadFailed: err => {
|
||||
if (err === FileViewError.FileNotFound) {
|
||||
root.loaded = true;
|
||||
Qt.callLater(() => setText("[]"));
|
||||
}
|
||||
}
|
||||
onLoaded: {
|
||||
const data = JSON.parse(text());
|
||||
for (const notif of data)
|
||||
root.list.push(notifComp.createObject(root, notif));
|
||||
root.list.sort((a, b) => b.time - a.time);
|
||||
root.loaded = true;
|
||||
store.insert(notif, root.shouldThrottle(notif.appName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,16 +76,12 @@ Singleton {
|
||||
description: "Clear all notifications"
|
||||
name: "clearnotifs"
|
||||
|
||||
onPressed: {
|
||||
for (const notif of root.list.slice())
|
||||
notif.close();
|
||||
}
|
||||
onPressed: store.clear()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function clear(): void {
|
||||
for (const notif of root.list.slice())
|
||||
notif.close();
|
||||
store.clear();
|
||||
}
|
||||
|
||||
function disableDnd(): void {
|
||||
@@ -160,192 +102,4 @@ Singleton {
|
||||
|
||||
target: "notifs"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notifComp
|
||||
|
||||
Notif {
|
||||
}
|
||||
}
|
||||
|
||||
component Notif: QtObject {
|
||||
id: notif
|
||||
|
||||
property list<var> actions
|
||||
property string appIcon
|
||||
property string appName
|
||||
property string body
|
||||
property string cachedImageSource: ""
|
||||
property bool cachingImage: false
|
||||
property bool closed
|
||||
readonly property Connections conn: Connections {
|
||||
function onActionsChanged(): void {
|
||||
notif.actions = notif.notification.actions.map(a => ({
|
||||
identifier: a.identifier,
|
||||
text: a.text,
|
||||
invoke: () => a.invoke()
|
||||
}));
|
||||
}
|
||||
|
||||
function onAppIconChanged(): void {
|
||||
notif.appIcon = notif.notification.appIcon;
|
||||
}
|
||||
|
||||
function onAppNameChanged(): void {
|
||||
notif.appName = notif.notification.appName;
|
||||
}
|
||||
|
||||
function onBodyChanged(): void {
|
||||
notif.body = notif.notification.body;
|
||||
}
|
||||
|
||||
function onClosed(): void {
|
||||
notif.close();
|
||||
}
|
||||
|
||||
function onExpireTimeoutChanged(): void {
|
||||
notif.expireTimeout = notif.notification.expireTimeout;
|
||||
}
|
||||
|
||||
function onHasActionIconsChanged(): void {
|
||||
notif.hasActionIcons = notif.notification.hasActionIcons;
|
||||
}
|
||||
|
||||
function onHintsChanged(): void {
|
||||
notif.hints = notif.notification.hints;
|
||||
}
|
||||
|
||||
function onImageChanged(): void {
|
||||
notif.imageSource = notif.notification.image || "";
|
||||
notif.image = notif.imageSource;
|
||||
notif.cacheImageIfNeeded();
|
||||
}
|
||||
|
||||
function onResidentChanged(): void {
|
||||
notif.resident = notif.notification.resident;
|
||||
}
|
||||
|
||||
function onSummaryChanged(): void {
|
||||
notif.summary = notif.notification.summary;
|
||||
}
|
||||
|
||||
function onUrgencyChanged(): void {
|
||||
notif.urgency = notif.notification.urgency;
|
||||
}
|
||||
|
||||
target: notif.notification
|
||||
}
|
||||
property real expireTimeout: 5
|
||||
property bool hasActionIcons
|
||||
property var hints
|
||||
property string image
|
||||
property string imageSource
|
||||
property var locks: new Set()
|
||||
property string notifId
|
||||
property Notification notification
|
||||
property bool popup
|
||||
property bool resident
|
||||
property string summary
|
||||
property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
const diff = Time.date.getTime() - time.getTime();
|
||||
const m = Math.floor(diff / 60000);
|
||||
|
||||
if (m < 1)
|
||||
return qsTr("now");
|
||||
|
||||
const h = Math.floor(m / 60);
|
||||
const d = Math.floor(h / 24);
|
||||
|
||||
if (d > 0)
|
||||
return `${d}d`;
|
||||
if (h > 0)
|
||||
return `${h}h`;
|
||||
return `${m}m`;
|
||||
}
|
||||
readonly property Timer timer: Timer {
|
||||
property bool paused: false
|
||||
property int remainingTime: totalTime
|
||||
property int totalTime: Config.notifs.defaultExpireTimeout
|
||||
|
||||
interval: 50
|
||||
repeat: true
|
||||
running: !paused
|
||||
|
||||
onTriggered: {
|
||||
remainingTime -= interval;
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
remainingTime = 0;
|
||||
notif.popup = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
property int urgency: NotificationUrgency.Normal
|
||||
|
||||
function cacheImageIfNeeded(): void {
|
||||
const source = imageSource;
|
||||
|
||||
if (!source || cachingImage)
|
||||
return;
|
||||
|
||||
if (cachedImageSource === source)
|
||||
return;
|
||||
|
||||
cachingImage = true;
|
||||
|
||||
ZShellIo.cacheImage(Qt.resolvedUrl(source), Paths.notifimagecache, (path, url) => {
|
||||
cachedImageSource = source;
|
||||
image = path;
|
||||
cachingImage = false;
|
||||
}, () => {
|
||||
cachingImage = false;
|
||||
});
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
closed = true;
|
||||
if (locks.size === 0 && root.list.includes(this)) {
|
||||
root.list = root.list.filter(n => n !== this);
|
||||
notification?.dismiss();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function lock(item: Item): void {
|
||||
locks.add(item);
|
||||
}
|
||||
|
||||
function unlock(item: Item): void {
|
||||
locks.delete(item);
|
||||
if (closed)
|
||||
close();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!notification)
|
||||
return;
|
||||
|
||||
notifId = notification.id;
|
||||
summary = notification.summary;
|
||||
body = notification.body;
|
||||
appIcon = notification.appIcon;
|
||||
appName = notification.appName;
|
||||
imageSource = notification.image || "";
|
||||
image = imageSource;
|
||||
hints = notification.hints;
|
||||
expireTimeout = notification.expireTimeout;
|
||||
urgency = notification.urgency;
|
||||
resident = notification.resident;
|
||||
hasActionIcons = notification.hasActionIcons;
|
||||
actions = notification.actions.map(a => ({
|
||||
identifier: a.identifier,
|
||||
text: a.text,
|
||||
invoke: () => a.invoke()
|
||||
}));
|
||||
|
||||
cacheImageIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,6 @@ Scope {
|
||||
hasBar: root.geometry.barOnBottom
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
|
||||
onTriggered: console.log("top height:", top.exclusiveZone, "left width:", left.exclusiveZone, "right width:", right.exclusiveZone, "bottom height:", bottom.exclusiveZone)
|
||||
}
|
||||
|
||||
component ExclusionZone: CustomWindow {
|
||||
property bool hasBar
|
||||
|
||||
@@ -59,7 +52,6 @@ Scope {
|
||||
name: "Bar-Exclusion"
|
||||
screen: root.screen
|
||||
|
||||
mask: Region {
|
||||
}
|
||||
mask: Region {}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -3,8 +3,8 @@ import QtQuick
|
||||
import qs.Components
|
||||
import qs.Modules.Bar.Popouts as Popouts
|
||||
import qs.Modules.Notifications as Notifications
|
||||
import qs.Modules.Notifications.Sidebar as Sidebar
|
||||
import qs.Modules.Notifications.Sidebar.Utils as Utils
|
||||
import qs.Modules.Sidebar as Sidebar
|
||||
import qs.Modules.Sidebar.Utils as Utils
|
||||
import qs.Modules.Dashboard as Dashboard
|
||||
import qs.Modules.Osd as Osd
|
||||
import qs.Components.Toast as Toasts
|
||||
@@ -19,15 +19,11 @@ import ZShell.Config
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item bar
|
||||
required property real borderThickness
|
||||
readonly property alias clipboard: clipboard
|
||||
readonly property alias dashboard: dashboard
|
||||
readonly property alias dashboardWrapper: dashboardWrapper
|
||||
readonly property alias dock: dock
|
||||
readonly property alias drawing: drawing
|
||||
required property var drawingItem
|
||||
required property EdgeGeometry geometry
|
||||
readonly property alias launcher: launcher
|
||||
readonly property alias notifications: notifications
|
||||
readonly property alias osd: osd
|
||||
@@ -36,14 +32,19 @@ Item {
|
||||
readonly property alias popoutsWrapper: popouts
|
||||
readonly property alias resources: resources
|
||||
readonly property alias resourcesWrapper: resourcesWrapper
|
||||
required property ShellScreen screen
|
||||
required property ScreenState screenState
|
||||
readonly property alias settings: settings
|
||||
readonly property alias settingsWrapper: settingsWrapper
|
||||
readonly property alias sidebar: sidebar
|
||||
readonly property alias toasts: toasts
|
||||
readonly property alias utilities: utilities
|
||||
|
||||
required property Item bar
|
||||
required property real borderThickness
|
||||
required property var drawingItem
|
||||
required property EdgeGeometry geometry
|
||||
required property ShellScreen screen
|
||||
required property ScreenState screenState
|
||||
|
||||
anchors.bottomMargin: geometry.insetBottom(borderThickness)
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: geometry.insetLeft(borderThickness)
|
||||
|
||||
+12
-28
@@ -27,27 +27,21 @@ ColumnLayout {
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: Colors.palette.m3secondary
|
||||
font.bold: true
|
||||
font.family: Config.appearance.font.family.clock
|
||||
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
|
||||
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
|
||||
text: Time.hourStr
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: Colors.palette.m3primary
|
||||
font.bold: true
|
||||
font.family: Config.appearance.font.family.clock
|
||||
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
|
||||
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
|
||||
text: ":"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: Colors.palette.m3secondary
|
||||
font.bold: true
|
||||
font.family: Config.appearance.font.family.clock
|
||||
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
|
||||
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
|
||||
text: Time.minuteStr
|
||||
}
|
||||
}
|
||||
@@ -56,13 +50,11 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: -Tokens.padding.large * 2
|
||||
color: Colors.palette.m3tertiary
|
||||
font.bold: true
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale)
|
||||
font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build()
|
||||
text: Time.format("dddd, d MMMM yyyy")
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Tokens.spacing.largeIncreased * 2
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
@@ -73,7 +65,7 @@ ColumnLayout {
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Math.floor(root.centerWidth / 4)
|
||||
fontStyle: Tokens.font.icon.size(root.centerWidth / 4).build()
|
||||
text: "person"
|
||||
}
|
||||
|
||||
@@ -88,9 +80,7 @@ ColumnLayout {
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
font.weight: 600
|
||||
font: Tokens.font.body.large
|
||||
text: root.greeter.username
|
||||
visible: text.length > 0
|
||||
}
|
||||
@@ -155,8 +145,7 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +180,6 @@ ColumnLayout {
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.greeter.buffer && !root.greeter.launching ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
font.weight: 500
|
||||
text: root.greeter.launching ? "hourglass_top" : "arrow_forward"
|
||||
}
|
||||
}
|
||||
@@ -204,8 +192,7 @@ ColumnLayout {
|
||||
implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
@@ -245,12 +232,10 @@ ColumnLayout {
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onMsgChanged: {
|
||||
@@ -292,8 +277,7 @@ ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: isError ? Colors.palette.m3error : Colors.palette.m3primary
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.small
|
||||
font: Tokens.font.mono.medium
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
@@ -20,7 +20,7 @@ BusyIndicator {
|
||||
property int animState
|
||||
property color bgColor: Colors.palette.m3secondaryContainer
|
||||
property color fgColor: Colors.palette.m3primary
|
||||
property real implicitSize: Tokens.font.size.normal * 3
|
||||
property real implicitSize: Tokens.font.body.medium.pointSize * 3
|
||||
property real internalStrokeWidth: strokeWidth
|
||||
readonly property alias progress: manager.progress
|
||||
property real strokeWidth: Tokens.padding.extraSmall * 0.8
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import ZShell.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real radius
|
||||
property real topLeftRadius: radius
|
||||
property real topRightRadius: radius
|
||||
property real bottomRightRadius: radius
|
||||
property real bottomLeftRadius: radius
|
||||
property color color: "transparent"
|
||||
default property alias contentData: contentItem.data
|
||||
|
||||
Item {
|
||||
id: contentItem
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
ShaderEffectSource {
|
||||
id: source
|
||||
|
||||
hideSource: true
|
||||
sourceItem: contentItem
|
||||
}
|
||||
|
||||
RoundedMask {
|
||||
anchors.fill: parent
|
||||
source: source
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
color: root.color
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,36 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Flickable {
|
||||
id: root
|
||||
|
||||
property bool doneFakeFlick
|
||||
|
||||
interactive: !ShellState.forActive().isDrawing
|
||||
maximumFlickVelocity: 3000
|
||||
|
||||
rebound: Transition {
|
||||
onRunningChanged: {
|
||||
if (!running && !root.doneFakeFlick) {
|
||||
root.doneFakeFlick = true;
|
||||
root.flick(1, 1);
|
||||
root.flick(-1, -1);
|
||||
Qt.callLater(() => root.cancelFlick());
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 10
|
||||
running: root.doneFakeFlick
|
||||
|
||||
onTriggered: {
|
||||
root.doneFakeFlick = false;
|
||||
root.returnToBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
|
||||
property bool doneFakeFlick
|
||||
|
||||
interactive: !ShellState.forActive()?.isDrawing
|
||||
maximumFlickVelocity: 3000
|
||||
|
||||
rebound: Transition {
|
||||
// onRunningChanged: {
|
||||
// if (!running && !root.doneFakeFlick) {
|
||||
// root.doneFakeFlick = true;
|
||||
// root.flick(1, 1);
|
||||
// root.flick(-1, -1);
|
||||
// Qt.callLater(() => root.cancelFlick());
|
||||
// }
|
||||
// }
|
||||
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
// Timer {
|
||||
// interval: 10
|
||||
// running: root.doneFakeFlick
|
||||
//
|
||||
// onTriggered: {
|
||||
// root.doneFakeFlick = false;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ MouseArea {
|
||||
property int scrollAccumulatedY: 0
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
onWheel: event => {
|
||||
|
||||
@@ -6,7 +6,6 @@ Rectangle {
|
||||
color: "transparent"
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,9 @@ Text {
|
||||
property real animateTo: 1
|
||||
|
||||
color: Colors.palette.m3onSurface
|
||||
font.family: Config.appearance.font.family.sans
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
font: Tokens.font.body.small
|
||||
linkColor: Colors.palette.m3onPrimaryFixedVariant
|
||||
renderType: Text.NativeRendering
|
||||
renderType: Text.QtRendering
|
||||
textFormat: Text.PlainText
|
||||
|
||||
Behavior on color {
|
||||
@@ -28,22 +27,25 @@ Text {
|
||||
enabled: root.animate
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
TAnim {
|
||||
target: root
|
||||
to: root.animateFrom
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
}
|
||||
|
||||
Anim {
|
||||
TAnim {
|
||||
target: root
|
||||
to: root.animateTo
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
component TAnim: Anim {
|
||||
duration: root.animateDuration / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
properties: root.animateProp.split(",").length > 1 ? root.animateProp : ""
|
||||
property: root.animateProp.split(",").length === 1 ? root.animateProp : ""
|
||||
target: root
|
||||
|
||||
@@ -1,76 +1,305 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Shapes
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
TextField {
|
||||
TextFieldBase {
|
||||
id: root
|
||||
|
||||
background: null
|
||||
color: Colors.palette.m3onSurface
|
||||
cursorVisible: !readOnly
|
||||
font.family: Config.appearance.font.family.sans
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
placeholderTextColor: Colors.palette.m3outline
|
||||
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
|
||||
enum TextFieldType {
|
||||
Outlined,
|
||||
Filled
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
|
||||
readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText
|
||||
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
|
||||
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
|
||||
readonly property real smallFontScale: smallFontSize / font.pointSize
|
||||
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
|
||||
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
|
||||
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
|
||||
|
||||
property int horizontalPadding: Tokens.padding.large
|
||||
property int verticalPadding: Tokens.padding.large
|
||||
|
||||
property int radius: Tokens.rounding.small
|
||||
|
||||
property string supportingText
|
||||
property string trailingIcon
|
||||
property int smallFontSize: Tokens.font.label.small.pointSize
|
||||
|
||||
property int type: CustomTextField.Outlined
|
||||
property var validate // Regex or function
|
||||
property bool emptyIsValid: true
|
||||
property string errorText
|
||||
property bool isError
|
||||
property string leadingIcon
|
||||
|
||||
bottomPadding: verticalPadding + supportingTextOffset - filledOffset
|
||||
leftPadding: horizontalPadding + leadingOffset
|
||||
rightPadding: horizontalPadding + trailingOffset
|
||||
topPadding: verticalPadding + filledOffset
|
||||
|
||||
background: Loader {
|
||||
anchors.bottomMargin: root.supportingTextOffset
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.type === CustomTextField.Filled ? filledComp : outlineComp
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
cursorShape: Qt.IBeamCursor
|
||||
enabled: !root.activeFocus
|
||||
manualPressOverride: tapHandler.pressed
|
||||
radius: root.type === CustomTextField.Outlined ? root.clampedRadius : 0
|
||||
topLeftRadius: root.clampedRadius
|
||||
topRightRadius: root.clampedRadius
|
||||
|
||||
onClicked: root.focus = true
|
||||
}
|
||||
}
|
||||
cursorDelegate: CustomRect {
|
||||
id: cursor
|
||||
|
||||
property bool disableBlink
|
||||
onEditingFinished: {
|
||||
if (!valid)
|
||||
isError = true;
|
||||
}
|
||||
onPressed: {
|
||||
if (stateLayer.enabled)
|
||||
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
|
||||
}
|
||||
onTextEdited: {
|
||||
if (isError)
|
||||
isError = false;
|
||||
}
|
||||
|
||||
color: Colors.palette.m3primary
|
||||
implicitWidth: 2
|
||||
radius: Tokens.rounding.medium
|
||||
Item {
|
||||
id: contentWrapper
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.small
|
||||
}
|
||||
}
|
||||
anchors.bottomMargin: root.supportingTextOffset
|
||||
anchors.fill: parent
|
||||
|
||||
Connections {
|
||||
function onCursorPositionChanged(): void {
|
||||
if (root.activeFocus && root.cursorVisible) {
|
||||
cursor.opacity = 1;
|
||||
cursor.disableBlink = true;
|
||||
enableBlink.restart();
|
||||
CustomText {
|
||||
id: placeholder
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.leftPadding
|
||||
anchors.topMargin: Tokens.padding.extraSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : root.text ? Colors.palette.m3outline : root.placeholderTextColor)
|
||||
font.family: root.font.family
|
||||
font.pointSize: root.font.pointSize
|
||||
font.variableAxes: root.font.variableAxes
|
||||
font.weight: root.font.weight
|
||||
renderType: Text.QtRendering
|
||||
text: root.placeholderText
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "smallOutlined"
|
||||
when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text)
|
||||
|
||||
PropertyChanges {
|
||||
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding
|
||||
placeholder.scale: root.smallFontScale
|
||||
}
|
||||
|
||||
AnchorChanges {
|
||||
anchors.verticalCenter: contentWrapper.top
|
||||
target: placeholder
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "smallFilled"
|
||||
when: root.type === CustomTextField.Filled && (root.activeFocus || root.text)
|
||||
|
||||
PropertyChanges {
|
||||
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding + root.leadingOffset
|
||||
placeholder.scale: root.smallFontScale
|
||||
}
|
||||
|
||||
AnchorChanges {
|
||||
anchors.top: contentWrapper.top
|
||||
anchors.verticalCenter: undefined
|
||||
target: placeholder
|
||||
}
|
||||
}
|
||||
]
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "scale,leftMargin"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
AnchorAnim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultEffects
|
||||
easing: Tokens.anim.expressiveDefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: enableBlink
|
||||
Loader {
|
||||
id: leadingIconLoader
|
||||
|
||||
interval: 100
|
||||
active: root.leadingIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
onTriggered: cursor.disableBlink = false
|
||||
sourceComponent: MaterialIcon {
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: root.leadingIcon
|
||||
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
|
||||
triggeredOnStart: true
|
||||
Loader {
|
||||
id: trailingIconLoader
|
||||
|
||||
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
|
||||
}
|
||||
active: root.trailingIcon
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Binding {
|
||||
cursor.opacity: 0
|
||||
when: !root.activeFocus || !root.cursorVisible
|
||||
sourceComponent: MaterialIcon {
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: root.trailingIcon
|
||||
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on placeholderTextColor {
|
||||
CAnim {
|
||||
|
||||
Loader {
|
||||
id: supportingTextLoader
|
||||
|
||||
active: root.effectiveSupportingText
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.horizontalPadding
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: root.isError ? Colors.palette.m3error : Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.label.small
|
||||
text: root.effectiveSupportingText
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
}
|
||||
|
||||
Component {
|
||||
id: outlineComp
|
||||
|
||||
Shape {
|
||||
id: bg
|
||||
|
||||
asynchronous: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: path
|
||||
|
||||
readonly property real inset: strokeWidth / 2
|
||||
readonly property real outlineGap: placeholder.width * root.smallFontScale + Tokens.spacing.small * 2
|
||||
property real outlineGapScale: root.activeFocus || root.text ? 1 : 0
|
||||
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
startX: path.inset + root.horizontalPadding - root.clampedRadius + path.outlineGap * (1 - path.outlineGapScale) / 2 + path.outlineGap * path.outlineGapScale
|
||||
startY: path.inset
|
||||
strokeColor: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
|
||||
strokeWidth: root.activeFocus ? 2 : 1
|
||||
|
||||
Behavior on outlineGapScale {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on strokeColor {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on strokeWidth {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: bg.width - path.inset - root.clampedRadius
|
||||
y: path.inset
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: bg.width - path.inset
|
||||
y: path.inset + root.clampedRadius
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: bg.width - path.inset
|
||||
y: bg.height - path.inset - root.clampedRadius
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: bg.width - path.inset - root.clampedRadius
|
||||
y: bg.height - path.inset
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: path.inset + root.clampedRadius
|
||||
y: bg.height - path.inset
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: path.inset
|
||||
y: bg.height - path.inset - root.clampedRadius
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: path.inset
|
||||
y: path.inset + root.clampedRadius
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: path.inset + root.clampedRadius
|
||||
y: path.inset
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: path.inset + root.horizontalPadding - root.clampedRadius / 2 + path.outlineGap * (1 - path.outlineGapScale) / 2
|
||||
y: path.inset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: filledComp
|
||||
|
||||
CustomRect {
|
||||
color: root.activeFocus ? Colors.tPalette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainerHigh
|
||||
topLeftRadius: root.clampedRadius
|
||||
topRightRadius: root.clampedRadius
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
|
||||
implicitHeight: root.activeFocus ? 2 : 1
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
TextInput {
|
||||
renderType: Text.NativeRendering
|
||||
selectedTextColor: Colors.palette.m3onSecondaryContainer
|
||||
selectionColor: Colors.tPalette.m3onSecondaryContainer
|
||||
selectionColor: Colors.tPalette.colSecondaryContainer
|
||||
|
||||
font {
|
||||
family: Config.appearance?.font.family.sans ?? "sans-serif"
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
pixelSize: Tokens?.font.size.normal ?? 15
|
||||
pixelSize: Config.appearance?.font.size.normal ?? 15
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property int fadeInAnim: Anim.DefaultEffects
|
||||
property int fadeInLargeAnim: Anim.StandardLarge
|
||||
property int fadeOutAnim: Anim.FastEffects
|
||||
property bool fadingOut
|
||||
property bool hadPrevious
|
||||
property bool preventInit
|
||||
|
||||
function maybeStartInAnim(): void {
|
||||
if (!preventInit && !opacityInAnim.running && status === Image.Ready) {
|
||||
opacityInAnim.type = hadPrevious ? fadeInAnim : fadeInLargeAnim;
|
||||
opacityInAnim.start();
|
||||
}
|
||||
}
|
||||
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: 0
|
||||
retainWhileLoading: true
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Anim on opacity {
|
||||
id: opacityInAnim
|
||||
|
||||
running: false
|
||||
to: 1
|
||||
}
|
||||
Behavior on source {
|
||||
SequentialAnimation {
|
||||
ScriptAction {
|
||||
script: opacityInAnim.stop()
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "fadingOut"
|
||||
target: root
|
||||
value: true
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: root
|
||||
to: 0
|
||||
type: root.fadeOutAnim
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "fadingOut"
|
||||
target: root
|
||||
value: false
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "hadPrevious"
|
||||
target: root
|
||||
value: root.source
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: root.maybeStartInAnim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPreventInitChanged: maybeStartInAnim()
|
||||
onStatusChanged: maybeStartInAnim()
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import M3Shapes
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
MaterialShape {
|
||||
id: root
|
||||
|
||||
property bool animated: true
|
||||
property real cRotation
|
||||
property bool containsIcon
|
||||
property real dampingRatio: 0.6
|
||||
property real lRotation
|
||||
property int morphAnimRotation: 60
|
||||
property real morphScale: 0.14
|
||||
property alias rotateAnimDuration: rotateAnim.duration
|
||||
property int shapeIndex
|
||||
property list<int> shapes: {
|
||||
if (containsIcon)
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
}
|
||||
readonly property real springDuration: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const r = -dampingRatio * wn;
|
||||
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
return Math.log(visibilityThreshold / c) / r;
|
||||
}
|
||||
readonly property real springMaxVelocity: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
|
||||
return wn * factor;
|
||||
}
|
||||
property bool springSettled: true
|
||||
property real stiffness: 180
|
||||
property real thisLRotation
|
||||
property real visibilityThreshold: 0.075
|
||||
|
||||
function spring(t: real): var {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const za = dampingRatio * wn;
|
||||
|
||||
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
const r = za / wd;
|
||||
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
|
||||
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
|
||||
|
||||
return [pos, vel];
|
||||
}
|
||||
|
||||
color: Colors.palette.m3primary
|
||||
implicitSize: 38
|
||||
toShape: shapes[0]
|
||||
|
||||
RotationAnimation on cRotation {
|
||||
id: rotateAnim
|
||||
|
||||
duration: 4666
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
running: root.animated
|
||||
to: 360
|
||||
}
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
running: root.animated && !root.springSettled
|
||||
|
||||
onTriggered: {
|
||||
const t = timer.elapsed();
|
||||
|
||||
if (t >= root.springDuration) {
|
||||
root.springSettled = true;
|
||||
} else {
|
||||
const [pos, vel] = root.spring(t);
|
||||
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
|
||||
root.thisLRotation = pos * root.morphAnimRotation;
|
||||
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 650
|
||||
repeat: true
|
||||
running: root.animated
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
root.beginBatchUpdate();
|
||||
root.fromShape = root.toShape;
|
||||
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
|
||||
root.toShape = root.shapes[root.shapeIndex];
|
||||
root.morphProgress = 0;
|
||||
|
||||
root.rotation = root.rotation;
|
||||
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
|
||||
root.thisLRotation = 0;
|
||||
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
|
||||
|
||||
root.springSettled = false;
|
||||
timer.restart();
|
||||
root.endBatchUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
CustomText {
|
||||
property real fill
|
||||
property int grade: Colors.light ? 0 : -25
|
||||
id: root
|
||||
|
||||
font.family: "Material Symbols Rounded"
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
font.variableAxes: ({
|
||||
FILL: fill.toFixed(1),
|
||||
GRAD: grade,
|
||||
opsz: fontInfo.pixelSize,
|
||||
wght: fontInfo.weight
|
||||
})
|
||||
property real fill: 0
|
||||
property int grade: Colors.light ? 0 : -25
|
||||
property font fontStyle: Tokens.font.icon.small
|
||||
|
||||
renderType: Text.NativeRendering
|
||||
font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import ZShell
|
||||
import ZShell.Components
|
||||
import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
@@ -13,7 +11,6 @@ MouseArea {
|
||||
property alias bottomRightRadius: base.bottomRightRadius
|
||||
property real circleRadius
|
||||
property alias color: base.color
|
||||
property bool disabled
|
||||
readonly property real endRadius: {
|
||||
const d1 = distSq(0, 0);
|
||||
const d2 = distSq(width, 0);
|
||||
@@ -22,6 +19,7 @@ MouseArea {
|
||||
return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3;
|
||||
}
|
||||
property real endRadiusAtPress
|
||||
property bool manualHoverOverride
|
||||
property bool manualPressOverride
|
||||
property real pressX: width / 2
|
||||
property real pressY: height / 2
|
||||
@@ -29,7 +27,7 @@ MouseArea {
|
||||
readonly property alias rect: base
|
||||
property bool shapeMorph
|
||||
property bool showHoverBackground: true
|
||||
property real stateOpacity: containsMouse ? 0.08 : 0
|
||||
property real stateOpacity: containsMouse || manualHoverOverride ? 0.08 : 0
|
||||
property alias topLeftRadius: base.topLeftRadius
|
||||
property alias topRightRadius: base.topRightRadius
|
||||
|
||||
@@ -52,8 +50,8 @@ MouseArea {
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: disabled ? undefined : Qt.PointingHandCursor
|
||||
enabled: !disabled
|
||||
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
hoverEnabled: true
|
||||
|
||||
Behavior on stateOpacity {
|
||||
@@ -100,13 +98,13 @@ MouseArea {
|
||||
id: base
|
||||
|
||||
anchors.fill: parent
|
||||
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
|
||||
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
|
||||
color: Colors.palette.m3onSurface
|
||||
opacity: root.stateOpacity
|
||||
// Pick up radius from parent if it has one (parent can be anything with radius props)
|
||||
// qmllint disable missing-property
|
||||
radius: root.parent?.radius ?? 0
|
||||
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
|
||||
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
|
||||
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
|
||||
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
|
||||
// qmllint enable missing-property
|
||||
|
||||
+16
-39
@@ -13,34 +13,16 @@ RowLayout {
|
||||
|
||||
spacing: Tokens.spacing.largeIncreased * 2
|
||||
|
||||
ColumnLayout {
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.medium
|
||||
topLeftRadius: Tokens.rounding.largeIncreased
|
||||
bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: weather.implicitHeight
|
||||
radius: Tokens.rounding.small
|
||||
topLeftRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
WeatherInfo {
|
||||
id: weather
|
||||
|
||||
rootHeight: root.height
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
UserDock {
|
||||
greeter: root.greeter
|
||||
}
|
||||
UserDock {
|
||||
greeter: root.greeter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,21 +31,16 @@ RowLayout {
|
||||
screenHeight: root.screenHeight
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.medium
|
||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
topRightRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
topRightRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
SessionDock {
|
||||
greeter: root.greeter
|
||||
}
|
||||
SessionDock {
|
||||
greeter: root.greeter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +134,7 @@ Scope {
|
||||
id: lockIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
font.bold: true
|
||||
font.pointSize: Tokens.font.size.extraLarge * 4
|
||||
fontStyle: Tokens.font.icon.builders.extraLarge.size(Tokens.font.icon.extraLarge.pointSize * 3).build()
|
||||
text: "lock"
|
||||
}
|
||||
|
||||
|
||||
+8
-15
@@ -39,8 +39,7 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
color: root.greeter.launching ? Colors.palette.m3secondary : Colors.palette.m3outline
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
font: Tokens.font.mono.medium
|
||||
opacity: root.buffer ? 0 : 1
|
||||
text: {
|
||||
if (root.greeter.launching)
|
||||
@@ -51,8 +50,7 @@ Item {
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,15 +62,13 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3onSurface
|
||||
elide: Text.ElideLeft
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
font: Tokens.font.mono.medium
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
opacity: root.greeter.echoResponse && root.buffer ? 1 : 0
|
||||
text: root.buffer
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +85,7 @@ Item {
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0
|
||||
implicitHeight: Tokens.font.size.normal
|
||||
implicitHeight: Tokens.font.body.medium
|
||||
implicitWidth: fullWidth
|
||||
interactive: false
|
||||
opacity: root.greeter.echoResponse ? 0 : 1
|
||||
@@ -107,8 +103,7 @@ Item {
|
||||
scale: 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
@@ -156,15 +151,13 @@ Item {
|
||||
Behavior on implicitWidth {
|
||||
id: imWidthBehavior
|
||||
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: root.buffer.split("")
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-17
@@ -13,22 +13,19 @@ ColumnLayout {
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.small
|
||||
spacing: Tokens.spacing.large
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.weight: 500
|
||||
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
||||
text: root.greeter.sessions.length > 0 ? qsTr("%1 session%2").arg(root.greeter.sessions.length).arg(root.greeter.sessions.length === 1 ? "" : "s") : qsTr("Sessions")
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
@@ -46,16 +43,14 @@ ColumnLayout {
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font.pointSize: Tokens.font.size.extraLarge * 2
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: "desktop_windows"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.large
|
||||
font.weight: 500
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Sessions Found")
|
||||
}
|
||||
}
|
||||
@@ -68,6 +63,7 @@ ColumnLayout {
|
||||
clip: true
|
||||
currentIndex: root.greeter.sessionIndex
|
||||
highlightFollowsCurrentItem: false
|
||||
boundsBehavior: ListView.StopAtBounds
|
||||
model: root.greeter.sessions
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
@@ -80,7 +76,7 @@ ColumnLayout {
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
implicitHeight: row.implicitHeight + Tokens.padding.small * 2
|
||||
radius: Tokens.rounding.medium - Tokens.padding.small
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
StateLayer {
|
||||
onClicked: {
|
||||
@@ -97,7 +93,7 @@ ColumnLayout {
|
||||
|
||||
MaterialIcon {
|
||||
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
text: modelData.kind === "x11" ? "tv" : "desktop_windows"
|
||||
}
|
||||
|
||||
@@ -109,8 +105,7 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
font.weight: 600
|
||||
font: Tokens.font.body.builders.medium.weight(Font.Bold).build()
|
||||
text: modelData.name
|
||||
}
|
||||
|
||||
@@ -118,8 +113,7 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimaryFixedVariant : Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.small
|
||||
font: Tokens.font.mono.small
|
||||
text: modelData.kind
|
||||
}
|
||||
}
|
||||
@@ -129,7 +123,7 @@ ColumnLayout {
|
||||
color: Colors.palette.m3primary
|
||||
implicitHeight: sessions.currentItem?.implicitHeight ?? 0
|
||||
implicitWidth: sessions.width
|
||||
radius: Tokens.rounding.medium - Tokens.padding.small
|
||||
radius: Tokens.rounding.large
|
||||
y: sessions.currentItem?.y ?? 0
|
||||
|
||||
Behavior on y {
|
||||
|
||||
+11
-16
@@ -14,22 +14,19 @@ ColumnLayout {
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.small
|
||||
spacing: Tokens.spacing.large
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.weight: 500
|
||||
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
||||
text: root.greeter.users.length > 0 ? qsTr("%1 user%2").arg(root.greeter.users.length).arg(root.greeter.users.length === 1 ? "" : "s") : qsTr("Users")
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
@@ -48,16 +45,14 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
fill: 1
|
||||
font.pointSize: Tokens.font.size.extraLarge * 2
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: "account_circle"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.large
|
||||
font.weight: 500
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Users Found")
|
||||
}
|
||||
}
|
||||
@@ -70,6 +65,7 @@ ColumnLayout {
|
||||
clip: true
|
||||
currentIndex: root.greeter.selectedUser ? root.greeter.users.indexOf(root.greeter.selectedUser) : -1
|
||||
highlightFollowsCurrentItem: false
|
||||
boundsBehavior: ListView.StopAtBounds
|
||||
model: root.greeter.users
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
@@ -82,7 +78,7 @@ ColumnLayout {
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
implicitHeight: row.implicitHeight + Tokens.padding.small * 2
|
||||
radius: Tokens.rounding.medium - Tokens.padding.small
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
StateLayer {
|
||||
onClicked: {
|
||||
@@ -97,7 +93,7 @@ ColumnLayout {
|
||||
anchors.margins: Tokens.padding.small
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
Layout.preferredHeight: 50
|
||||
Layout.preferredWidth: 50
|
||||
radius: Tokens.rounding.full
|
||||
@@ -106,7 +102,7 @@ ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
|
||||
fill: 1
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
text: modelData.kind === "x11" ? "tv" : "account_circle"
|
||||
}
|
||||
|
||||
@@ -124,8 +120,7 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
font.weight: 600
|
||||
font: Tokens.font.body.builders.medium.weight(Font.Bold).build()
|
||||
text: modelData.username
|
||||
}
|
||||
}
|
||||
@@ -135,7 +130,7 @@ ColumnLayout {
|
||||
color: Colors.palette.m3primary
|
||||
implicitHeight: users.currentItem?.implicitHeight ?? 0
|
||||
implicitWidth: users.width
|
||||
radius: Tokens.rounding.medium - Tokens.padding.small
|
||||
radius: Tokens.rounding.large
|
||||
y: users.currentItem?.y ?? 0
|
||||
|
||||
Behavior on y {
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.large * 2
|
||||
anchors.right: parent.right
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: -Tokens.padding.large
|
||||
Layout.topMargin: Tokens.padding.large * 2
|
||||
active: root.rootHeight > 610
|
||||
visible: active
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: Colors.palette.m3primary
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
font.weight: 500
|
||||
text: qsTr("Weather")
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: Colors.palette.m3secondary
|
||||
font.pointSize: Tokens.font.size.extraLarge * 2.5
|
||||
text: Weather.icon
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: Colors.palette.m3secondary
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.large
|
||||
font.weight: 500
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: qsTr("Humidity: %1%").arg(Weather.humidity)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.rightMargin: Tokens.padding.small
|
||||
active: root.width > 400
|
||||
visible: active
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: Colors.palette.m3primary
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
font.weight: 500
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: forecastLoader
|
||||
|
||||
Layout.bottomMargin: Tokens.padding.large * 2
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Tokens.spacing.small
|
||||
active: root.rootHeight > 820
|
||||
visible: active
|
||||
|
||||
sourceComponent: RowLayout {
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const forecast = Weather.hourlyForecast;
|
||||
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
|
||||
if (!forecast)
|
||||
return Array.from({
|
||||
length: count
|
||||
}, () => null);
|
||||
|
||||
const start = Math.max(0, Weather.currentHourIndex);
|
||||
return forecast.slice(start, start + count);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: forecastHour
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: {
|
||||
const hour = forecastHour.modelData?.hour ?? 0;
|
||||
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pointSize: Tokens.font.size.extraLarge * 1.5
|
||||
font.weight: 500
|
||||
text: forecastHour.modelData?.icon ?? "cloud_alert"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3secondary
|
||||
font.pointSize: Tokens.font.size.larger
|
||||
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 900000 // 15 minutes
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: Weather.reload()
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ Singleton {
|
||||
|
||||
function toggleNightLight(): void {
|
||||
service.manualToggle = true;
|
||||
service.toggle();
|
||||
service.toggle(true);
|
||||
}
|
||||
|
||||
NightlightManager {
|
||||
|
||||
+105
-43
@@ -5,7 +5,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import ZShell.Models
|
||||
import ZShell.Internal
|
||||
import ZShell.Wallpaper
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
@@ -16,24 +16,48 @@ Searcher {
|
||||
id: root
|
||||
|
||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||
property alias crops: adapter.monitorCrops
|
||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||
property alias monitorCrops: monitorCrops
|
||||
readonly property string currentThumbnail: thumbnailFor(current)
|
||||
readonly property bool currentIsVideo: isVideoPath(current)
|
||||
property bool pendingPreviewClear
|
||||
property bool previewColorLock
|
||||
property string previewPath
|
||||
property bool recentlyChanged
|
||||
property bool showPreview: false
|
||||
property int cropVersion: 0
|
||||
|
||||
function getCategoryFor(w: FileSystemEntry): string {
|
||||
function getCategoryFor(w: WallpaperEntry): string {
|
||||
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
||||
if (category.includes("/"))
|
||||
category = category.slice(0, category.indexOf("/"));
|
||||
return category;
|
||||
}
|
||||
|
||||
readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"]
|
||||
|
||||
function isVideoPath(path: string): bool {
|
||||
if (!path)
|
||||
return false;
|
||||
const dot = path.lastIndexOf(".");
|
||||
if (dot < 0)
|
||||
return false;
|
||||
return videoExts.includes(path.slice(dot + 1).toLowerCase());
|
||||
}
|
||||
|
||||
function thumbnailFor(path: string): string {
|
||||
if (!path)
|
||||
return "";
|
||||
|
||||
for (const entry of root.list) {
|
||||
if (entry.path === path)
|
||||
return entry.thumbnailPath || path;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function getCrop(screen: string): var {
|
||||
return root.crops[screen];
|
||||
return wallpapers.crop(root.current, screen);
|
||||
}
|
||||
|
||||
function preview(path: string): void {
|
||||
@@ -50,19 +74,18 @@ Searcher {
|
||||
else if (zoom > 5.0)
|
||||
zoom = 5.0;
|
||||
|
||||
root.crops[screen] = {
|
||||
wallpapers.setCrop(root.current, screen, {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
zoom: zoom
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
WallpaperPath.currentWallpaperPath = path;
|
||||
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}`]);
|
||||
if (Config.colors.schemeGen)
|
||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
|
||||
@@ -96,38 +119,13 @@ Searcher {
|
||||
target: "wallpaper"
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: monitorCrops
|
||||
|
||||
path: `${Paths.state}/wallpaper-crops.json`
|
||||
watchChanges: true
|
||||
|
||||
onAdapterUpdated: cropWriteDelay.restart()
|
||||
onFileChanged: reload()
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property var monitorCrops: ({})
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: cropWriteDelay
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: monitorCrops.writeAdapter()
|
||||
}
|
||||
|
||||
FileSystemModel {
|
||||
WallpaperDb {
|
||||
id: wallpapers
|
||||
|
||||
filter: FileSystemModel.Images
|
||||
dbPath: `${Paths.state}/wallpapers.sqlite`
|
||||
cacheSize: wallpaper.largestBufferSize
|
||||
requireHardwareCodec: true
|
||||
path: Config.general.wallpaperPath
|
||||
recursive: true
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -146,36 +144,100 @@ Searcher {
|
||||
WallpaperService {
|
||||
id: wallpaper
|
||||
|
||||
dbPath: wallpapers.dbPath
|
||||
path: root.current
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: wallpapers
|
||||
|
||||
function onCacheStored(path: string): void {
|
||||
if (path === root.current)
|
||||
wallpaper.invalidateCache();
|
||||
}
|
||||
|
||||
function onCropsChanged(path: string): void {
|
||||
if (path === root.current)
|
||||
root.cropVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: screenVariants
|
||||
model: Quickshell.screens
|
||||
|
||||
WallpaperObject {
|
||||
readonly property var crops: root.getCrop(modelData.name)
|
||||
id: obj
|
||||
required property var modelData
|
||||
|
||||
cropHeight: crops?.height ?? 1.0
|
||||
cropWidth: crops?.width ?? 1.0
|
||||
cropX: crops?.x ?? 0.0
|
||||
cropY: crops?.y ?? 0.0
|
||||
// Crop animation only for edits, never for wallpaper swaps.
|
||||
property bool animateCrop: false
|
||||
|
||||
screen: modelData.name
|
||||
service: wallpaper
|
||||
zoom: crops?.zoom ?? 1.0
|
||||
|
||||
// Apply the wallpaper's own crop. Driven off the service's pathChanged
|
||||
// (which fires after applyPath has snapshotted the outgoing frame with
|
||||
// the outgoing crop) and off crop edits, never as a reactive binding on
|
||||
// the wallpaper path — so a swap applies the incoming crop atomically
|
||||
// after the base is captured.
|
||||
function seedCrop(): void {
|
||||
// Load the current wallpaper's crop without pushing: the service
|
||||
// already read it from the database. This only seeds the values so
|
||||
// a later edit animates from the right starting point.
|
||||
obj.syncCrop(root.getCrop(modelData.name));
|
||||
}
|
||||
|
||||
// Apply an edited crop: assign via the setters so the Behaviors animate
|
||||
// the numbers and push each intermediate value to the service.
|
||||
function editCrop(): void {
|
||||
const c = root.getCrop(modelData.name);
|
||||
obj.cropX = c?.x ?? 0.0;
|
||||
obj.cropY = c?.y ?? 0.0;
|
||||
obj.cropWidth = c?.width ?? 1.0;
|
||||
obj.cropHeight = c?.height ?? 1.0;
|
||||
obj.zoom = c?.zoom ?? 1.0;
|
||||
}
|
||||
|
||||
Component.onCompleted: seedCrop()
|
||||
onScreenChanged: seedCrop()
|
||||
|
||||
Behavior on cropHeight {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
Behavior on cropWidth {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
Behavior on cropX {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
Behavior on cropY {
|
||||
enabled: obj.animateCrop
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: wallpaper
|
||||
function onPathChanged(): void {
|
||||
for (const obj of screenVariants.instances) {
|
||||
obj.animateCrop = false;
|
||||
obj.seedCrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCropVersionChanged(): void {
|
||||
for (const obj of screenVariants.instances) {
|
||||
obj.animateCrop = true;
|
||||
obj.editCrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -210,8 +210,7 @@ GridLayout {
|
||||
roleValue: "network"
|
||||
|
||||
delegate: EntryWrapper {
|
||||
NetworkWidget {
|
||||
}
|
||||
NetworkWidget {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item bar
|
||||
required property ScreenState screenState
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -1
|
||||
color: Colors.palette.m3surface
|
||||
layer.enabled: true
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskInverted: true
|
||||
maskSource: mask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Config.bar.border + 1
|
||||
anchors.topMargin: root.bar.implicitHeight + 1
|
||||
radius: Config.bar.border > 0 ? Config.bar.rounding : 0
|
||||
topLeftRadius: Config.bar.rounding
|
||||
topRightRadius: Config.bar.rounding
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,9 @@ WidgetBase {
|
||||
}
|
||||
required property GridLayout loader
|
||||
required property Wrapper popouts
|
||||
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||
readonly property int roundedWidth: Math.round(timeText.implicitWidth)
|
||||
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
|
||||
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
|
||||
required property ScreenState screenState
|
||||
|
||||
implicitHeight: horizontal ? baseSize : size
|
||||
|
||||
@@ -24,7 +24,7 @@ Row {
|
||||
color: Battery.getColors(root.percentage, root.devState).bg
|
||||
height: root.batHeight
|
||||
radius: root.radius
|
||||
width: root.batWidth
|
||||
width: root.batWidth - 1
|
||||
|
||||
CustomText {
|
||||
color: Battery.getColors(root.percentage, root.devState).fg
|
||||
|
||||
@@ -13,7 +13,7 @@ WidgetBase {
|
||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||
|
||||
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
|
||||
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
|
||||
implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Behavior on implicitHeight {
|
||||
|
||||
@@ -12,7 +12,6 @@ GridLayout {
|
||||
property real animatedPercentage: 0
|
||||
readonly property real arcStartAngle: 0.75 * Math.PI
|
||||
readonly property real arcSweep: 1.5 * Math.PI
|
||||
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
|
||||
required property bool horizontal
|
||||
property string icon
|
||||
required property color iconColor
|
||||
@@ -23,17 +22,12 @@ GridLayout {
|
||||
property bool warning: percentage * 100 >= warningThreshold
|
||||
property int warningThreshold: 80
|
||||
|
||||
function snap(v) {
|
||||
return Math.round(v * dpr) / dpr;
|
||||
}
|
||||
|
||||
columnSpacing: Tokens.spacing.medium
|
||||
columns: horizontal ? -1 : 1
|
||||
percentage: 0
|
||||
|
||||
Behavior on animatedPercentage {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Component.onCompleted: animatedPercentage = percentage
|
||||
@@ -59,7 +53,7 @@ GridLayout {
|
||||
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
|
||||
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||
radius: Math.min(width, height) / 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Item {
|
||||
id: clipper
|
||||
@@ -69,18 +63,20 @@ GridLayout {
|
||||
anchors.left: root.horizontal ? undefined : parent.left
|
||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||
clip: true
|
||||
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
|
||||
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
|
||||
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
|
||||
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: root.horizontal ? parent.bottom : undefined
|
||||
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
||||
anchors.left: root.horizontal ? undefined : parent.left
|
||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||
|
||||
color: root.mainColor
|
||||
height: track.height
|
||||
radius: track.radius
|
||||
width: track.width
|
||||
|
||||
implicitHeight: track.height
|
||||
implicitWidth: track.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ WidgetBase {
|
||||
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
|
||||
anchors.verticalCenterOffset: root.horizontal ? 0 : -3
|
||||
columnSpacing: Tokens.spacing.small
|
||||
rowSpacing: 0
|
||||
columns: root.horizontal ? -1 : 1
|
||||
|
||||
ServiceRef {
|
||||
@@ -51,6 +52,7 @@ WidgetBase {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillHeight: root.horizontal
|
||||
Layout.fillWidth: !root.horizontal
|
||||
|
||||
horizontal: root.horizontal
|
||||
icon: "memory"
|
||||
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
|
||||
@@ -31,7 +31,7 @@ WidgetBase {
|
||||
return -1;
|
||||
}
|
||||
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
|
||||
readonly property int spacing: Tokens.spacing.medium / 2
|
||||
readonly property int spacing: Tokens.spacing.extraSmall
|
||||
|
||||
// Entries that can shrink to nothing, spacing included
|
||||
function collapsed(entry: var): bool {
|
||||
@@ -51,14 +51,12 @@ WidgetBase {
|
||||
Behavior on implicitHeight {
|
||||
enabled: !root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.horizontal
|
||||
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
@@ -70,6 +68,8 @@ WidgetBase {
|
||||
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
|
||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||
columns: root.horizontal ? -1 : 1
|
||||
columnSpacing: root.spacing
|
||||
rowSpacing: root.spacing
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
@@ -111,8 +111,7 @@ WidgetBase {
|
||||
delegate: EntryWrapper {
|
||||
name: "upower"
|
||||
|
||||
UPowerWidget {
|
||||
}
|
||||
UPowerWidget {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,8 +134,8 @@ WidgetBase {
|
||||
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
|
||||
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
|
||||
children: item
|
||||
implicitHeight: item?.implicitHeight ?? 0
|
||||
implicitWidth: item?.implicitWidth ?? 0
|
||||
implicitHeight: item?.height ?? item?.implicitHeight ?? 0
|
||||
implicitWidth: item?.width ?? item?.implicitWidth ?? 0
|
||||
|
||||
Behavior on leftGap {
|
||||
Anim {
|
||||
|
||||
@@ -6,7 +6,7 @@ import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MaterialIcon {
|
||||
id: speaker
|
||||
id: root
|
||||
|
||||
required property bool horizontal
|
||||
|
||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
||||
fill: 1
|
||||
fontStyle: Tokens.font.icon.small
|
||||
text: Audio.muted ? "volume_off" : "volume_up"
|
||||
width: Math.round(implicitWidth)
|
||||
height: Math.round(implicitHeight)
|
||||
|
||||
Behavior on Layout.maximumWidth {
|
||||
Anim {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MaterialIcon {
|
||||
id: mic
|
||||
id: root
|
||||
|
||||
required property bool horizontal
|
||||
|
||||
@@ -15,6 +15,8 @@ MaterialIcon {
|
||||
fill: 1
|
||||
fontStyle: Tokens.font.icon.small
|
||||
text: Audio.sourceMuted ? "mic_off" : "mic"
|
||||
width: Math.round(implicitWidth)
|
||||
height: Math.round(implicitHeight)
|
||||
|
||||
Behavior on Layout.maximumWidth {
|
||||
Anim {}
|
||||
|
||||
@@ -19,7 +19,7 @@ Item {
|
||||
required property Wrapper popouts
|
||||
|
||||
function resolveIcon(app: string, icon: string): string {
|
||||
if (app === "chrome_status_icon_1") {
|
||||
if (app === "discord_status_icon_1" && Config.bar.tray.replaceIcons) {
|
||||
return Quickshell.iconPath("discord-tray");
|
||||
} else if (app === "AyuGramDesktop") {
|
||||
if (icon === Quickshell.iconPath("com.ayugram.desktop-attention-symbolic"))
|
||||
@@ -50,6 +50,8 @@ Item {
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
id: layer
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
anchors.fill: parent
|
||||
color: icon.layer.enabled && root.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
@@ -77,6 +79,7 @@ Item {
|
||||
|
||||
anchors.centerIn: parent
|
||||
antialiasing: true
|
||||
backer.retainWhileLoading: true
|
||||
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
implicitSize: Config.bar.tray.trayIconSize * root.dpr
|
||||
layer.enabled: Config.bar.tray.recolorIcons
|
||||
|
||||
@@ -38,6 +38,7 @@ WidgetBase {
|
||||
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
|
||||
columns: root.horizontal ? -1 : 1
|
||||
rowSpacing: -2
|
||||
columnSpacing: Tokens.spacing.extraSmall
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
@@ -12,13 +12,14 @@ import qs.Services
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
|
||||
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
|
||||
readonly property int spacing: Tokens.spacing.extraSmall / 2
|
||||
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
||||
required property bool horizontal
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||
required property ShellScreen screen
|
||||
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
|
||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
|
||||
readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2
|
||||
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
|
||||
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
|
||||
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
|
||||
@@ -59,15 +60,17 @@ Item {
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.activeWorkspaceMargin
|
||||
columnSpacing: 0
|
||||
columnSpacing: root.spacing
|
||||
columns: root.horizontal ? root.workspacesShown : 1
|
||||
rowSpacing: 0
|
||||
rowSpacing: root.spacing
|
||||
rows: root.horizontal ? 1 : root.workspacesShown
|
||||
layer.enabled: true
|
||||
layer.mipmap: true
|
||||
|
||||
Repeater {
|
||||
model: root.workspaces
|
||||
|
||||
Button {
|
||||
CustomRect {
|
||||
id: button
|
||||
|
||||
required property int index
|
||||
@@ -75,27 +78,19 @@ Item {
|
||||
|
||||
implicitHeight: indicator.indicatorThickness
|
||||
implicitWidth: indicator.indicatorThickness
|
||||
width: root.workspaceButtonWidth
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
background: Item {
|
||||
id: workspaceButtonBackground
|
||||
|
||||
implicitHeight: root.workspaceButtonWidth
|
||||
implicitWidth: root.workspaceButtonWidth
|
||||
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: button.modelData?.name ?? ""
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
text: button.modelData?.name ?? ""
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
const ws = button.modelData.name;
|
||||
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
||||
StateLayer {
|
||||
onPressed: {
|
||||
const ws = button.modelData.name;
|
||||
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,8 +99,10 @@ Item {
|
||||
CustomRect {
|
||||
id: indicator
|
||||
|
||||
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth
|
||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
|
||||
readonly property real step: root.workspaceButtonWidth + root.spacing
|
||||
|
||||
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth + Math.abs(idxPair.idx1 - idxPair.idx2) * root.spacing
|
||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * step + root.activeWorkspaceMargin
|
||||
property real indicatorThickness: root.workspaceButtonWidth
|
||||
|
||||
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
|
||||
@@ -129,9 +126,10 @@ Item {
|
||||
implicitHeight: grid.height
|
||||
implicitWidth: grid.width
|
||||
source: grid
|
||||
autoPaddingEnabled: false
|
||||
sourceColor: Colors.palette.m3onSurface
|
||||
x: root.horizontal ? -indicator.x + 3 : 0
|
||||
y: root.horizontal ? 0 : -indicator.y + 3
|
||||
x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0
|
||||
y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ Item {
|
||||
y: parent.y
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +100,7 @@ Item {
|
||||
value: Audio.volume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setVolume(value)
|
||||
@@ -123,7 +121,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 65 + Tokens.spacing.small * 2
|
||||
Layout.topMargin: root.topMargin
|
||||
@@ -136,17 +134,21 @@ Item {
|
||||
node: Audio.source
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: sourcePeakFill
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * sourcePeak.peak
|
||||
CustomRect {
|
||||
id: sourcePeakFill
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * sourcePeak.peak
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,8 +197,7 @@ Item {
|
||||
value: Audio.sourceVolume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setSourceVolume(value)
|
||||
@@ -230,7 +231,7 @@ Item {
|
||||
|
||||
model: Audio.streams.filter(s => s.isSink)
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: appBox
|
||||
|
||||
required property int index
|
||||
@@ -248,17 +249,21 @@ Item {
|
||||
node: appBox.modelData
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: peakFill
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * peak.peak
|
||||
CustomRect {
|
||||
id: peakFill
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * peak.peak
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,8 +321,7 @@ Item {
|
||||
value: appBox.modelData.audio.volume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setStreamVolume(appBox.modelData, value)
|
||||
|
||||
@@ -9,7 +9,7 @@ import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0
|
||||
@@ -73,6 +73,7 @@ CustomClippingRect {
|
||||
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
|
||||
implicitWidth: contentWidth
|
||||
spacing: Tokens.spacing.medium
|
||||
clip: true
|
||||
|
||||
delegate: CustomRect {
|
||||
id: update
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
|
||||
@@ -25,7 +25,7 @@ Item {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: search
|
||||
|
||||
anchors.left: parent.left
|
||||
@@ -54,7 +54,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: preview
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -64,6 +64,7 @@ Item {
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
|
||||
radius: 25
|
||||
clip: true
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
@@ -228,11 +229,12 @@ Item {
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
id: textRect
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
|
||||
Item {
|
||||
id: textWrapper
|
||||
|
||||
@@ -14,7 +14,7 @@ Row {
|
||||
padding: 20
|
||||
spacing: 12
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: info.implicitHeight
|
||||
implicitWidth: info.implicitHeight
|
||||
|
||||
@@ -16,7 +16,7 @@ Item {
|
||||
implicitWidth: 1000
|
||||
implicitHeight: 320
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
radius: root.radius
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ CustomListView {
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: iconClip
|
||||
|
||||
Layout.fillHeight: true
|
||||
|
||||
@@ -25,7 +25,7 @@ Item {
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: popupBackground
|
||||
readonly property real padding: 4
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Item {
|
||||
visible: false
|
||||
z: 999
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: popupBackground
|
||||
|
||||
readonly property real padding: Tokens.padding.extraSmall
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
@@ -9,7 +11,7 @@ Item {
|
||||
id: root
|
||||
|
||||
required property bool isCurrent
|
||||
required property var modelData // FileSystemEntry-like {path, relativePath}
|
||||
required property var modelData
|
||||
required property real tileHeight
|
||||
required property real tileWidth
|
||||
required property ScreenState screenState
|
||||
@@ -19,7 +21,7 @@ Item {
|
||||
implicitHeight: tileHeight + Tokens.padding.large * 2
|
||||
implicitWidth: tileWidth
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: image
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -29,25 +31,75 @@ Item {
|
||||
radius: Tokens.rounding.small
|
||||
y: Tokens.padding.large
|
||||
|
||||
MaterialIcon {
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
color: Colors.tPalette.m3outline
|
||||
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
|
||||
text: "image"
|
||||
opacity: img.status === Image.Ready ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: CustomRect {
|
||||
color: Colors.palette.m3primaryContainer
|
||||
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
LoadingIndicator {
|
||||
id: loadingIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
containsIcon: true
|
||||
implicitSize: Math.min(image.width, image.height) * 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
Image {
|
||||
id: img
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
cache: true
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
height: root.tileHeight
|
||||
path: root.modelData ? root.modelData.path : ""
|
||||
source: root.modelData ? root.modelData.thumbnailPath : ""
|
||||
smooth: root.isCurrent
|
||||
sourceSize.height: root.tileHeight
|
||||
sourceSize.width: root.tileHeight * (16 / 9)
|
||||
width: root.tileHeight * (16 / 9)
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||
implicitHeight: implicitWidth
|
||||
radius: Tokens.rounding.full
|
||||
color: Colors.tPalette.m3surface
|
||||
|
||||
MaterialIcon {
|
||||
id: videoIcon
|
||||
anchors.centerIn: parent
|
||||
text: "video_library"
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
|
||||
@@ -55,7 +55,7 @@ ColumnLayout {
|
||||
text: Time.format("dddd, d MMMM yyyy")
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Tokens.spacing.largeIncreased * 2
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
|
||||
@@ -5,14 +5,13 @@ import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitWidth: layout.implicitWidth
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
@@ -50,22 +49,10 @@ CustomClippingRect {
|
||||
lock: root.lock
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
NotifDock {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
topRightRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,9 +221,9 @@ WlSessionLockSurface {
|
||||
|
||||
anchors.fill: parent
|
||||
color: Colors.palette.m3surface
|
||||
layer.enabled: true
|
||||
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
|
||||
radius: lockContent.radius
|
||||
layer.enabled: true
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
blurMax: 15
|
||||
|
||||
+32
-27
@@ -2,44 +2,49 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
layer.enabled: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
bottomLeftRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: Colors.palette.m3surface
|
||||
opacity: 0.7
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: Colors.palette.m3surface
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +82,7 @@ CustomClippingRect {
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoPrevious
|
||||
enabled: Players.active?.canGoPrevious ?? false
|
||||
icon: "skip_previous"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
@@ -88,7 +93,7 @@ CustomClippingRect {
|
||||
|
||||
IconButton {
|
||||
checked: Players.active?.isPlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
enabled: Players.active?.canTogglePlaying ?? false
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
|
||||
isRound: true
|
||||
@@ -98,7 +103,7 @@ CustomClippingRect {
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoNext
|
||||
enabled: Players.active?.canGoNext ?? false
|
||||
icon: "skip_next"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
|
||||
+94
-105
@@ -11,127 +11,116 @@ import ZShell.Config
|
||||
import qs.Daemons
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.small
|
||||
bottomRightRadius: Tokens.rounding.largeIncreased
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.small
|
||||
topRightRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.weight: 500
|
||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||
}
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
ClippingRectangle {
|
||||
id: clipRect
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: NotifServer.list.length > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.extraLarge
|
||||
}
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
layer.enabled: true
|
||||
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
|
||||
sourceSize.width: clipRect.width * 0.8
|
||||
|
||||
layer.effect: Coloriser {
|
||||
brightness: 1
|
||||
colorizationColor: Colors.palette.m3outlineVariant
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Notifications")
|
||||
}
|
||||
}
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
spacing: Tokens.spacing.small
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: NotifServer.list.length > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.extraLarge
|
||||
}
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
from: 0
|
||||
property: "scale"
|
||||
to: 1
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Notifications")
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: NotifGroup {}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
||||
return [...new Map(list).keys()];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
CustomListView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 0.6
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
from: 0
|
||||
property: "scale"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: NotifGroup {}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
||||
return [...new Map(list).keys()];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Services
|
||||
import qs.Modules
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
@@ -85,7 +86,7 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) : Colors.palette.m3secondaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
@@ -277,7 +278,7 @@ CustomRect {
|
||||
component NotifLine: CustomText {
|
||||
id: notifLine
|
||||
|
||||
required property NotifServer.Notif modelData
|
||||
required property NotificationRow modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import qs.Paths
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ClippingRectangle {
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.title.large
|
||||
font: Tokens.font.body.large
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ ColumnLayout {
|
||||
|
||||
animate: true
|
||||
color: Colors.palette.m3primary
|
||||
font: Tokens.font.body.large
|
||||
font: Tokens.font.title.large
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ ColumnLayout {
|
||||
animate: true
|
||||
color: Colors.palette.m3secondary
|
||||
text: Weather.icon
|
||||
fontStyle: Tokens.font.icon.large
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.normal
|
||||
font: Tokens.font.body.medium
|
||||
text: {
|
||||
const today = Weather.forecast[0];
|
||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||
|
||||
@@ -36,7 +36,7 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
|
||||
VerticalFadeListView {
|
||||
CustomListView {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -45,13 +45,20 @@ CustomRect {
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentItem.childrenRect.height
|
||||
model: Weather.hourlyForecast
|
||||
currentIndex: Weather.currentHourIndex
|
||||
// highlightFollowsCurrentItem: true
|
||||
// preferredHighlightEnd: currentItem.implicitWidth
|
||||
// preferredHighlightBegin: 0
|
||||
// highlightRangeMode: ListView.ApplyRange
|
||||
clip: true
|
||||
model: Weather.hourlyForecast.slice(Weather.currentHourIndex - 1)
|
||||
orientation: VerticalFadeListView.Horizontal
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: hour
|
||||
|
||||
readonly property bool current: ListView.view.currentItem === this
|
||||
readonly property var cond: modelData
|
||||
required property int index
|
||||
required property var modelData
|
||||
@@ -60,7 +67,7 @@ CustomRect {
|
||||
|
||||
MaterialShape {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
|
||||
color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0)
|
||||
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
|
||||
shape: MaterialShape.Cookie4Sided
|
||||
|
||||
@@ -72,7 +79,7 @@ CustomRect {
|
||||
id: temp
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
font: Tokens.font.body.medium
|
||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||
}
|
||||
@@ -96,7 +103,7 @@ CustomRect {
|
||||
Layout.topMargin: Tokens.spacing.extraSmall
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.small
|
||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||
text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ CustomRect {
|
||||
return base + brief.anchors.topMargin;
|
||||
}
|
||||
radius: Tokens.rounding.small
|
||||
topLeftRadius: Tokens.rounding.largeIncreased
|
||||
|
||||
Timer {
|
||||
interval: 900000 // 15 minutes
|
||||
@@ -48,7 +49,6 @@ CustomRect {
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Forecast {
|
||||
}
|
||||
sourceComponent: Forecast {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
|
||||
@@ -30,31 +32,29 @@ Item {
|
||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
|
||||
return 0;
|
||||
|
||||
if (screenState.dashboard && (panels.dashboard.x + panels.dashboard.width >= panels.notifications.x))
|
||||
return 0;
|
||||
|
||||
if (screenState.osd) {
|
||||
const h = panels.osd.y - Tokens.spacing.small * 2 - padding * 2;
|
||||
if (height > h)
|
||||
height = h;
|
||||
}
|
||||
|
||||
if (screenState.session) {
|
||||
const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2;
|
||||
if (height > h)
|
||||
height = h;
|
||||
}
|
||||
|
||||
return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - 1 * 2, height + padding * 2);
|
||||
}
|
||||
|
||||
implicitWidth: Config.notifs.sizes.width + padding * 2
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ClippingWrapperRectangle {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.large - root.padding
|
||||
clip: true
|
||||
// radius: Tokens.rounding.large - root.padding
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
@@ -98,7 +98,7 @@ Item {
|
||||
|
||||
property int idx
|
||||
required property int index
|
||||
required property NotifServer.Notif modelData
|
||||
required property NotificationRow modelData
|
||||
readonly property alias nonAnimHeight: notif.nonAnimHeight
|
||||
|
||||
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.small)
|
||||
|
||||
@@ -5,7 +5,7 @@ import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Components
|
||||
import qs.Daemons
|
||||
import ZShell.Services
|
||||
import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
@@ -17,7 +17,7 @@ CustomRect {
|
||||
property bool expanded: Config.notifs.openExpanded
|
||||
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
||||
readonly property bool hasImage: modelData.image.length > 0
|
||||
required property NotifServer.Notif modelData
|
||||
required property NotificationRow modelData
|
||||
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
|
||||
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondaryContainer : Colors.tPalette.m3surfaceContainer
|
||||
@@ -56,10 +56,10 @@ CustomRect {
|
||||
if (actions.length === 1)
|
||||
actions[0].invoke();
|
||||
}
|
||||
onEntered: root.modelData.timer.stop()
|
||||
onEntered: root.modelData.pausePopup()
|
||||
onExited: {
|
||||
if (!pressed)
|
||||
root.modelData.timer.start();
|
||||
root.modelData.resumePopup();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
@@ -69,14 +69,14 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
root.modelData.timer.stop();
|
||||
root.modelData.pausePopup();
|
||||
startY = event.y;
|
||||
if (event.button === Qt.MiddleButton)
|
||||
root.modelData.close();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (!containsMouse)
|
||||
root.modelData.timer.start();
|
||||
root.modelData.resumePopup();
|
||||
|
||||
if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold)
|
||||
root.x = 0;
|
||||
@@ -108,7 +108,7 @@ CustomRect {
|
||||
visible: root.hasImage || root.hasAppIcon
|
||||
width: Config.notifs.sizes.image
|
||||
|
||||
sourceComponent: CustomClippingRect {
|
||||
sourceComponent: ClipRect {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
@@ -200,7 +200,7 @@ CustomRect {
|
||||
radiusX: progressIndicator.width / 2 - Tokens.padding.extraSmall / 2
|
||||
radiusY: progressIndicator.height / 2 - Tokens.padding.extraSmall / 2
|
||||
startAngle: -90
|
||||
sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360
|
||||
sweepAngle: (root.modelData.progress / 100) * 360
|
||||
|
||||
Behavior on sweepAngle {
|
||||
Anim {
|
||||
@@ -477,7 +477,12 @@ CustomRect {
|
||||
shapeMorph: true
|
||||
text: modelData.text
|
||||
|
||||
onClicked: modelData.invoke()
|
||||
onClicked: {
|
||||
if (modelData.invokable)
|
||||
modelData.invoke();
|
||||
else if (!root.modelData.resident)
|
||||
root.modelData.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,32 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,619 +0,0 @@
|
||||
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))
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
onCurrentChanged: expanded = false
|
||||
|
||||
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: Tokens.font.icon.medium
|
||||
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: Tokens.font.label.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: Tokens.font.headline.medium
|
||||
color: Colors.palette.m3onSurface
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolsBg
|
||||
|
||||
property bool open: false
|
||||
property Item rootParent: root.rootParent
|
||||
property int maxHeight: rootParent.height - Tokens.padding.medium * 2
|
||||
property int openHeight: Math.min(toolsContent.item?.implicitHeight, maxHeight)
|
||||
property int openWidth: toolsBg.rootParent.width - Tokens.padding.medium * 2
|
||||
property real openHeightProg: openHeight / maxHeight
|
||||
property int collapsedY: 0
|
||||
|
||||
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
|
||||
z: open || dialogTransition.running ? 2 : 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
function reparentWrapper(): void {
|
||||
const newParent = open ? toolsBg.rootParent : toolsBg;
|
||||
const c = toolsWrapper.parent.mapToItem(newParent, toolsWrapper.x + toolsWrapper.width / 2, toolsWrapper.y + toolsWrapper.height / 2);
|
||||
toolsBg.collapsedY = toolsWrapper.y;
|
||||
toolsWrapper.parent = newParent;
|
||||
toolsWrapper.x = c.x - toolsWrapper.width / 2;
|
||||
toolsWrapper.y = c.y - toolsWrapper.height / 2;
|
||||
}
|
||||
|
||||
onOpenChanged: {
|
||||
if (open)
|
||||
toolsBg.rootParent.interactive = false;
|
||||
else
|
||||
toolsBg.rootParent.interactive = true;
|
||||
}
|
||||
|
||||
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
|
||||
rotation: parent === toolsBg.rootParent ? 180 : 0
|
||||
z: 1
|
||||
|
||||
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: (confirmationRow.implicitHeight + Tokens.spacing.small + Tokens.padding.medium) * (1 - toolsBg.openHeightProg) + Tokens.padding.medium
|
||||
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: {
|
||||
toolsBg.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: toolList
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: root.current.pendingToolCalls?.slice(0, 4) ?? []
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: toolText.modelData.name === "webfetch" ? "web" : "text_snippet"
|
||||
fill: 1
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: qsTr("And %1 more").arg(root.current.pendingToolCalls.slice(4).length)
|
||||
visible: repeater.count < root.current.pendingToolCalls.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: toolsContent
|
||||
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
opacity: 0
|
||||
|
||||
sourceComponent: MouseArea {
|
||||
onWheel: event => event.accepted = true
|
||||
preventStealing: true
|
||||
implicitHeight: innerLayout.contentHeight + innerLayout.anchors.margins * 2
|
||||
|
||||
VerticalFadeListView {
|
||||
id: innerLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
cacheBuffer: height * 10
|
||||
fadeAmount: 0.1
|
||||
fadeEnabled: interactive
|
||||
spacing: 0
|
||||
clip: true
|
||||
interactive: contentHeight > toolsBg.maxHeight
|
||||
|
||||
model: root.current.pendingToolCalls ?? []
|
||||
|
||||
delegate: Item {
|
||||
id: toolCall
|
||||
|
||||
required property LlmSegment modelData
|
||||
property bool expanded: false
|
||||
|
||||
implicitWidth: CustomListView.view.width
|
||||
implicitHeight: collapsedRow.implicitHeight + collapsedRow.anchors.margins + (expanded ? expandedTool.implicitHeight + expandedTool.anchors.margins + expandedTool.anchors.topMargin : collapsedRow.anchors.margins)
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: collapsedRow
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
IconButton {
|
||||
id: toolExpandBtn
|
||||
|
||||
font: Tokens.font.icon.medium
|
||||
icon: "keyboard_arrow_down"
|
||||
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
rotation: toolCall.expanded ? 180 : 0
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: toolCall.expanded = !toolCall.expanded
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: preview
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: toolCall.modelData.name
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: expandedTool
|
||||
|
||||
anchors.top: collapsedRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
|
||||
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
|
||||
CustomText {
|
||||
id: fullToolCall
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
text: toolCall.toolTarget(toolCall.modelData)
|
||||
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: confirmationRow
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
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: Tokens.font.body.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: root
|
||||
|
||||
property real animPerc: UPower.displayDevice.percentage
|
||||
|
||||
@@ -4,7 +4,7 @@ import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property color accent
|
||||
@@ -19,15 +19,20 @@ CustomClippingRect {
|
||||
implicitWidth: 450
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(root.accent, 0.05)
|
||||
implicitWidth: parent.width * root.usage
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(root.accent, 0.05)
|
||||
implicitWidth: parent.width * root.usage
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
VerticalFadeFlickable {
|
||||
CustomFlickable {
|
||||
id: flickable
|
||||
|
||||
Layout.fillHeight: true
|
||||
@@ -158,7 +158,8 @@ ColumnLayout {
|
||||
|
||||
bottomMargin: Tokens.padding.extraLarge
|
||||
topMargin: Tokens.padding.large
|
||||
fadeAmount: 0.1
|
||||
clip: true
|
||||
// fadeAmount: 0.1
|
||||
|
||||
contentHeight: root.contentChild?.implicitHeight ?? 0
|
||||
contentItem.children: [root.contentChild]
|
||||
|
||||
@@ -25,7 +25,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: imgWrapper
|
||||
|
||||
Layout.fillWidth: true
|
||||
@@ -39,8 +39,7 @@ Item {
|
||||
opacity: img.status === Image.Ready ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: CustomRect {
|
||||
color: Colors.palette.m3primaryContainer
|
||||
@@ -64,18 +63,43 @@ Item {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
retainWhileLoading: true
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||
implicitHeight: implicitWidth
|
||||
radius: Tokens.rounding.full
|
||||
color: Colors.tPalette.m3surface
|
||||
|
||||
MaterialIcon {
|
||||
id: videoIcon
|
||||
anchors.centerIn: parent
|
||||
text: "video_library"
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,14 @@ import qs.Services
|
||||
Item {
|
||||
id: wrapper
|
||||
|
||||
property int buttonRowHeight: Tokens.padding.large * 3
|
||||
readonly property bool isVideo: Wallpapers.currentIsVideo
|
||||
readonly property string currentWallpaper: Wallpapers.current
|
||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||
readonly property real maxZoom: 5.0
|
||||
readonly property bool changesMade: {
|
||||
cropRevision;
|
||||
Wallpapers.cropVersion;
|
||||
wrapper.currentWallpaper;
|
||||
|
||||
const r = cropRectLoader.item;
|
||||
if (!r || !currentScreen || scaledImg.status !== Image.Ready)
|
||||
@@ -25,9 +30,9 @@ Item {
|
||||
|
||||
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
|
||||
}
|
||||
|
||||
property int buttonRowHeight: Tokens.padding.large * 3
|
||||
property int cropRevision: 0
|
||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
||||
readonly property real maxZoom: 5.0
|
||||
property var screens: []
|
||||
property int selectedScreenIndex: 0
|
||||
property bool shouldBeActive: true
|
||||
@@ -100,29 +105,28 @@ Item {
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
Behavior on y {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
refreshScreens();
|
||||
Qt.callLater(syncCropToScreen);
|
||||
}
|
||||
onCurrentWallpaperChanged: {
|
||||
Qt.callLater(syncCropToScreen);
|
||||
}
|
||||
onSelectedScreenIndexChanged: {
|
||||
Qt.callLater(syncCropToScreen);
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: screenSelector
|
||||
|
||||
Loader {
|
||||
id: selectorLoader
|
||||
anchors.bottom: imageSlot.top
|
||||
anchors.bottomMargin: Tokens.spacing.medium
|
||||
anchors.left: parent.left
|
||||
@@ -130,25 +134,32 @@ Item {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.extraLarge * 2
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Repeater {
|
||||
model: wrapper.screens
|
||||
active: wrapper.screens.length > 1 && !wrapper.isVideo
|
||||
|
||||
delegate: TextButton {
|
||||
required property int index
|
||||
readonly property bool isCurrent: wrapper.selectedScreenIndex === index
|
||||
required property var modelData
|
||||
sourceComponent: ButtonRow {
|
||||
id: screenSelector
|
||||
|
||||
fillWidth: true
|
||||
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
|
||||
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
|
||||
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
text: modelData.name
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
onClicked: wrapper.selectScreen(index)
|
||||
Repeater {
|
||||
model: wrapper.screens
|
||||
|
||||
delegate: TextButton {
|
||||
required property int index
|
||||
readonly property bool isCurrent: wrapper.selectedScreenIndex === index
|
||||
required property var modelData
|
||||
|
||||
fillWidth: true
|
||||
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
|
||||
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
|
||||
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
text: modelData.name
|
||||
|
||||
onClicked: wrapper.selectScreen(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +171,7 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: 30
|
||||
enabled: !wrapper.isVideo
|
||||
|
||||
CustomSlider {
|
||||
id: zoomSlider
|
||||
@@ -222,9 +234,9 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: wrapper.buttonRowHeight + Tokens.spacing.medium
|
||||
anchors.topMargin: selectorLoader.active ? wrapper.buttonRowHeight + Tokens.spacing.medium : Tokens.spacing.medium
|
||||
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
id: fitBox
|
||||
|
||||
readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1
|
||||
@@ -271,7 +283,7 @@ Item {
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
source: Wallpapers.current
|
||||
source: Wallpapers.currentThumbnail
|
||||
sourceSize.height: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return imageSlot.height * dpr;
|
||||
@@ -282,12 +294,37 @@ Item {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
opacity: wrapper.isVideo && scaledImg.status === Image.Ready ? 1 : 0
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
|
||||
implicitHeight: implicitWidth
|
||||
radius: Tokens.rounding.full
|
||||
color: Colors.tPalette.m3surface
|
||||
|
||||
MaterialIcon {
|
||||
id: videoIcon
|
||||
anchors.centerIn: parent
|
||||
text: "video_library"
|
||||
fontStyle: Tokens.font.icon.extraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: cropRectLoader
|
||||
|
||||
active: scaledImg.status === Image.Ready
|
||||
active: scaledImg.status === Image.Ready && !wrapper.isVideo
|
||||
|
||||
sourceComponent: Component {
|
||||
CustomRect {
|
||||
@@ -330,7 +367,6 @@ Item {
|
||||
y = t.y;
|
||||
}
|
||||
|
||||
// Where the rect should be according to saved data (same rules as a restore).
|
||||
function savedTarget() {
|
||||
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
||||
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
|
||||
@@ -357,8 +393,7 @@ Item {
|
||||
width: Math.round(baseWidth / zoom)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -434,9 +469,10 @@ Item {
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
enabled: !wrapper.isVideo
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color blobColor: Colors.tPalette.m3surfaceContainerLow
|
||||
@@ -21,7 +21,6 @@ CustomClippingRect {
|
||||
|
||||
implicitHeight: sState.screen.height * 0.7
|
||||
implicitWidth: implicitHeight * ratio
|
||||
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
||||
|
||||
Behavior on blobColor {
|
||||
CAnim {}
|
||||
@@ -39,6 +38,7 @@ CustomClippingRect {
|
||||
}
|
||||
|
||||
BlobInvertedRect {
|
||||
id: blobRect
|
||||
anchors.fill: parent
|
||||
borderBottom: Tokens.padding.small
|
||||
borderLeft: navPane.width + navPane.anchors.margins * 2
|
||||
@@ -46,6 +46,8 @@ CustomClippingRect {
|
||||
borderTop: Tokens.padding.small
|
||||
group: blobGroup
|
||||
opacity: root.blobColor.a
|
||||
// outerRadius: 10
|
||||
outerRadius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
}
|
||||
|
||||
@@ -64,6 +66,7 @@ CustomClippingRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: navPane.right
|
||||
anchors.leftMargin: navPane.anchors.margins + anchors.margins
|
||||
anchors.bottomMargin: blobRect.borderBottom
|
||||
anchors.margins: Tokens.padding.extraLarge
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
@@ -38,7 +38,6 @@ Item {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: opacity < 1
|
||||
objectName: "PageContainer"
|
||||
|
||||
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
|
||||
|
||||
@@ -88,6 +88,7 @@ PageBase {
|
||||
id: selectScheme
|
||||
|
||||
acceptLabel: qsTr("Confirm")
|
||||
settingAnchor: "panels-sidebar-llm-scheme"
|
||||
enabled: Object.keys(model).length > 0
|
||||
header: qsTr("Select scheme")
|
||||
first: true
|
||||
|
||||
@@ -50,11 +50,12 @@ PageBase {
|
||||
}
|
||||
|
||||
WallItem {
|
||||
required property FileSystemEntry modelData
|
||||
id: wallItem
|
||||
required property WallpaperEntry modelData
|
||||
|
||||
enabled: modelData
|
||||
opacity: modelData ? 1 : 0
|
||||
source: String(modelData?.path ?? "")
|
||||
source: String(modelData?.thumbnailPath ?? "")
|
||||
|
||||
onClicked: {
|
||||
Wallpapers.setWallpaper(modelData.path);
|
||||
|
||||
@@ -13,6 +13,42 @@ import qs.Services
|
||||
PageBase {
|
||||
id: root
|
||||
|
||||
readonly property list<MenuItem> wallTransitions: [
|
||||
MenuItem {
|
||||
text: "Bowtie"
|
||||
},
|
||||
MenuItem {
|
||||
text: "Fade"
|
||||
},
|
||||
MenuItem {
|
||||
text: "LinearBlur"
|
||||
},
|
||||
MenuItem {
|
||||
text: "WindowSlices"
|
||||
},
|
||||
MenuItem {
|
||||
text: "WipeRight"
|
||||
},
|
||||
MenuItem {
|
||||
text: "WipeDown"
|
||||
},
|
||||
MenuItem {
|
||||
text: "TilesWave"
|
||||
},
|
||||
MenuItem {
|
||||
text: "SplitSlideInHorizontal"
|
||||
},
|
||||
MenuItem {
|
||||
text: "Hexagonalize"
|
||||
},
|
||||
MenuItem {
|
||||
text: "DoomMelt"
|
||||
},
|
||||
MenuItem {
|
||||
text: "Random"
|
||||
}
|
||||
]
|
||||
|
||||
title: qsTr("Wallpaper & style")
|
||||
|
||||
ColumnLayout {
|
||||
@@ -73,6 +109,18 @@ PageBase {
|
||||
onToggled: Config.background.enabled = checked
|
||||
}
|
||||
|
||||
SelectRow {
|
||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||
active: root.wallTransitions[Config.background.wallTransition]
|
||||
menuItems: root.wallTransitions
|
||||
menuOnTop: true
|
||||
settingAnchor: "style-wallpaper-transition"
|
||||
subtext: qsTr("Currently selected: %1").arg(active.text)
|
||||
text: qsTr("Wallpaper transition")
|
||||
|
||||
onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item)
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
|
||||
checked: Colors.transparency.enabled
|
||||
@@ -199,7 +247,7 @@ PageBase {
|
||||
stepSize: 1
|
||||
subtext: qsTr("How long it takes for the gradual nightlight to finish (s)")
|
||||
text: qsTr("Fade duration")
|
||||
to: 60 * 5
|
||||
to: 60 * 20
|
||||
value: Config.display.nightlight.fadeDuration / 1000
|
||||
|
||||
onMoved: v => Config.display.nightlight.fadeDuration = v * 1000
|
||||
|
||||
@@ -2,8 +2,9 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -27,21 +28,17 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
sourceComponent: Content {
|
||||
sState.animatingContainer: content.opacity < 1
|
||||
sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode)
|
||||
sState.screen: root.screen
|
||||
}
|
||||
sourceComponent: Content {
|
||||
sState.animatingContainer: content.opacity < 1
|
||||
sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode)
|
||||
sState.screen: root.screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-10
@@ -3,10 +3,11 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Services
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Modules.Sidebar.Chat.Content
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
@@ -90,7 +91,9 @@ Item {
|
||||
signal requestClose
|
||||
|
||||
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
|
||||
readonly property bool importing: importIds.length > 0
|
||||
|
||||
property var importIds: []
|
||||
property var pendingAttachments: []
|
||||
|
||||
function attachFiles(): void {
|
||||
@@ -171,7 +174,7 @@ Item {
|
||||
Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.title.medium
|
||||
text: qsTr(root.chatData.title)
|
||||
text: qsTr(root.chatData?.title || ChatState.title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +439,7 @@ Item {
|
||||
width: attachments.cellWidth
|
||||
height: width
|
||||
opacity: modelData ? 1 : 0
|
||||
CustomClippingRect {
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.spacing.extraSmall
|
||||
|
||||
@@ -540,17 +543,17 @@ Item {
|
||||
anchors.margins: Tokens.padding.medium
|
||||
bg.border.color: Colors.palette.m3outlineVariant
|
||||
bg.color: Colors.tPalette.m3surfaceContainerLowest
|
||||
fastGlow: Chat.busy
|
||||
glow.opacity: input.activeFocus && (!Chat.busy || ChatState.isWindow)
|
||||
fastGlow: LlmClient.busy
|
||||
glow.opacity: !ChatState.narrowSidebarExpanded ? (input.activeFocus && (!LlmClient.busy || ChatState.isWindow) ? 1 : 0) : 0
|
||||
font: Tokens.font.body.small
|
||||
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
|
||||
focus: true
|
||||
placeholderText: qsTr("Send a message")
|
||||
sendIcon.font: Tokens.font.icon.medium
|
||||
sendIcon.icon: Chat.busy ? "stop" : "arrow_upward"
|
||||
sendIcon.icon: LlmClient.busy ? "stop" : "arrow_upward"
|
||||
sendIcon.padding: Tokens.padding.extraSmall
|
||||
sendIcon.opacity: text || Chat.busy ? 1.0 : 0.5
|
||||
sendIcon.enabled: text || Chat.busy
|
||||
sendIcon.opacity: text || LlmClient.busy ? 1.0 : 0.5
|
||||
sendIcon.enabled: text || LlmClient.busy
|
||||
|
||||
Component.onCompleted: {
|
||||
root.focusInput();
|
||||
@@ -572,10 +575,58 @@ Item {
|
||||
}
|
||||
}
|
||||
onButtonPressed: {
|
||||
if (Chat.busy)
|
||||
Chat.stop();
|
||||
if (LlmClient.busy)
|
||||
LlmClient.stop();
|
||||
else
|
||||
root.send(text);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DropImporter
|
||||
|
||||
function onImportFinished(id: int, paths: var, errors: var): void {
|
||||
if (!root.importIds.includes(id))
|
||||
return;
|
||||
root.importIds = root.importIds.filter(i => i !== id);
|
||||
|
||||
if (paths.length > 0) {
|
||||
const added = root.chatData.addAttachments(paths);
|
||||
root.pendingAttachments = root.pendingAttachments.concat(added);
|
||||
}
|
||||
for (const e of errors)
|
||||
console.warn("Drop import:", e);
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onEntered: drag => {
|
||||
drag.accepted = drag.hasUrls || drag.hasText || drag.hasHtml || drag.formats.some(f => f.startsWith("image/"));
|
||||
}
|
||||
|
||||
onDropped: drop => {
|
||||
const formats = drop.formats;
|
||||
const urls = drop.hasUrls ? drop.urls : [];
|
||||
const text = drop.hasText ? drop.text : "";
|
||||
const html = drop.hasHtml ? drop.html : "";
|
||||
|
||||
const imgFormat = formats.find(f => f.startsWith("image/"));
|
||||
const raw = imgFormat ? drop.getDataAsArrayBuffer(imgFormat) : new ArrayBuffer(0);
|
||||
|
||||
switch (DropImporter.decide(urls, text, html, raw.byteLength > 0)) {
|
||||
case DropImporter.Attach:
|
||||
root.importIds = [...root.importIds, DropImporter.importAttachments(urls, html, raw)];
|
||||
break;
|
||||
case DropImporter.InsertText:
|
||||
input.insert(input.cursorPosition, text || urls.map(u => u.toString()).join("\n"));
|
||||
root.focusInput();
|
||||
break;
|
||||
}
|
||||
drop.acceptProposedAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
-32
@@ -6,7 +6,7 @@ import qs.Effects
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
property bool expanded: false
|
||||
@@ -91,6 +91,50 @@ CustomClippingRect {
|
||||
|
||||
opacity: root.slim ? 0 : 1
|
||||
|
||||
state: titleText.readOnly ? "static" : "editing"
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "static"
|
||||
PropertyChanges {
|
||||
staticText.opacity: 1
|
||||
}
|
||||
PropertyChanges {
|
||||
titleText.opacity: 0
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "editing"
|
||||
PropertyChanges {
|
||||
staticText.opacity: 0
|
||||
}
|
||||
PropertyChanges {
|
||||
titleText.opacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
// Going static: titleText fades out (animated), staticText snaps in
|
||||
Transition {
|
||||
to: "static"
|
||||
Anim {
|
||||
target: titleText
|
||||
property: "opacity"
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
},
|
||||
// Going editing: staticText fades out (animated), titleText snaps in
|
||||
Transition {
|
||||
to: "editing"
|
||||
Anim {
|
||||
target: staticText
|
||||
property: "opacity"
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
@@ -106,7 +150,7 @@ CustomClippingRect {
|
||||
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
|
||||
implicitWidth: titleText.width + Tokens.padding.large * 2
|
||||
opacity: titleText.readOnly ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -122,11 +166,6 @@ CustomClippingRect {
|
||||
anchors.right: parent.right
|
||||
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: fadeMask
|
||||
}
|
||||
|
||||
TextFieldBase {
|
||||
id: titleText
|
||||
|
||||
@@ -134,10 +173,12 @@ CustomClippingRect {
|
||||
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
|
||||
width: Math.min(implicitWidth, parent.width - Tokens.padding.large * 2)
|
||||
font: Tokens.font.body.medium
|
||||
text: root.modelData.title || qsTr("New chat")
|
||||
readOnly: true
|
||||
color: Colors.palette.m3onSurface
|
||||
opacity: 0
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
@@ -146,6 +187,11 @@ CustomClippingRect {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
function cancelTitleEdit(): void {
|
||||
text = root.modelData.title;
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
onEditingFinished: root.modelData.title = text
|
||||
onReadOnlyChanged: {
|
||||
if (!readOnly) {
|
||||
@@ -156,31 +202,24 @@ CustomClippingRect {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: fadeMask
|
||||
CustomText {
|
||||
id: staticText
|
||||
|
||||
// anchors.top: titleText.top
|
||||
// anchors.bottom: titleText.bottom
|
||||
// anchors.left: titleText.left
|
||||
// anchors.right: parent.right
|
||||
anchors.fill: titleWrapper
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: titleText.readOnly ? 0 : Tokens.padding.large
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: titleText.readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
|
||||
anchors.right: parent.right
|
||||
font: titleText.font
|
||||
text: titleText.text
|
||||
elide: CustomText.ElideRight
|
||||
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0)
|
||||
position: 0.85
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 0)
|
||||
position: 1.0
|
||||
Behavior on anchors.topMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +298,7 @@ CustomClippingRect {
|
||||
font: Tokens.font.body.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")
|
||||
text: root.modelData.messagesModel.lastMessage?.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
@@ -284,7 +323,7 @@ CustomClippingRect {
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
maximumLineCount: 5
|
||||
opacity: root.expanded ? 1 : 0
|
||||
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? qsTr("No messages yet")
|
||||
text: root.modelData.messagesModel.lastMessage?.content ?? qsTr("No messages yet")
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
Behavior on y {
|
||||
@@ -327,7 +366,13 @@ CustomClippingRect {
|
||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
|
||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||
|
||||
onClicked: root.remove(root.modelData)
|
||||
onClicked: {
|
||||
if (!titleText.readOnly) {
|
||||
titleText.cancelTitleEdit();
|
||||
return;
|
||||
}
|
||||
root.remove(root.modelData);
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
@@ -370,7 +415,10 @@ CustomClippingRect {
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
onClicked: root.expanded = !root.expanded
|
||||
onClicked: {
|
||||
root.expanded = !root.expanded;
|
||||
titleText.readOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
+39
-155
@@ -5,6 +5,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Modules.Sidebar.Chat.Content
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
|
||||
@@ -21,8 +22,13 @@ Item {
|
||||
signal loadChatRequest(content: ChatSession, index: int)
|
||||
signal newChatRequest
|
||||
|
||||
onSlimChanged: {
|
||||
if (slim)
|
||||
modelsContainer.open = false;
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: header
|
||||
id: chatHeader
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.extraSmall
|
||||
@@ -41,7 +47,7 @@ Item {
|
||||
anchors.bottom: modelsContainer.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: header.bottom
|
||||
anchors.top: chatHeader.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
clip: true
|
||||
spacing: Tokens.spacing.medium
|
||||
@@ -98,156 +104,24 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
ModelsContainer {
|
||||
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
|
||||
rootParent: root
|
||||
fabRoot: fabRoot
|
||||
|
||||
function prettyModelName(): string {
|
||||
if (modelsList.count < 1)
|
||||
return qsTr("No models found");
|
||||
opacity: root.slim ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
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
|
||||
fontStyle: Tokens.font.icon.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
|
||||
fontStyle: Tokens.font.icon.normal
|
||||
opacity: model.selected ? 1 : 0
|
||||
text: "check"
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
onClicked: {
|
||||
Chat.selectModel(model.modelData);
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,19 +136,11 @@ Item {
|
||||
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
|
||||
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded || !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
|
||||
|
||||
Behavior on angle {
|
||||
Anim {}
|
||||
@@ -282,12 +148,12 @@ Item {
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.slim && !ChatState.narrowSidebarExpanded) {
|
||||
if ((root.slim && !ChatState.narrowSidebarExpanded) || ChatState.shrinkSidebarOverride) {
|
||||
root.requestExpand();
|
||||
ChatState.shrinkSidebarOverride = false;
|
||||
return;
|
||||
}
|
||||
|
||||
modelsContainer.expanded = false;
|
||||
ChatState.fabExpanded = !ChatState.fabExpanded;
|
||||
}
|
||||
|
||||
@@ -318,9 +184,13 @@ Item {
|
||||
name: "pop out in window"
|
||||
icon: "open_in_new"
|
||||
}
|
||||
ListElement {
|
||||
name: "shrink sidebar"
|
||||
icon: "left_panel_close"
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: fabMenuItem
|
||||
|
||||
required property var modelData
|
||||
@@ -331,7 +201,19 @@ Item {
|
||||
|
||||
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
|
||||
color: Colors.palette.m3primaryContainer
|
||||
visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
|
||||
visible: {
|
||||
const n = modelData.name;
|
||||
const w = ChatState.isWindow;
|
||||
const wide = ChatState.isWide;
|
||||
|
||||
if (n === "pop out in window" && w)
|
||||
return false;
|
||||
if (n === "shrink sidebar" && (!w || !wide))
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
clip: true
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
@@ -431,6 +313,8 @@ Item {
|
||||
root.newChatRequest();
|
||||
else if (name === "pop out in window")
|
||||
Detach.create();
|
||||
else if (name === "shrink sidebar")
|
||||
ChatState.shrinkSidebarOverride = true;
|
||||
|
||||
ChatState.fabExpanded = false;
|
||||
}
|
||||
+27
-17
@@ -6,7 +6,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Helpers
|
||||
import qs.Modules.Notifications.Sidebar
|
||||
import qs.Modules.Sidebar
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
@@ -17,15 +17,26 @@ Item {
|
||||
anchors.margins: Tokens.padding.small
|
||||
|
||||
Component.onCompleted: {
|
||||
if (ChatState.firstOpen) {
|
||||
ChatState.openDraft();
|
||||
stack.push(chatList);
|
||||
stack.push(chatContent);
|
||||
ChatState.firstOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ChatState.inChat) {
|
||||
stack.push(chatList);
|
||||
stack.push(chatContent, {
|
||||
"chatData": ChatState.chatSession
|
||||
});
|
||||
stack.push(chatContent);
|
||||
} else
|
||||
stack.push(chatList);
|
||||
}
|
||||
|
||||
function openChat(): void {
|
||||
if (stack.depth < 2)
|
||||
stack.push(chatContent);
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stack
|
||||
anchors.fill: parent
|
||||
@@ -35,27 +46,23 @@ Item {
|
||||
id: chatList
|
||||
|
||||
ChatList {
|
||||
model: ScriptModel {
|
||||
values: Chat.chats.values
|
||||
}
|
||||
model: LlmClient.chats
|
||||
|
||||
onDeleteChatRequest: chat => {
|
||||
Chat.chats.remove(chat);
|
||||
const wasCurrent = chat === ChatState.chatSession;
|
||||
LlmClient.chats.remove(chat);
|
||||
if (wasCurrent)
|
||||
ChatState.openDraft();
|
||||
}
|
||||
onLoadChatRequest: (chat, index) => {
|
||||
stack.push(chatContent, {
|
||||
"chatData": chat
|
||||
});
|
||||
ChatState.inChat = true;
|
||||
ChatState.chatSession = chat;
|
||||
openChat();
|
||||
ChatState.inChat = true;
|
||||
}
|
||||
onNewChatRequest: {
|
||||
const data = Chat.chats.insert();
|
||||
stack.push(chatContent, {
|
||||
"chatData": data
|
||||
});
|
||||
ChatState.openDraft();
|
||||
openChat();
|
||||
ChatState.inChat = true;
|
||||
ChatState.chatSession = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,9 +71,12 @@ Item {
|
||||
id: chatContent
|
||||
|
||||
ChatContent {
|
||||
chatData: ChatState.chatSession
|
||||
|
||||
onRequestClose: {
|
||||
stack.pop();
|
||||
ChatState.inChat = false;
|
||||
ChatState.openDraft();
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -8,9 +8,10 @@ import qs.Services
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property ChatSession chatSession
|
||||
readonly property string title: chatSession ? chatSession.title : "Chat"
|
||||
property ChatSession chatSession: LlmClient.chats.draft()
|
||||
readonly property string title: chatSession?.title || "Chat"
|
||||
property int currentIdx: -1
|
||||
property bool firstOpen: true
|
||||
property bool inChat: false
|
||||
property bool fabExpanded: false
|
||||
property bool isWindow: false
|
||||
@@ -18,10 +19,18 @@ Singleton {
|
||||
property bool settledSlim: false
|
||||
property bool settledWide: false
|
||||
property bool narrowSidebarExpanded: false
|
||||
property bool windowIsWide: false
|
||||
property bool shrinkSidebarOverride: false
|
||||
property ShellScreen screen
|
||||
property bool sidebarVisible: ShellState.forActive().sidebar
|
||||
|
||||
function openDraft(): void {
|
||||
root.chatSession = LlmClient.chats.draft();
|
||||
root.currentIdx = -1;
|
||||
}
|
||||
|
||||
onIsWideChanged: if (isWide)
|
||||
narrowSidebarExpanded = false
|
||||
|
||||
onSidebarVisibleChanged: if (!sidebarVisible)
|
||||
fabExpanded = false
|
||||
}
|
||||
+3
-4
@@ -9,12 +9,11 @@ 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
|
||||
required property bool running
|
||||
|
||||
implicitHeight: editButton.implicitHeight
|
||||
implicitWidth: generationTools.implicitWidth + editTools.implicitWidth + Tokens.spacing.small
|
||||
@@ -63,7 +62,7 @@ Item {
|
||||
id: editTools
|
||||
|
||||
anchors.right: parent.right
|
||||
opacity: !root.current.streaming && root.hovered ? 1 : 0
|
||||
opacity: !root.running && root.hovered ? 1 : 0
|
||||
spacing: Tokens.spacing.small
|
||||
visible: opacity > 0
|
||||
|
||||
@@ -99,7 +98,7 @@ Item {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: Quickshell.clipboardText = root.current.content
|
||||
onClicked: Quickshell.clipboardText = root.message.content
|
||||
}
|
||||
|
||||
IconButton {
|
||||
@@ -0,0 +1,408 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Llm
|
||||
import ZShell.Blobs
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item rootParent
|
||||
required property ChatGeneration current
|
||||
|
||||
implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2
|
||||
|
||||
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: Tokens.font.headline.medium
|
||||
color: Colors.palette.m3onSurface
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolsBg
|
||||
|
||||
property bool open: false
|
||||
property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2
|
||||
property int openHeight: Math.min(toolsContent.item?.implicitHeight, maxHeight)
|
||||
property int openWidth: root.rootParent.width - Tokens.padding.medium * 2
|
||||
property real openHeightProg: openHeight / maxHeight
|
||||
property int collapsedY: 0
|
||||
|
||||
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
|
||||
z: open || dialogTransition.running ? 2 : 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
function reparentWrapper(): void {
|
||||
const newParent = open ? root.rootParent : toolsBg;
|
||||
const c = toolsWrapper.parent.mapToItem(newParent, toolsWrapper.x + toolsWrapper.width / 2, toolsWrapper.y + toolsWrapper.height / 2);
|
||||
toolsBg.collapsedY = toolsWrapper.y;
|
||||
toolsWrapper.parent = newParent;
|
||||
toolsWrapper.x = c.x - toolsWrapper.width / 2;
|
||||
toolsWrapper.y = c.y - toolsWrapper.height / 2;
|
||||
}
|
||||
|
||||
onOpenChanged: {
|
||||
if (open)
|
||||
root.rootParent.interactive = false;
|
||||
else
|
||||
root.rootParent.interactive = true;
|
||||
}
|
||||
|
||||
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 ? root.rootParent : toolsBg
|
||||
|
||||
onClicked: toolsBg.open = false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolsWrapper
|
||||
|
||||
width: toolsBg.width
|
||||
height: toolBox.implicitHeight
|
||||
rotation: parent === root.rootParent ? 180 : 0
|
||||
z: 1
|
||||
|
||||
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: (root.rootParent.width - toolsBg.openWidth) / 2
|
||||
toolsWrapper.y: (confirmationRow.implicitHeight + Tokens.spacing.small + Tokens.padding.medium) * (1 - toolsBg.openHeightProg) + Tokens.padding.medium
|
||||
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: {
|
||||
toolsBg.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: toolList
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: root.current.pendingToolCalls?.slice(0, 4) ?? []
|
||||
|
||||
delegate: RowLayout {
|
||||
id: toolText
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
function toolTarget(seg: var): 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);
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: toolText.modelData.name === "webfetch" ? "web" : "text_snippet"
|
||||
fill: 1
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
text: qsTr("And %1 more").arg(root.current.pendingToolCalls.slice(4).length)
|
||||
visible: repeater.count < root.current.pendingToolCalls.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: toolsContent
|
||||
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
opacity: 0
|
||||
|
||||
sourceComponent: MouseArea {
|
||||
onWheel: event => event.accepted = true
|
||||
preventStealing: true
|
||||
implicitHeight: innerLayout.contentHeight + innerLayout.anchors.margins * 2
|
||||
|
||||
VerticalFadeListView {
|
||||
id: innerLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
cacheBuffer: height * 10
|
||||
fadeAmount: 0.1
|
||||
fadeEnabled: interactive
|
||||
spacing: 0
|
||||
clip: true
|
||||
interactive: contentHeight > toolsBg.maxHeight
|
||||
|
||||
model: root.current.pendingToolCalls ?? []
|
||||
|
||||
delegate: Item {
|
||||
id: toolCall
|
||||
|
||||
required property var modelData
|
||||
property bool expanded: false
|
||||
|
||||
implicitWidth: CustomListView.view.width
|
||||
implicitHeight: collapsedRow.implicitHeight + collapsedRow.anchors.margins + (expanded ? expandedTool.implicitHeight + expandedTool.anchors.margins + expandedTool.anchors.topMargin : collapsedRow.anchors.margins)
|
||||
|
||||
function toolTarget(seg: var): 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);
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: collapsedRow
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
IconButton {
|
||||
id: toolExpandBtn
|
||||
|
||||
font: Tokens.font.icon.medium
|
||||
icon: "keyboard_arrow_down"
|
||||
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
rotation: toolCall.expanded ? 180 : 0
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: toolCall.expanded = !toolCall.expanded
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: preview
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: toolCall.modelData.name
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: expandedTool
|
||||
|
||||
anchors.top: collapsedRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
|
||||
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
|
||||
clip: true
|
||||
|
||||
CustomRect {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: fullToolCall
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
text: toolCall.toolTarget(toolCall.modelData)
|
||||
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: confirmationRow
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -3,7 +3,7 @@ import ZShell.Llm
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Modules.Notifications.Sidebar.Chat
|
||||
import qs.Modules.Sidebar.Chat
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -59,7 +59,6 @@ Item {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: opacity < 1
|
||||
objectName: "ConversationContainer"
|
||||
|
||||
Component.onCompleted: {
|
||||
+8
-8
@@ -220,9 +220,8 @@ CustomRect {
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: root.codeHeaderColor
|
||||
font: Tokens.font.label.small
|
||||
font: Tokens.font.label.builders.small.capitalization(Font.Capitalize).build()
|
||||
text: CodeIcons.name(root.language)
|
||||
visible: root.language.length > 0
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -252,7 +251,7 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: codeRect
|
||||
|
||||
anchors.left: parent.left
|
||||
@@ -265,6 +264,7 @@ CustomRect {
|
||||
implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2
|
||||
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
|
||||
color: CodeColors.active.bg
|
||||
clip: true
|
||||
|
||||
Flickable {
|
||||
id: codeFlick
|
||||
@@ -272,6 +272,9 @@ CustomRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
contentWidth: codeText.implicitWidth
|
||||
|
||||
CustomScrollBar.horizontal: CustomScrollBar {
|
||||
flickable: codeFlick
|
||||
parent: codeFlick.parent
|
||||
@@ -280,16 +283,13 @@ CustomRect {
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
||||
TextAreaBase.flickable: TextAreaBase {
|
||||
CustomText {
|
||||
id: codeText
|
||||
|
||||
color: CodeColors.active.normal
|
||||
layer.enabled: true
|
||||
clip: false
|
||||
font: Tokens.font.mono.small
|
||||
textFormat: root.colored ? Text.RichText : Text.PlainText
|
||||
textFormat: Text.RichText
|
||||
text: root.colored ? root.html : root.plainForDisplay(root.code)
|
||||
readOnly: true
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user