Huge quickshell progress dump

Was requested
This commit is contained in:
outfoxxed 2024-06-17 00:49:34 -07:00
parent 57d9f9a72e
commit 945793973e
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
42 changed files with 2140 additions and 142 deletions

View file

@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
Scope {
id: root
property bool failed;
property string errorString;
// Connect to the Quickshell global to listen for the reload signals.
Connections {
target: Quickshell
function onReloadCompleted() {
root.failed = false;
popupLoader.loading = true;
}
function onReloadFailed(error: string) {
// Close any existing popup before making a new one.
popupLoader.active = false;
root.failed = true;
root.errorString = error;
popupLoader.loading = true;
}
}
// Keep the popup in a loader because it isn't needed most of the timeand will take up
// memory that could be used for something else.
LazyLoader {
id: popupLoader
PanelWindow {
id: popup
anchors {
top: true
left: true
}
margins {
top: 25
left: 25
}
width: rect.width
height: rect.height
// color blending is a bit odd as detailed in the type reference.
color: "transparent"
Rectangle {
id: rect
color: failed ? "#40802020" : "#40009020"
implicitHeight: layout.implicitHeight + 50
implicitWidth: layout.implicitWidth + 30
// Fills the whole area of the rectangle, making any clicks go to it,
// which dismiss the popup.
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: popupLoader.active = false
// makes the mouse area track mouse hovering, so the hide animation
// can be paused when hovering.
hoverEnabled: true
}
ColumnLayout {
id: layout
anchors {
top: parent.top
topMargin: 20
horizontalCenter: parent.horizontalCenter
}
Text {
text: root.failed ? "Reload failed." : "Reloaded completed!"
color: "white"
}
Text {
text: root.errorString
color: "white"
// When visible is false, it also takes up no space.
visible: root.errorString != ""
}
}
// A progress bar on the bottom of the screen, showing how long until the
// popup is removed.
Rectangle {
id: bar
color: "#20ffffff"
anchors.bottom: parent.bottom
anchors.left: parent.left
height: 20
PropertyAnimation {
id: anim
target: bar
property: "width"
from: rect.width
to: 0
duration: failed ? 10000 : 800
onFinished: popupLoader.active = false
// Pause the animation when the mouse is hovering over the popup,
// so it stays onscreen while reading. This updates reactively
// when the mouse moves on and off the popup.
paused: mouseArea.containsMouse
}
}
// We could set `running: true` inside the animation, but the width of the
// rectangle might not be calculated yet, due to the layout.
// In the `Component.onCompleted` event handler, all of the component's
// properties and children have been initialized.
Component.onCompleted: anim.start()
}
}
}
}

View file

