Huge quickshell progress dump
Was requested
13
modules/user/modules/quickshell/README.md
Normal 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.
|
|
@ -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
|
||||
|
|
126
modules/user/modules/quickshell/shell/ReloadPopup.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
5
modules/user/modules/quickshell/shell/Shortcut.qml
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Quickshell.Hyprland
|
||||
|
||||
GlobalShortcut {
|
||||
appid: "shell"
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
modules/user/modules/quickshell/shell/bar/audio/Mixer.qml
Normal 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}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
109
modules/user/modules/quickshell/shell/bar/audio/VolumeSlider.qml
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Scope {
|
||||
required property MprisPlayer player;
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import QtQuick
|
||||
import
|
527
modules/user/modules/quickshell/shell/bar/mpris/Players.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
114
modules/user/modules/quickshell/shell/bar/workspaces/Widget.qml
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 |
4
modules/user/modules/quickshell/shell/icons/icons.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
phosphor icons
|
||||
shuffle-off is an edit of shuffle.
|
||||
play is edited.
|
||||
repeat-none is an edit of repeat.
|
1
modules/user/modules/quickshell/shell/icons/monitor.svg
Normal 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 |
1
modules/user/modules/quickshell/shell/icons/pause.svg
Normal 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 |
17
modules/user/modules/quickshell/shell/icons/play.svg
Normal 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 |
|
@ -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 |
17
modules/user/modules/quickshell/shell/icons/repeat-none.svg
Normal 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 |
|
@ -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 |
1
modules/user/modules/quickshell/shell/icons/rewind.svg
Normal 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 |
17
modules/user/modules/quickshell/shell/icons/shuffle-off.svg
Normal 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 |
1
modules/user/modules/quickshell/shell/icons/shuffle.svg
Normal 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 |
|
@ -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 |
|
@ -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 |
149
modules/user/modules/quickshell/shell/lock/Controller.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
modules/user/modules/quickshell/shell/lock/LockButton.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
modules/user/modules/quickshell/shell/lock/LockContent.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
modules/user/modules/quickshell/shell/lock/LockContext.qml
Normal 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;
|
||||
}
|
||||
}
|
|
@ -99,6 +99,7 @@ Scope {
|
|||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "shell:screenshot"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
|
|