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,13 @@
## DO NOT STEAL BLINDLY
You think the author's configuration is well formed and a good place to learn from?
**Think Again.**
This config has evolved over the lifetime of quickshell itself and my own learning of QML with
very little maintenance. If you are going to steal something please pay close attention to what
it is doing and why, because it may be a wrong or inefficient way of solving the problem.
This will eventually undergo a refactor to be made presentable but it currently is not, and
should not be treated as a good place to learn from, which is why it isn't linked on any
quickshell pages.

View File

@ -3,6 +3,7 @@
in {
home.packages = with pkgs; [
qt6.qtimageformats # amog
qt6.qt5compat # shader fx
quickshell.packages.${system}.default
pamtester # lockscreen
grim imagemagick # screenshot

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