@ -7,11 +7,13 @@ Singleton {
readonly property string rtpath: "/run/user/1000/quickshell"
readonly property var colors: QtObject {
readonly property var bar: "#30c0ffff";
readonly property var barOutline: "#50ffffff";
readonly property var widget: "#40ceffff";
readonly property var widgetOutline: "#60ffffff";
readonly property var separator: "#60ffffff";
readonly property color bar: "#30c0ffff";
readonly property color barOutline: "#50ffffff";
readonly property color widget: "#25ceffff";
readonly property color widgetActive: "#80ceffff";
readonly property color widgetOutline: "#40ffffff";
readonly property color widgetOutlineSeparate: "#20ffffff";
readonly property color separator: "#60ffffff";
}
readonly property var popoutXCurve: EasingCurve {
@ -31,4 +33,9 @@ Singleton {
onTriggered: time = new Date()
}
function interpolateColors(x: real, a: color, b: color): color {
const xa = 1.0 - x;
return Qt.rgba(a.r * xa + b.r * x, a.g * xa + b.g * x, a.b * xa + b.b * x, a.a * xa + b.a * x);
}
}

View file

@ -0,0 +1,5 @@
import Quickshell.Hyprland
GlobalShortcut {
appid: "shell"
}

View file

@ -1,16 +1,46 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import "systray" as SysTray
import "audio" as Audio
import "mpris" as Mpris
import "workspaces" as Workspaces
BarContainment {
id: root
property bool isSoleBar: Quickshell.screens.length == 1;
ColumnLayout {
anchors {
left: parent.left
right: parent.right
top: parent.top
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Loader {
active: isSoleBar
Layout.preferredHeight: active ? implicitHeight : 0;
Layout.fillWidth: true
sourceComponent: Workspaces.Widget {
bar: root
wsBaseIndex: 1
}
}
Workspaces.Widget {
bar: root
Layout.fillWidth: true
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
hideWhenEmpty: isSoleBar
}
}
}
ColumnLayout {
@ -20,10 +50,19 @@ BarContainment {
bottom: parent.bottom
}
Mpris.Players {
bar: root
Layout.fillWidth: true
}
Audio.AudioControls {
bar: root
Layout.fillWidth: true
}
SysTray.SysTray {
bar: root
Layout.fillWidth: true
//width: 24
}
ClockWidget {

View file

@ -1,14 +1,13 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import ".."
import "root:."
import "root:lock" as Lock
PanelWindow {
id: root
default property list<QtObject> widgetSurfaceData;
readonly property var widgetSurface: widgetSurface;
property list<var> overlays: [];
default property alias barItems: containment.data;
anchors {
left: true
@ -17,9 +16,25 @@ PanelWindow {
}
width: 70
margins.left: Lock.Controller.locked ? -width : 0
exclusiveZone: width - margins.left
color: "transparent"
WlrLayershell.namespace: "shell:bar"
readonly property var tooltip: tooltip;
Tooltip {
id: tooltip
bar: root
}
readonly property real tooltipXOffset: root.width + 2;
function boundedY(targetY: real, height: real): real {
return Math.max(barRect.anchors.topMargin + height, Math.min(barRect.height + barRect.anchors.topMargin - height, targetY))
}
Rectangle {
id: barRect
@ -43,72 +58,4 @@ PanelWindow {
}
}
}
// note: must be above the widgetSurface due to reload order
PersistentProperties {
id: persist
reloadableId: "persist"
property bool visible: false
}
onBackingWindowVisibleChanged: {
persist.visible = Qt.binding(() => backingWindowVisible);
}
PanelWindow {
id: widgetSurface
reloadableId: "widgetSurface"
visible: persist.visible
anchors: root.anchors
screen: root.screen
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "shell:bar"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
width: {
const extents = overlays
.filter(overlay => !overlay.fullyCollapsed)
.map(overlay => overlayXOffset + overlay.expandedWidth);
return Math.max(root.width, ...extents);
}
readonly property real overlayXOffset: root.width + 10;
readonly property real tooltipXOffset: root.width + 2;
function overlayRect(targetY: real, size: rect): rect {
const y = Math.max(barRect.y, Math.min((barRect.y + barRect.height) - size.height, targetY));
return Qt.rect(overlayXOffset, y, size.width, size.height);
}
function boundedY(targetY: real, height: real): real {
return Math.max(0, Math.min(barRect.height - height, targetY))
}
Item {
id: contentArea
data: widgetSurfaceData
}
readonly property var tooltip: tooltip;
Tooltip {
id: tooltip
bar: root
}
function repositionContentArea() {
// abusing the knowledge that both bars are in the same position onscreen
const contentRect = containment.mapToItem(root.contentItem, 0, 0, containment.width, containment.height)
contentArea.x = contentRect.x
contentArea.y = contentRect.y
contentArea.width = contentRect.width
contentArea.height = contentRect.height
}
}
onWindowTransformChanged: widgetSurface.repositionContentArea()
}

View file

@ -12,9 +12,21 @@ Scope {
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip;
property TooltipItem lastActiveItem: null;
property Item tooltipItem: null;
onActiveItemChanged: {
if (activeItem != null) activeItem.visible = true;
if (lastActiveItem != null) lastActiveItem.visible = false;
if (activeItem != null) {
activeItem.targetVisible = true;
if (tooltipItem) {
activeItem.parent = tooltipItem;
}
}
if (lastActiveItem != null && lastActiveItem != activeItem) {
lastActiveItem.targetVisible = false;
}
lastActiveItem = activeItem;
}
@ -40,21 +52,22 @@ Scope {
PopupWindow {
id: popup
parentWindow: bar.widgetSurface
relativeX: bar.widgetSurface.tooltipXOffset
parentWindow: bar
relativeX: bar.tooltipXOffset
relativeY: 0
height: bar.widgetSurface.height
width: tooltipItem.width
height: bar.height
width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth)
visible: true
color: "transparent"
//color: "#20000000"
mask: Region {
item: (activeItem?.isMenu ?? false) ? tooltipItem : null
item: (activeItem?.hoverable ?? false) ? tooltipItem : null
}
HyprlandFocusGrab {
active: activeItem?.isMenu ?? false
windows: [ popup, bar.widgetSurface ]
windows: [ popup, bar ]
onActiveChanged: {
if (!active && activeItem?.isMenu) {
activeMenu.close()
@ -62,19 +75,34 @@ Scope {
}
}
BarWidgetInner {
Item {
id: tooltipItem
Component.onCompleted: {
root.tooltipItem = this;
if (root.activeItem) {
root.activeItem.parent = this;
}
}
readonly property var targetWidth: activeItem?.implicitWidth ?? 10;
readonly property var targetHeight: (activeItem?.implicitHeight ?? 0) + 10;
readonly property var targetWidth: activeItem?.implicitWidth ?? 0;
readonly property var targetHeight: activeItem?.implicitHeight ?? 0;
property var lastTargetWidthTracker: 0;
property var lastTargetWidth: 0;
onTargetWidthChanged: {
lastTargetWidth = lastTargetWidthTracker;
lastTargetWidthTracker = targetWidth;
}
readonly property real targetY: {
if (activeItem == null) return 0;
const target = bar.widgetSurface.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
return bar.widgetSurface.boundedY(target, activeItem.implicitHeight / 2);
const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
return bar.boundedY(target, activeItem.implicitHeight / 2);
}
width: targetWidth + 10
property var w: -1
width: Math.max(1, w)
property var y1: -1
property var y2: -1
@ -112,13 +140,19 @@ Scope {
}
}
Item {
clip: true
children: [ activeItem ]
anchors {
fill: parent
margins: 5
SmoothedAnimation {
id: widthAnim
target: tooltipItem
property: "w"
to: tooltipItem.targetWidth;
onToChanged: {
if (tooltipItem.w == -1) {
stop();
tooltipItem.w = to;
} else {
velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
restart();
}
}
}
}

View file

@ -1,19 +1,29 @@
import QtQuick
import Quickshell
import "root:/"
Item {
id: root
required property var tooltip;
required property Item owner;
property bool isMenu: false;
property bool hoverable: isMenu;
property bool animateSize: true;
property bool show: false;
property bool preloadBackground: root.visible;
property real targetRelativeY: owner.height / 2;
property real hangTime: isMenu ? 0 : 200;
signal close();
default property alias data: contentItem.data;
property Component backgroundComponent: BarWidgetInner {
color: ShellGlobals.colors.bar
anchors.fill: parent
}
onShowChanged: {
if (show) {
hangTimer.stop();
@ -28,4 +38,60 @@ Item {
interval: hangTime
onTriggered: tooltip.removeItem(root);
}
property bool targetVisible: false
property real targetOpacity: 0
opacity: targetOpacity / 1000
Behavior on targetOpacity {
id: opacityAnimation
SmoothedAnimation { velocity: 5000 }
}
function snapOpacity(opacity: real) {
opacityAnimation.enabled = false;
targetOpacity = opacity * 1000
opacityAnimation.enabled = true;
}
onTargetVisibleChanged: {
if (targetVisible) {
visible = true;
targetOpacity = 1000;
} else {
close()
targetOpacity = 0;
}
}
onTargetOpacityChanged: {
if (!targetVisible && targetOpacity == 0) {
visible = false;
this.parent = null;
}
}
anchors.fill: parent
visible: false
clip: true
implicitHeight: contentItem.implicitHeight + 10
implicitWidth: contentItem.implicitWidth + 10
readonly property Item item: contentItem;
Loader {
anchors.fill: parent
active: root.visible || root.preloadBackground
asynchronous: !root.visible && root.preloadBackground
sourceComponent: backgroundComponent
}
Item {
id: contentItem
anchors.fill: parent
anchors.margins: 5
implicitHeight: childrenRect.height
implicitWidth: childrenRect.width
}
}

View file

@ -0,0 +1,78 @@
import QtQuick
import Quickshell.Services.Pipewire
import ".."
ClickableIcon {
id: root
required property var bar;
required property PwNode node;
property bool mixerOpen: false;
PwObjectTracker { objects: [ node ] }
implicitHeight: width;
acceptedButtons: Qt.LeftButton | Qt.RightButton;
showPressed: mixerOpen
onClicked: event => {
event.accepted = true;
if (event.button === Qt.LeftButton) {
node.audio.muted = !node.audio.muted;
} else if (event.button === Qt.RightButton) {
mixerOpen = !mixerOpen;
}
}
onWheel: event => {
event.accepted = true;
node.audio.volume += (event.angleDelta.y / 120) * 0.05
}
property var tooltip: TooltipItem {
tooltip: bar.tooltip
owner: root
show: root.containsMouse || mouseArea.containsMouse
hoverable: true
MouseArea {
id: mouseArea
hoverEnabled: true
acceptedButtons: Qt.NoButton
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
VolumeSlider {
implicitWidth: 200
implicitHeight: root.height
//enabled: !node.audio.muted
value: node.audio.volume
onValueChanged: node.audio.volume = value
}
}
}
property var rightclickMenu: TooltipItem {
tooltip: bar.tooltip
owner: root
isMenu: true
show: mixerOpen
onClose: mixerOpen = false
/*onVisibleChanged: {
if (!visible) mixerOpen = false;
}*/
Loader {
active: rightclickMenu.visible
sourceComponent: Mixer {
width: 550
trackedNode: node
nodeImage: root.image
}
}
}
}

View file

@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import ".."
BarWidgetInner {
id: root
required property var bar;
implicitHeight: column.implicitHeight + 10;
ColumnLayout {
anchors {
fill: parent;
margins: 5;
}
id: column;
implicitHeight: childrenRect.height;
spacing: 5;
Loader {
Layout.fillWidth: true;
active: Pipewire.defaultAudioSink != null;
sourceComponent: AudioControl {
bar: root.bar;
node: Pipewire.defaultAudioSink;
image: `image://icon/${node.audio.muted ? "audio-volume-muted-symbolic" : "audio-volume-high-symbolic"}`
}
}
Loader {
Layout.fillWidth: true;
active: Pipewire.defaultAudioSource != null;
sourceComponent: AudioControl {
bar: root.bar;
node: Pipewire.defaultAudioSource;
image: `image://icon/${node.audio.muted ? "microphone-sensitivity-muted-symbolic" : "microphone-sensitivity-high-symbolic"}`
}
}
}
}

View file

@ -0,0 +1,56 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
import "../.."
ColumnLayout {
required property PwNode trackedNode;
required property string nodeImage;
PwNodeLinkTracker {
id: linkTracker
node: trackedNode
}
PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
MixerEntry {
id: nodeEntry
node: trackedNode
image: nodeImage
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
visible: linkTracker.linkGroups.length > 0
color: ShellGlobals.colors.separator
}
Repeater {
model: linkTracker.linkGroups
MixerEntry {
required property PwLinkGroup modelData;
node: trackedNode.isSink ? modelData.source : modelData.target;
state: modelData.state;
image: {
let icon = "";
let props = node.properties;
if (props["application.icon-name"] != undefined) {
icon = props["application.icon-name"];
} else if (props["application.process.binary"] != undefined) {
icon = props["application.process.binary"];
}
// special cases :(
if (icon == "firefox") icon = "firefox-devedition";
return `image://icon/${icon}`
}
}
}
}

View file

@ -0,0 +1,51 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
RowLayout {
id: root
required property PwNode node;
required property string image;
property int state: PwLinkState.Unlinked;
PwObjectTracker { objects: [ node ] }
ClickableIcon {
image: root.image
asynchronous: true
implicitHeight: 40
implicitWidth: height
}
ColumnLayout {
RowLayout {
Item {
implicitHeight: title.implicitHeight
Layout.fillWidth: true
Text {
id: title
color: "white"
anchors.fill: parent
elide: Text.ElideRight
text: {
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
const mediaName = node.properties["media.name"];
return mediaName != undefined ? `${name} - ${mediaName}` : name;
}
}
}
}
VolumeSlider {
//Layout.fillHeight: true
Layout.fillWidth: true
implicitWidth: 200
value: node.audio.volume
onValueChanged: node.audio.volume = value
}
}
}

View file

@ -0,0 +1,109 @@
import QtQuick
import QtQuick.Shapes
Item {
id: root
property real from: 0.0
property real to: 1.5
property real warning: 1.0
property real value: 0.0
implicitWidth: groove.implicitWidth
implicitHeight: 20
property real __valueOffset: ((value - from) / (to - from)) * groove.width
property real __wheelValue: -1
MouseArea {
id: mouseArea
anchors.fill: parent
Rectangle {
id: grooveWarning
anchors {
left: groove.left
leftMargin: ((warning - from) / (to - from)) * groove.width
right: groove.right
top: groove.top
bottom: groove.bottom
}
color: "#60ffa800"
topRightRadius: 5
bottomRightRadius: 5
}
Rectangle {
anchors {
top: groove.bottom
horizontalCenter: grooveWarning.left
}
color: "#60eeffff"
width: 1
height: groove.height
}
Rectangle {
id: grooveFill
anchors {
left: groove.left
top: groove.top
bottom: groove.bottom
}
radius: 5
color: "#80ceffff"
width: __valueOffset
}
Rectangle {
id: groove
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
implicitHeight: 7
color: "transparent"
border.color: "#20050505"
border.width: 1
radius: 5
}
Rectangle {
id: handle
anchors.verticalCenter: groove.verticalCenter
height: 15
width: height
radius: height * 0.5
x: __valueOffset - width * 0.5
}
onWheel: event => {
event.accepted = true;
__wheelValue = value + (event.angleDelta.y / 120) * 0.05
__wheelValue = -1
}
}
Binding {
when: mouseArea.pressed
target: root
property: "value"
value: (mouseArea.mouseX / width) * (to - from) + from
restoreMode: Binding.RestoreBinding
}
Binding {
when: __wheelValue != -1
target: root
property: "value"
value: __wheelValue
restoreMode: Binding.RestoreBinding
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M806-56 677.67-184.33q-27 18.66-58 32.16-31 13.5-64.34 21.17v-68.67q20-6.33 38.84-13.66 18.83-7.34 35.5-19l-154.34-155V-160l-200-200h-160v-240H262L51.33-810.67 98.67-858l754.66 754L806-56Zm-26.67-232-48-48q19-33 28.17-69.62 9.17-36.61 9.17-75.38 0-100.22-58.34-179.11Q652-739 555.33-762.33V-831q124 28 202 125.5t78 224.5q0 51.67-14.16 100.67-14.17 49-41.84 92.33Zm-134-134-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5t-7.5 28.5Zm-170-170-104-104 104-104v208Zm-66.66 270v-131.33l-80-80H182v106.66h122L408.67-322Zm-40-171.33Z"/></svg>

After

Width:  |  Height:  |  Size: 650 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M560-131v-68.67q94.67-27.33 154-105 59.33-77.66 59.33-176.33 0-98.67-59-176.67-59-78-154.33-104.66V-831q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm426.67 45.33v-332Q599-628 629.5-582T660-480q0 55-30.83 100.83-30.84 45.84-82.5 64.5ZM413.33-634l-104 100.67H186.67v106.66h122.66l104 101.34V-634Zm-96 154Z"/></svg>

After

Width:  |  Height:  |  Size: 474 B

View file

@ -0,0 +1,161 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import ".."
import "../.."
BarWidgetInner {
id: root
border.color: "transparent"
property real renderWidth: width
property real renderHeight: height
property real blurRadius: 20;
property real blurSamples: 41;
property bool reverse: false;
function setArt(art: string, reverse: bool, immediate: bool) {
this.reverse = reverse;
if (art.length == 0) {
stack.replace(null);
} else {
stack.replace(component, { uri: art }, immediate)
}
}
property var component: Component {
Item {
id: componentRoot
property var uri: null;
readonly property bool svReady: image.status === Image.Ready;
Image {
id: image
anchors.centerIn: parent;
source: uri;
cache: false;
asynchronous: true;
fillMode: Image.PreserveAspectCrop;
sourceSize.width: width;
sourceSize.height: height;
width: stack.width + blurRadius * 2;
height: stack.height + blurRadius * 2;
}
property Component blurComponent: Item {
id: blur
//parent: blurContainment
// blur into the neighboring elements if applicable
x: componentRoot.x - blurRadius * 4
y: componentRoot.y + image.y
width: componentRoot.width + blurRadius * 8
height: image.height
onVisibleChanged: {
if (visible) blurSource.scheduleUpdate();
}
Connections {
target: image
function onStatusChanged() {
if (image.status == Image.Ready) {
blurSource.scheduleUpdate();
}
}
}
ShaderEffectSource {
id: blurSource
sourceItem: stack
sourceRect: Qt.rect(blur.x, blur.y, blur.width, blur.height);
live: false
anchors.fill: parent
visible: false
}
Item {
x: blurRadius
width: blur.width - blurRadius * 2
height: blur.height
clip: true
GaussianBlur {
source: blurSource
x: -parent.x
width: blur.width
height: blur.height
radius: root.blurRadius
samples: root.blurSamples
visible: true
}
}
}
// Weird crash if the blur is not owned by its visual parent,
// so it has to be a component.
property Item blur: blurComponent.createObject(blurContainment);
Component.onDestruction: blur.destroy();
}
}
SlideView {
id: stack;
height: renderHeight
width: renderWidth
anchors.centerIn: parent;
visible: false;
animate: root.visible;
readonly property real fromPos: (stack.width + blurRadius * 2) * (reverse ? -1 : 1);
enterTransition: PropertyAnimation {
property: "x"
from: stack.fromPos
to: 0
duration: 400
easing.type: Easing.OutExpo;
}
exitTransition: PropertyAnimation {
property: "x"
to: -stack.fromPos
duration: 400
easing.type: Easing.OutExpo;
}
}
Item {
id: blurContainment
x: stack.x
y: stack.y
width: stack.width
height: stack.height
}
readonly property Rectangle overlay: overlayItem;
Rectangle {
id: overlayItem
visible: false
anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutlineSeparate
border.width: 0//1
radius: 0//root.radius
color: "transparent"
}
// slightly offset on the corners :/
layer.enabled: true
layer.effect: ShaderEffect {
fragmentShader: "radial_clip.frag.qsb"
// +1 seems to match Rectangle
property real radius: root.radius + 1
property size size: Qt.size(root.width, root.height)
property real borderWidth: 1//.5
property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
property color tint: overlayItem.color
}
}

View file

@ -0,0 +1,43 @@
import QtQuick
import QtQuick.Templates as T
T.Slider {
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitHandleWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitHandleHeight + topPadding + bottomPadding)
background: Rectangle {
x: control.leftPadding
y: control.topPadding + control.availableHeight / 2 - height / 2
implicitWidth: 200
implicitHeight: 7
width: control.availableWidth
height: implicitHeight
radius: 5
color: "#30ceffff"
border.width: 0
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
}
width: control.handle.x + (control.handle.width / 2) - parent.x
radius: parent.radius
color: "#80ceffff"
}
}
handle: Rectangle {
x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
y: control.topPadding + control.availableHeight / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
radius: 8
}
}

View file

@ -0,0 +1,168 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Hyprland
import "../.."
Singleton {
id: root;
property MprisPlayer trackedPlayer: null;
property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null;
signal trackChanged(reverse: bool);
property bool __reverse: false;
property var activeTrack;
Component.onCompleted: {
for (const player of Mpris.players.values) {
if (player.playbackState == MprisPlaybackState.Playing) {
if (root.trackedPlayer == null) {
root.trackedPlayer = player;
}
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
}
}
Connections {
target: activePlayer
function onTrackChanged() {
root.updateTrack();
}
}
// Change the tracked player when one changes playback state or is created in a playing state.
Connections {
target: Mpris.players;
function onObjectInsertedPost(player: MprisPlayer) {
if (player.playbackState === MprisPlaybackState.Playing) {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
}
player.playbackStateChanged.connect(() => {
if (root.trackedPlayer !== player) root.trackedPlayer = player;
});
}
function onObjectRemovedPre() {
console.log(`trackedPlayer: ${root.trackedPlayer}`)
if (root.trackedPlayer == null) {
for (const player of Mpris.players.values) {
if (player.playbackState === MprisPlaybackState.Playing) {
root.trackedPlayer = player;
break;
}
}
}
}
}
onActivePlayerChanged: this.updateTrack();
function updateTrack() {
const metadata = this.activePlayer?.metadata ?? {};
this.activeTrack = {
artUrl: metadata["mpris:artUrl"] ?? "",
title: metadata["xesam:title"] ?? "",
artist: metadata["xesam:artist"] ?? "",
};
this.trackChanged(__reverse);
this.__reverse = false;
}
property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing;
property bool canPlay: this.activePlayer?.canPlay ?? false;
function play() {
if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
}
property bool canPause: this.activePlayer?.canPause ?? false;
function pause() {
if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
}
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
function previous() {
if (this.canGoPrevious) {
this.__reverse = true;
this.activePlayer.previous();
}
}
property bool canGoNext: this.activePlayer?.canGoNext ?? false;
function next() {
if (this.canGoNext) {
this.__reverse = false;
this.activePlayer.next();
}
}
property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl;
property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl;
property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None;
function setLoopState(loopState: var) {
if (this.loopSupported) {
this.activePlayer.loopState = loopState;
}
}
property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl;
property bool hasShuffle: this.activePlayer?.shuffle ?? false;
function setShuffle(shuffle: bool) {
if (this.shuffleSupported) {
this.activePlayer.shuffle = shuffle;
}
}
function setActivePlayer(player: MprisPlayer) {
const targetPlayer = player ?? MprisPlayer.players[0];
console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
if (targetPlayer && this.activePlayer) {
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer);
} else {
// always animate forward if going to null
this.__reverse = false;
}
this.trackedPlayer = targetPlayer;
}
Shortcut {
name: "music-pauseall";
onPressed: {
for (let i = 0; i < Mpris.players.length; i++) {
const player = Mpris.players[i];
if (player.canPause) player.playbackState = MprisPlaybackState.Paused;
}
}
}
Shortcut {
name: "music-playpause";
onPressed: {
if (root.isPlaying) root.pause();
else root.play();
}
}
Shortcut {
name: "music-previous";
onPressed: root.previous();
}
Shortcut {
name: "music-next";
onPressed: root.next();
}
}

View file

@ -0,0 +1,7 @@
import Quickshell
import Quickshell.Services.Mpris
Scope {
required property MprisPlayer player;
}

View file

@ -0,0 +1,2 @@
import QtQuick
import

View file

@ -0,0 +1,527 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import ".."
import "../.."
MouseArea {
id: root
hoverEnabled: true
required property var bar;
implicitHeight: column.implicitHeight + 10
PersistentProperties {
id: persist
reloadableId: "MusicWidget";
property bool widgetOpen: false;
onReloaded: {
rightclickMenu.snapOpacity(widgetOpen ? 1.0 : 0.0)
}
}
property alias widgetOpen: persist.widgetOpen;
acceptedButtons: Qt.RightButton
onClicked: widgetOpen = !widgetOpen
onWheel: event => {
event.accepted = true;
if (MprisController.canChangeVolume) {
this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
}
}
readonly property var activePlayer: MprisController.activePlayer
Item {
id: widget
anchors.fill: parent
property real scaleMul: root.pressed || widgetOpen ? 100 : 1
Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
EasingCurve {
id: scaleCurve
curve.type: Easing.Linear
}
implicitHeight: column.implicitHeight + 10
BackgroundArt {
id: bkg
anchors.fill: parent
function updateArt(reverse: bool) {
this.setArt(MprisController.activeTrack.artUrl, reverse, false)
}
Component.onCompleted: this.updateArt(false);
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
bkg.updateArt(reverse);
}
}
}
ColumnLayout {
id: column
anchors {
fill: parent
margins: 5;
}
ClickableIcon {
Layout.fillWidth: true
image: "root:icons/rewind.svg"
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoPrevious;
onClicked: MprisController.previous();
}
ClickableIcon {
Layout.fillWidth: true
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay;
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
}
ClickableIcon {
Layout.fillWidth: true
image: "root:icons/fast-forward.svg"
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoNext;
onClicked: MprisController.next();
}
}
property var tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
//implicitHeight: root.height - 10
//implicitWidth: childrenRect.width
Item {
implicitWidth: 200
implicitHeight: 100
}
/*Loader {
active: tooltip.visible
sourceComponent: ColumnLayout {
height: root.height - 10
RowLayout {
Image {
Layout.fillHeight: true
source: mainPlayer.metadata["mpris:artUrl"] ?? ""
cache: false
fillMode: Image.PreserveAspectCrop
sourceSize.width: height
sourceSize.height: height
}
Label {
text: mainPlayer.identity
}
}
Slider {
Layout.fillWidth: true
}
}
}*/
}
property var rightclickMenu: TooltipItem {
id: rightclickMenu
tooltip: bar.tooltip
owner: root
isMenu: true
show: widgetOpen
onClose: widgetOpen = false
// some very large covers take a sec to appear in the background,
// so we'll try to preload them.
preloadBackground: root.containsMouse
backgroundComponent: BackgroundArt {
id: popupBkg
anchors.fill: parent
renderHeight: rightclickMenu.implicitHeight
renderWidth: rightclickMenu.implicitWidth
blurRadius: 100
blurSamples: 201
overlay.color: "#80000000"
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
}
}
Component.onCompleted: {
setArt(MprisController.activeTrack.artUrl, false, true);
}
}
Loader {
width: 500
height: 650
active: rightclickMenu.visible
sourceComponent: ColumnLayout {
property var player: activePlayer;
anchors.fill: parent;
property int position: 0;
property int length: 0;
FrameAnimation {
id: posTracker;
running: player.playbackState == MprisPlaybackState.Playing && widgetOpen;
onTriggered: player.positionChanged();
}
Connections {
target: player
function onPositionChanged() {
const newPosition = Math.floor(player.position);
if (newPosition != position) position = newPosition;
}
function onLengthChanged() {
const newLength = Math.floor(player.length);
if (newLength != length) length = newLength;
}
}
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
trackStack.updateTrack(reverse, false);
length = Math.floor(player.length);
}
}
Component.onCompleted: {
position = Math.floor(player.position);
length = Math.floor(player.length);
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
Item {
id: playerSelectorContainment
Layout.fillWidth: true
implicitHeight: playerSelector.implicitHeight + 20
implicitWidth: playerSelector.implicitWidth
ScrollView {
id: playerSelector
anchors.centerIn: parent
width: Math.min(implicitWidth, playerSelectorContainment.width)
RowLayout {
Repeater {
model: Mpris.players
MouseArea {
required property MprisPlayer modelData;
readonly property bool selected: modelData == player;
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
onClicked: MprisController.setActivePlayer(modelData);
Rectangle {
implicitWidth: 50
implicitHeight: 50
radius: 5
color: selected ? "#20ceffff" : "transparent"
Image {
anchors.fill: parent
anchors.margins: 5
// lazy and wont always work, but good enough until a desktop entry impl.
source: {
const entry = DesktopEntries.byId(modelData.desktopEntry);
console.log(`ent ${entry} id ${modelData.desktopEntry}`)
if (!entry) return "image://icon/";
return `image://icon/${entry.icon}`;
}
//asynchronous: true
sourceSize.width: 50
sourceSize.height: 50
cache: false
}
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.bottomMargin: 10
Label {
anchors.centerIn: parent
text: activePlayer.identity
}
}
SlideView {
id: trackStack
Layout.fillWidth: true
implicitHeight: 400
property bool reverse: false;
Component.onCompleted: updateTrack(false, true);
function updateTrack(reverse: bool, immediate: bool) {
this.reverse = reverse;
this.replace(
trackComponent,
{ track: MprisController.activeTrack },
immediate
)
}
property var trackComponent: Component {
Flickable {
id: flickable
required property var track;
// in most cases this is ready around the same time as the background,
// but may take longer if the image is huge.
readonly property bool svReady: img.status === Image.Ready;
contentWidth: width + 1
onDragEnded: {
return;
console.log(`dragend ${contentX}`)
if (Math.abs(contentX) > 75) {
if (contentX < 0) MprisController.previous();
else if (contentX > 0) MprisController.next();
}
}
ColumnLayout {
id: trackContent
width: flickable.width
height: flickable.height
Item {
Layout.fillWidth: true
implicitHeight: 300//img.implicitHeight
implicitWidth: img.implicitWidth
Image {
id: img;
anchors.centerIn: parent;
source: track.artUrl ?? "";
//height: 300
//fillMode: Image.PreserveAspectFit
cache: false
asynchronous: true
sourceSize.height: 300
sourceSize.width: 300
}
}
Item {
Layout.fillWidth: true
Layout.topMargin: 20
Label {
anchors.centerIn: parent
text: track.title
}
}
Item {
Layout.fillWidth: true
Layout.topMargin: 20
Label {
anchors.centerIn: parent
text: track.artist
}
}
Item { Layout.fillHeight: true }
}
}
}
readonly property real fromPos: trackStack.width * (trackStack.reverse ? -1 : 1);
// intentionally slightly faster than the background
enterTransition: PropertyAnimation {
property: "x"
from: trackStack.fromPos
to: 0;
duration: 350;
easing.type: Easing.OutExpo;
}
exitTransition: PropertyAnimation {
property: "x"
to: target.x - trackStack.fromPos;
duration: 350;
easing.type: Easing.OutExpo;
}
}
Item { Layout.fillHeight: true }
Item {
Layout.fillWidth: true
implicitHeight: controlsRow.implicitHeight
RowLayout {
id: controlsRow
anchors.centerIn: parent
ClickableIcon {
image: {
switch (MprisController.loopState) {
case MprisLoopState.None: return "root:icons/repeat-none.svg";
case MprisLoopState.Playlist: return "root:icons/repeat-all.svg";
case MprisLoopState.Track: return "root:icons/repeat-once.svg";
}
}
implicitWidth: 50
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.loopSupported;
onClicked: {
let target = MprisLoopState.None;
switch (MprisController.loopState) {
case MprisLoopState.None: target = MprisLoopState.Playlist; break;
case MprisLoopState.Playlist: target = MprisLoopState.Track; break;
case MprisLoopState.Track: target = MprisLoopState.None; break;
}
MprisController.setLoopState(target);
}
}
ClickableIcon {
image: "root:icons/rewind.svg"
implicitWidth: 60
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoPrevious;
onClicked: MprisController.previous();
}
ClickableIcon {
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
Layout.leftMargin: -10
Layout.rightMargin: -10
implicitWidth: 80
implicitHeight: width
scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
onClicked: {
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
}
ClickableIcon {
image: "root:icons/fast-forward.svg"
implicitWidth: 60
implicitHeight: width
scaleIcon: false
baseMargin: 3
enabled: MprisController.canGoNext;
onClicked: MprisController.next();
}
ClickableIcon {
image: `root:icons/${MprisController.hasShuffle ? "shuffle" : "shuffle-off"}.svg`
implicitWidth: 50
implicitHeight: width
scaleIcon: false
enabled: MprisController.shuffleSupported;
onClicked: MprisController.setShuffle(!MprisController.hasShuffle);
}
}
}
RowLayout {
Label {
Layout.preferredWidth: lengthLabel.implicitWidth
text: timeStr(position)
}
MediaSlider {
id: slider
Layout.fillWidth: true
property var bindSlider: true;
enabled: player.canSeek
from: 0
to: player.length
onPressedChanged: {
if (!pressed) player.position = value;
bindSlider = !pressed;
}
Binding {
when: slider.bindSlider
slider.value: player.position
}
}
Label {
id: lengthLabel
text: timeStr(length)
}
}
}
}
}
}
}

View file

@ -41,11 +41,8 @@ OverlayWidget {
Layout.fillWidth: true
implicitHeight: width
Behavior on implicitHeight {
SmoothedAnimation { velocity: 50 }
}
MouseArea {
ClickableIcon {
id: mouseArea
anchors {
top: parent.top
@ -54,31 +51,10 @@ OverlayWidget {
}
width: height
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
Image {
id: image
anchors.fill: parent
anchors.margins: mouseArea.pressed || targetMenuOpen ? 3 : 0
Behavior on anchors.margins { SmoothedAnimation { velocity: 30 } }
source: modelData.icon
sourceSize.width: width
sourceSize.height: height
cache: false
visible: false
}
MultiEffect {
anchors.fill: image
source: image
property real targetBrightness: mouseArea.pressed || targetMenuOpen ? -25 : mouseArea.containsMouse ? 75 : 0
Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } }
brightness: targetBrightness / 1000
}
image: modelData.icon
showPressed: targetMenuOpen
onClicked: event => {
event.accepted = true;
@ -99,42 +75,30 @@ OverlayWidget {
}
property var tooltip: TooltipItem {
anchors.fill: parent
tooltip: bar.widgetSurface.tooltip
owner: image
tooltip: bar.tooltip
owner: mouseArea
show: mouseArea.containsMouse
implicitWidth: tooltipText.implicitWidth
implicitHeight: tooltipText.implicitHeight
Text {
id: tooltipText
anchors.fill: parent
text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id
color: "white"
}
}
property var rightclickMenu: TooltipItem {
anchors.fill: parent
tooltip: bar.widgetSurface.tooltip
owner: image
tooltip: bar.tooltip
owner: mouseArea
isMenu: true
show: targetMenuOpen && menu.showChildren
animateSize: !rightclickItems.animating
implicitHeight: rightclickItems.implicitHeight
implicitWidth: rightclickItems.implicitWidth
onVisibleChanged: {
if (!visible) targetMenuOpen = false;
}
onClose: targetMenuOpen = false;
MenuItemList {
id: rightclickItems
anchors.fill: parent
items: menu == null ? [] : menu.children
onClose: targetMenuOpen = false;
}

View file

@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import ".."
import "root:."
MouseArea {
id: root
required property var bar;
required property int wsBaseIndex;
property int wsCount: 10;
property bool hideWhenEmpty: false;
implicitHeight: column.implicitHeight + 10;
acceptedButtons: Qt.NoButton
onWheel: event => {
event.accepted = true;
const step = -Math.sign(event.angleDelta.y);
const targetWs = currentIndex + step;
if (targetWs >= wsBaseIndex && targetWs < wsBaseIndex + wsCount) {
Hyprland.dispatch(`workspace ${targetWs}`)
}
}
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen);
property int currentIndex: 0;
property int existsCount: 0;
visible: !hideWhenEmpty || existsCount > 0;
property real animPos: 0;
Behavior on animPos { SmoothedAnimation { velocity: 100 } }
// destructor takes care of nulling
signal workspaceAdded(workspace: HyprlandWorkspace);
ColumnLayout {
id: column
spacing: 0
anchors {
fill: parent;
topMargin: 0;
margins: 5;
}
Repeater {
model: 10
MouseArea {
id: wsItem
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
Layout.fillWidth: true
implicitHeight: 15
required property int index;
property int wsIndex: wsBaseIndex + index;
property HyprlandWorkspace workspace: null;
property bool exists: workspace != null;
property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace;
onActiveChanged: {
if (active) root.currentIndex = wsIndex;
}
onExistsChanged: {
root.existsCount += exists ? 1 : -1;
}
Connections {
target: root
function onWorkspaceAdded(workspace: HyprlandWorkspace) {
if (workspace.id == wsItem.wsIndex) {
wsItem.workspace = workspace;
}
}
}
property real animActive: active ? 100 : 0
Behavior on animActive { NumberAnimation { duration: 100 } }
property real animExists: exists ? 100 : 0
Behavior on animExists { NumberAnimation { duration: 100 } }
Rectangle {
anchors.centerIn: parent
height: 10
width: parent.width
scale: 1 + animActive * 0.003
radius: height / 2
border.color: ShellGlobals.colors.widgetOutline
border.width: 1
color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
}
}
}
}
Connections {
target: Hyprland.workspaces
function onObjectInsertedPost(workspace) {
root.workspaceAdded(workspace);
}
}
Component.onCompleted: {
Hyprland.workspaces.values.forEach(workspace => {
root.workspaceAdded(workspace)
});
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"></path></svg>

After

Width:  |  Height:  |  Size: 445 B

View file

@ -0,0 +1,4 @@
phosphor icons
shuffle-off is an edit of shuffle.
play is edited.
repeat-none is an edit of repeat.

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"></path></svg>

After

Width:  |  Height:  |  Size: 353 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"></path></svg>

After

Width:  |  Height:  |  Size: 386 B

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="32"
fill="#ffffff"
viewBox="0 0 256 256"
data-darkreader-inline-fill=""
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="m 215.99998,128 a 15.74,15.74 0 0 1 -7.6,13.51 L 64.319977,229.65 a 16,16 0 0 1 -16.2,0.3 15.86,15.86 0 0 1 -8.12,-13.82 V 39.87 a 15.86,15.86 0 0 1 8.12,-13.82 16,16 0 0 1 16.2,0.3 l 144.080003,88.14 a 15.74,15.74 0 0 1 7.6,13.51 z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 580 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"></path></svg>

After

Width:  |  Height:  |  Size: 521 B

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="32"
fill="#ffffff"
viewBox="0 0 256 256"
data-darkreader-inline-fill=""
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
id="path1"
d="M 200.25,31.984375 C 196.06443,31.879199 191.99588,35.096466 192,40 V 56 H 96 c -39.746185,0.04409 -71.955911,32.253815 -72,72 0,4.41828 3.581726,8 8,8 4.418274,0 8,-3.58172 8,-8 0.03307,-30.914207 25.085793,-55.96693 56,-56 h 96 v 16 c -0.006,7.132413 8.61568,10.702436 13.65625,5.65625 l 24,-24 c 3.12839,-3.12491 3.12839,-8.18759 0,-11.3125 l -24,-24 C 204.08107,32.766817 202.15253,32.032182 200.25,31.984375 Z M 192.92188,153.6875 A 38.310444,38.310444 0 0 0 154.60938,192 38.310444,38.310444 0 0 0 192.92188,230.3125 38.310444,38.310444 0 0 0 231.21875,192 38.310444,38.310444 0 0 0 192.92188,153.6875 Z" />
</svg>

After

Width:  |  Height:  |  Size: 957 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Zm-88,40a8,8,0,0,0,8-8V104a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,1,0,7.16,14.31l4.42-2.21V152A8,8,0,0,0,136,160Z"></path></svg>

After

Width:  |  Height:  |  Size: 627 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"></path></svg>

After

Width:  |  Height:  |  Size: 415 B

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="32"
fill="#ffffff"
viewBox="0 0 256 256"
data-darkreader-inline-fill=""
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="m 237.66,178.34 c 3.12839,3.12491 3.12839,8.19509 0,11.32 l -24,24 C 208.61942,218.70619 199.99439,215.13242 200,208 V 192 H 32 c -4.418281,0 -7.999997,-3.58172 -7.999997,-8 0,-4.41828 3.581716,-8 7.999997,-8 h 168 v -16 c -0.006,-7.13242 8.61942,-10.70619 13.66,-5.66 z M 24,72 c 0,3.6 3.6,8 8,8 h 168 v 16 c -0.006,7.13242 8.61942,10.70619 13.66,5.66 l 24,-24 c 3.12839,-3.124913 3.12839,-8.195087 0,-11.32 l -24,-24 C 208.61942,37.293809 199.99439,40.86758 200,48 V 64 H 32 c -4.4,0 -8,3.6 -8,8 z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 847 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M237.66,178.34a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,200,208V192a72.15,72.15,0,0,1-57.65-30.14l-41.72-58.4A56.1,56.1,0,0,0,55.06,80H32a8,8,0,0,1,0-16H55.06a72.12,72.12,0,0,1,58.59,30.15l41.72,58.4A56.08,56.08,0,0,0,200,176V160a8,8,0,0,1,13.66-5.66ZM143,107a8,8,0,0,0,11.16-1.86l1.2-1.67A56.08,56.08,0,0,1,200,80V96a8,8,0,0,0,13.66,5.66l24-24a8,8,0,0,0,0-11.32l-24-24A8,8,0,0,0,200,48V64a72.15,72.15,0,0,0-57.65,30.14l-1.2,1.67A8,8,0,0,0,143,107Zm-30,42a8,8,0,0,0-11.16,1.86l-1.2,1.67A56.1,56.1,0,0,1,55.06,176H32a8,8,0,0,0,0,16H55.06a72.12,72.12,0,0,0,58.59-30.15l1.2-1.67A8,8,0,0,0,113,149Z"></path></svg>

After

Width:  |  Height:  |  Size: 787 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,47.88V208.12a16,16,0,0,1-24.43,13.43L64,146.77V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0v69.23L183.57,34.45A15.95,15.95,0,0,1,208,47.88Z"></path></svg>

After

Width:  |  Height:  |  Size: 335 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,40V216a8,8,0,0,1-16,0V146.77L72.43,221.55A15.95,15.95,0,0,1,48,208.12V47.88A15.95,15.95,0,0,1,72.43,34.45L192,109.23V40a8,8,0,0,1,16,0Z"></path></svg>

After

Width:  |  Height:  |  Size: 339 B

View file

@ -0,0 +1,149 @@
pragma Singleton
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import ".."
import "../.."
Singleton {
id: root
function init() {}
property bool locked: false;
onLockedChanged: {
if (locked) {
lockContextLoader.active = true;
lock.locked = true;
} else {
lockClearTimer.start();
workspaceUnlockAnimation();
}
}
Timer {
id: lockClearTimer
interval: 600
onTriggered: {
lock.locked = false;
lockContextLoader.active = false;
}
}
property var oldWorkspaces: ({});
function workspaceLockAnimation() {
const focusedMonitor = Hyprland.focusedMonitor.id;
Hyprland.monitors.values.forEach(monitor => {
if (monitor.activeWorkspace) {
root.oldWorkspaces[monitor.id] = monitor.activeWorkspace.id;
}
Hyprland.dispatch(`workspace name:lock_${monitor.name}`);
});
Hyprland.dispatch(`focusmonitor ${focusedMonitor}`);
}
function workspaceUnlockAnimation() {
const focusedMonitor = Hyprland.focusedMonitor.id;
Hyprland.monitors.values.forEach(monitor => {
const workspace = root.oldWorkspaces[monitor.id];
if (workspace) Hyprland.dispatch(`workspace ${workspace}`);
});
Hyprland.dispatch(`focusmonitor ${focusedMonitor}`);
root.oldWorkspaces = ({});
}
Shortcut {
name: "lock"
onPressed: {
if (root.locked) root.locked = false;
else root.locked = true;
}
}
LazyLoader {
id: lockContextLoader
LockContext {
onUnlocked: root.locked = false;
}
}
WlSessionLock {
id: lock
onSecureChanged: {
if (secure) {
Qt.callLater(() => root.workspaceLockAnimation());
}
}
WlSessionLockSurface {
id: lockSurface
color: "transparent"
// Ensure nothing spawns in the workspace behind the transparent lock
// by filling in the background after animations complete.
Rectangle {
anchors.fill: parent
color: "gray"
visible: backgroundImage.visible
}
BackgroundImage {
id: backgroundImage
anchors.fill: parent
screen: lockSurface.screen
visible: !lockAnim.running
asynchronous: true
}
LockContent {
id: lockContent
context: lockContextLoader.item;
visible: false
width: lockSurface.width
height: lockSurface.height
}
NumberAnimation {
id: lockAnim
target: lockContent
property: "y"
to: 0
duration: 600
easing.type: Easing.BezierSpline
easing.bezierCurve: [0.0, 0.75, 0.15, 1.0, 1.0, 1.0]
}
onVisibleChanged: {
if (visible) {
lockContent.y = -lockSurface.height
console.log(`y ${lockContent.y}`)
lockContent.visible = true;
lockAnim.running = true;
}
}
Connections {
target: root
function onLockedChanged() {
if (!locked) {
lockAnim.to = -lockSurface.height
lockAnim.running = true;
}
}
}
}
}
}

View file

@ -0,0 +1,50 @@
import QtQuick
import "root:."
Item {
id: root
implicitHeight: 75
implicitWidth: showProgress * 0.1
signal clicked();
property string icon;
property bool show: true;
property int showProgress: show ? 1000 : 0
Behavior on showProgress {
NumberAnimation {
duration: 100
easing.type: Easing.OutQuad
}
}
MouseArea {
id: mouseArea
implicitWidth: 75
implicitHeight: 75
hoverEnabled: true
y: -(height + 30) * (1.0 - showProgress * 0.001)
x: 12.5 - 50 * (1.0 - showProgress * 0.001)
Component.onCompleted: clicked.connect(root.clicked);
Rectangle {
anchors.fill: parent
radius: 5
color: ShellGlobals.interpolateColors(hoverColorInterp * 0.001, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
border.width: 1
border.color: ShellGlobals.colors.widgetOutline
property int hoverColorInterp: mouseArea.containsMouse || mouseArea.pressed ? 1000 : 0;
Behavior on hoverColorInterp { SmoothedAnimation { velocity: 10000 } }
Image {
anchors.fill: parent
anchors.margins: 15
source: root.icon
}
}
}
}

View file

@ -0,0 +1,113 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import ".."
Item {
id: root
required property LockContext context;
property real focusAnim: focusAnimInternal * 0.001
property int focusAnimInternal: Window.active ? 1000 : 0
Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } }
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height / 2 + textBox.height
id: sep
implicitHeight: 6
implicitWidth: 800
radius: height / 2
color: ShellGlobals.colors.widget
}
ColumnLayout {
implicitWidth: sep.implicitWidth
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: sep.top
spacing: 0
Text {
id: timeText
Layout.alignment: Qt.AlignHCenter
font {
pointSize: 120
hintingPreference: Font.PreferFullHinting
family: "Noto Sans"
}
color: "white"
renderType: Text.NativeRendering
text: {
const hours = ShellGlobals.time.getHours().toString().padStart(2, '0');
const minutes = ShellGlobals.time.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
}
Item {
Layout.alignment: Qt.AlignHCenter
implicitHeight: childrenRect.height * focusAnim
implicitWidth: sep.implicitWidth
clip: true
TextInput {
id: textBox
focus: true
width: parent.width
color: enabled ?
root.context.failed ? "#ffa0a0" : "white"
: "#80ffffff";
font.pointSize: 24
horizontalAlignment: TextInput.AlignHCenter
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
onTextChanged: root.context.currentText = text;
Window.onActiveChanged: {
if (Window.active) {
text = root.context.currentText;
}
}
onAccepted: {
if (text != "") root.context.tryUnlock();
}
enabled: !root.context.isUnlocking;
}
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: sep.bottom
implicitHeight: (childrenRect.height + 30) * focusAnim
implicitWidth: sep.implicitWidth
clip: true
RowLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.topMargin: 50
spacing: 0
LockButton {
icon: "root:icons/monitor.svg"
onClicked: root.context.dpms();
}
LockButton {
icon: "root:icons/pause.svg"
show: context.mediaPlaying;
onClicked: root.context.pauseMedia();
}
}
}
}

View file

@ -0,0 +1,57 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import Quickshell.Services.Mpris
Scope {
id: root
signal unlocked();
property string currentText: "";
readonly property alias isUnlocking: pamtester.running;
property bool failed: false;
onCurrentTextChanged: failed = false;
readonly property bool mediaPlaying: Mpris.players.values.some(player => {
return player.playbackState === MprisPlaybackState.Playing && player.canPause;
});
function pauseMedia() {
Mpris.players.values.forEach(player => {
if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
player.playbackState = MprisPlaybackState.Paused;
}
});
}
function dpms() {
Hyprland.dispatch(`dpms`);
}
Process {
id: pamtester
property bool failed: true
command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
onStarted: this.write(`${currentText}\n`)
stdout: SplitParser {
// fails go to stderr
onRead: pamtester.failed = false
}
onExited: {
if (failed) {
root.failed = true;
} else {
root.unlocked();
}
}
}
function tryUnlock() {
pamtester.running = true;
}
}

View file

@ -99,6 +99,7 @@ Scope {
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "shell:screenshot"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
anchors {
top: true