last 7 months of qs changes

This commit is contained in:
outfoxxed 2025-01-06 00:13:19 -08:00
parent 2c64563ade
commit 4b90113a54
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
103 changed files with 3467 additions and 1415 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.envrc
.direnv/

View file

@ -0,0 +1,55 @@
{ inputs, system, config, lib, pkgs, ... }: let
hyprlandPackage = config.home-manager.users.${config.main-user}.wayland.windowManager.hyprland.package;
hyprlandConfig = pkgs.writeText "greetd-hyprland-config" ''
# for some reason pkill is way faster than dispatching exit, to the point greetd thinks the greeter died.
exec-once = quickshell -c greeter >& qslog.txt && pkill Hyprland
input {
kb_layout = us
sensitivity = 0
follow_mouse = 1
# mouse_refocus = false - #6393
accel_profile = flat
}
decoration {
blur {
enabled = no
}
}
animations {
enabled = no
}
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
background_color = 0x000000
key_press_enables_dpms = true
mouse_move_enables_dpms = true
}
${config.hyprland-session.extraConfigStatic}
'';
in {
services.greetd = {
enable = true;
restart = false;
settings.default_session.command = "${lib.getExe hyprlandPackage} -c ${hyprlandConfig}";
};
# needed for hyprland cache dir
users.users.greeter = {
home = "/home/greeter";
createHome = true;
};
home-manager.users.greeter = {
home.stateVersion = config.system.stateVersion;
imports = [
../../../theme/home.nix # also fixes cursor
../quickshell # set up quickshell manifest and such
];
};
}

View file

@ -1,16 +1,29 @@
{ inputs, pkgs, lib, system, impurity, ... }: let { config, inputs, pkgs, lib, system, impurity, ... }: let
inherit (inputs) quickshell; inherit (inputs) quickshell;
# hack because the greeter user cant access /home/admin
maybeLink = path: if config.home.username == "admin" then impurity.link path else path;
in { in {
home.packages = with pkgs; [ home.packages = with pkgs; [
qt6.qtimageformats # amog qt6.qtimageformats # amog
qt6.qt5compat # shader fx qt6.qt5compat # shader fx
quickshell.packages.${system}.default (quickshell.packages.${system}.default.override (prevqs: {
pamtester # lockscreen debug = true;
qt6 = prevqs.qt6.overrideScope (_: prevqt: {
qtdeclarative = prevqt.qtdeclarative.overrideAttrs (prev: {
cmakeBuildType = "Debug";
dontStrip = true;
});
});
breakpad = prevqs.breakpad.override rec {
stdenv = pkgs.gcc13Stdenv;
};
}))
grim imagemagick # screenshot grim imagemagick # screenshot
]; ];
xdg.configFile."quickshell/manifest.conf".text = lib.generators.toKeyValue {} { xdg.configFile."quickshell/manifest.conf".text = lib.generators.toKeyValue {} {
shell = "${impurity.link ./shell}"; shell = "${maybeLink ./.}/shell";
lockscreen = "${impurity.link ./lockscreen}"; greeter = "${maybeLink ./.}/shell/greeter.qml";
}; };
} }

View file

@ -1,43 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
property int status: AuthContext.Status.FirstLogin
signal unlocked();
enum Status {
FirstLogin,
Authenticating,
LoginFailed
}
property string password
property var pamtester: Process {
property bool failed: true
command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
onStarted: this.write(`${password}\n`)
stdout: SplitParser {
// fails go to stderr
onRead: pamtester.failed = false
}
onExited: {
if (failed) {
status = AuthContext.Status.LoginFailed;
} else {
unlocked();
}
}
}
function tryLogin(password: string) {
this.password = password
status = AuthContext.Status.Authenticating;
pamtester.running = true;
}
}

View file

@ -1,17 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
property var time: new Date();
property string text;
Timer {
interval: 10000
running: true
repeat: true
onTriggered: time = new Date()
}
}

View file

@ -1,131 +0,0 @@
import QtQuick
import QtQuick.Controls.Basic
import Quickshell.Io
Item {
required property AuthContext context
Item {
anchors.centerIn: parent
Text {
id: timeText
anchors {
bottom: entryBox.top
bottomMargin: 100
horizontalCenter: parent.horizontalCenter
}
font {
pointSize: 120
hintingPreference: Font.PreferFullHinting
family: "Noto Sans"
}
color: "white"
text: {
const hours = LockGlobals.time.getHours().toString().padStart(2, '0');
const minutes = LockGlobals.time.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
}
Text {
anchors {
top: timeText.bottom
topMargin: -20
horizontalCenter: parent.horizontalCenter
}
font.pointSize: 40
color: "#50ffffff"
text: "Locked"
}
TextField {
id: entryBox
anchors.centerIn: parent
width: 600
font.pointSize: 24
enabled: context.status != AuthContext.Status.Authenticating
focus: true
horizontalAlignment: TextInput.AlignHCenter
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
onCursorVisibleChanged: {
if (cursorVisible) cursorVisible = false;
}
cursorVisible: false
color: "white"
background: Rectangle {
color: "#20ffffff"
border.color: "#30ffffff"
radius: height / 2
}
text: LockGlobals.text
onTextChanged: {
LockGlobals.text = text
}
onAccepted: {
if (text != "") context.tryLogin(text)
}
onEnabledChanged: {
if (enabled) text = ""
}
}
Text {
id: status
color: "white"
font.pointSize: 24
anchors {
horizontalCenter: entryBox.horizontalCenter
top: entryBox.bottom
topMargin: 40
}
text: {
switch (context.status) {
case AuthContext.Status.FirstLogin: return ""
case AuthContext.Status.Authenticating: return "Authenticating"
case AuthContext.Status.LoginFailed: return "Login Failed"
}
}
}
}
Button {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 20
}
contentItem: Text {
text: "Turn off Monitors"
color: "#aaeeffff"
}
onClicked: dpms.running = true
background: Rectangle {
color: "#20ffffff"
border.color: "#30ffffff"
radius: height / 2
}
Process {
id: dpms
command: [ "hyprctl", "dispatch", "dpms" ]
}
}
}

View file

@ -1,36 +0,0 @@
//@ pragma NativeTextRendering
import QtQuick
import Quickshell
import Quickshell.Wayland
import ".."
ShellRoot {
AuthContext {
id: authContext
onUnlocked: lock.locked = false
}
WlSessionLock {
id: lock
locked: true
onLockedChanged: {
if (!locked) Qt.quit();
}
WlSessionLockSurface {
id: surface
BackgroundImage {
anchors.fill: parent
screen: surface.screen
}
Lockscreen {
anchors.fill: parent
context: authContext
}
}
}
}

View file

@ -1,39 +0,0 @@
import QtQuick
import Quickshell
import ".."
ShellRoot {
AuthContext {
id: authContext
onUnlocked: Qt.quit()
}
FloatingWindow {
BackgroundImage {
anchors.fill: parent
screen: Quickshell.screens.filter(s => s.name == "eDP-1")[0]
}
Lockscreen {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
right: parent.horizontalCenter
}
context: authContext
}
Lockscreen {
anchors {
left: parent.horizontalCenter
top: parent.top
bottom: parent.bottom
right: parent.right
}
context: authContext
}
}
}

View file

@ -1,26 +0,0 @@
pragma Singleton
import Quickshell
import Quickshell.Io
Singleton {
signal windowOpened(address: string, workspace: string, klass: string, title: string);
Socket {
connected: true
path: `/tmp/hypr/${Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")}/.socket2.sock`
parser: SplitParser {
onRead: message => {
const [type, body] = message.split(">>");
const args = body.split(",");
switch (type) {
case "openwindow":
windowOpened(args[0], args[1], args[2], args[3])
break;
}
}
}
}
}

View file

@ -1,81 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
WlrLayershell {
id: root
required property var bar;
property var popup: null;
property list<variant> overlays: [];
property variant activeOverlay: null;
property variant lastActiveOverlay: null;
onActiveOverlayChanged: {
if (lastActiveOverlay != null && lastActiveOverlay != activeOverlay) {
lastActiveOverlay.expanded = false;
}
lastActiveOverlay = activeOverlay;
}
readonly property rect barRect: {
void [width, height];
this.contentItem.mapFromItem(bar, 0, 0, bar.width, bar.height)
}
readonly property real overlayXOffset: barRect.x + barRect.width + 10
exclusionMode: ExclusionMode.Ignore
color: "transparent"
namespace: "shell:bar"
Variants {
id: masks
model: overlays
Region {
required property var modelData;
item: modelData.widget
}
}
mask: Region {
regions: masks.instances
}
anchors {
left: true
top: true
bottom: true
}
width: {
const extents = overlays
.filter(overlay => !overlay.fullyCollapsed)
.map(overlay => overlayXOffset + overlay.expandedWidth);
return Math.max(barRect.x + barRect.width, ...extents);
}
function connectOverlay(overlay: variant) {
overlay.widget.parent = this.contentItem
overlays.push(overlay);
}
function disconnectOverlay(overlay: variant) {
// Splice seems to make it undefined as an intermediary step
// which breaks bindings.
overlays = overlays.filter(o => o != overlay);
}
function expandedPosition(overlay: variant): rect {
const rect = overlay.collapsedLayerRect;
const idealY = rect.y + (rect.height / 2) - (overlay.expandedHeight / 2)
const y = Math.max(barRect.y, Math.min((barRect.y + barRect.height) - overlay.expandedHeight, idealY));
return Qt.rect(overlayXOffset, y, overlay.expandedWidth, overlay.expandedHeight);
}
}

View file

@ -1,27 +0,0 @@
import QtQuick
Item {
id: root
required property var screen;
required property var selectionArea;
signal selectionComplete(x: real, y: real, width: real, height: real)
MouseArea {
anchors.fill: parent
onPressed: {
selectionArea.startX = mouseX;
selectionArea.startY = mouseY;
selectionArea.endX = mouseX;
selectionArea.endY = mouseY;
selectionArea.startSelection(false);
}
onPositionChanged: {
selectionArea.endX = mouseX;
selectionArea.endY = mouseY;
}
onReleased: selectionArea.endSelection();
}
}

View file

@ -1,99 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
WlrLayershell {
signal selectionComplete(x: real, y: real, width: real, height: real)
color: "transparent"
visible: selectionArea.selecting || selectionArea.initializing
exclusionMode: ExclusionMode.Ignore
layer: WlrLayer.Overlay
namespace: "termspawner"
anchors {
left: true
right: true
top: true
bottom: true
}
property var selectionArea: area
Rectangle {
id: area
property bool selecting: false
property bool initializing: false
property bool locked: false
property real startX: 0
property real startY: 0
property real endX: 0
property real endY: 0
readonly property bool bigEnough: width > 300 && height > 150
border.color: bigEnough ? "#ee33ccff" : "#aa595959"
border.width: 1
radius: 5
color: "#66001017"
visible: selecting
x: Math.min(startX, endX) - border.width
y: Math.min(startY, endY) - border.width
width: Math.max(startX, endX) - x + border.width * 2
height: Math.max(startY, endY) - y + border.width * 2
function startSelection(initialize: bool) {
locked = false;
if (!initialize) {
selecting = true;
return;
}
initializing = true
if (selecting) {
area.startX = mouseArea.mouseX;
area.startY = mouseArea.mouseY;
area.endX = mouseArea.mouseX;
area.endY = mouseArea.mouseY;
}
}
function endSelection() {
const wasSelecting = selecting;
initializing = false;
if (wasSelecting && bigEnough) {
locked = true;
selectionComplete(x + 1, y + 1, width - 2, height - 2);
} else {
selecting = false;
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
if (area.initializing) {
if (!containsMouse) {
area.initializing = false;
return;
}
area.startX = mouseX;
area.startY = mouseY;
area.initializing = false;
area.selecting = true;
}
if (!selectionArea.locked) {
area.endX = mouseX;
area.endY = mouseY;
}
}
}
}

View file

@ -24,16 +24,6 @@ Singleton {
curve.type: Easing.InQuart curve.type: Easing.InQuart
} }
property var time: new Date();
Timer {
interval: 1000
running: true
repeat: true
onTriggered: time = new Date()
}
function interpolateColors(x: real, a: color, b: color): color { function interpolateColors(x: real, a: color, b: color): color {
const xa = 1.0 - x; 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); 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

@ -1,19 +1,14 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Hyprland import Quickshell.Hyprland
Singleton { Singleton {
readonly property alias termSelect: termSelectBind.pressed;
signal screenshot(); signal screenshot();
Shortcut { IpcHandler {
name: "screenshot" target: "screenshot"
onPressed: screenshot() function takeScreenshot() { screenshot(); }
}
Shortcut {
id: termSelectBind
name: "termselect"
} }
} }

View file

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

View file

@ -0,0 +1,112 @@
import QtQuick
// kind of like a lighter StackView which handles replacement better.
Item {
id: root
property Component enterTransition: XAnimator {
from: root.width
duration: 3000
}
property Component exitTransition: XAnimator {
to: target.x - target.width
duration: 3000
}
property bool animate: this.visible;
onAnimateChanged: {
if (!this.animate) this.finishAnimations();
}
property Component itemComponent: SlideViewItem {}
property SlideViewItem activeItem: null;
property Item pendingItem: null;
property bool pendingNoAnim: false;
property list<SlideViewItem> removingItems;
readonly property bool animating: activeItem?.activeAnimation != null
function replace(component: Component, defaults: var, noanim: bool) {
this.pendingNoAnim = noanim;
if (component) {
const props = defaults ?? {};
props.parent = null;
props.width = Qt.binding(() => this.width);
props.height = Qt.binding(() => this.height);
const item = component.createObject(this, props);
if (pendingItem) pendingItem.destroy();
pendingItem = item;
const ready = item?.svReady ?? true;
if (ready) finishPending();
} else {
finishPending(); // remove
}
}
Connections {
target: pendingItem
function onSvReadyChanged() {
if (pendingItem.svReady) {
root.finishPending();
}
}
}
function finishPending() {
const noanim = this.pendingNoAnim || !this.animate;
if (this.activeItem) {
if (noanim) {
this.activeItem.destroyAll();
this.activeItem = null;
} else {
removingItems.push(this.activeItem);
this.activeItem.animationCompleted.connect(item => root.removeItem(item));
this.activeItem.stopIfRunning();
this.activeItem.createAnimation(exitTransition);
this.activeItem = null;
}
}
if (!this.animate) finishAnimations();
if (this.pendingItem) {
pendingItem.parent = this;
this.activeItem = itemComponent.createObject(this, { item: this.pendingItem });
this.pendingItem = null;
if (!noanim) {
this.activeItem.createAnimation(enterTransition);
}
}
}
function removeItem(item: SlideViewItem) {
item.destroyAll();
for (const i = 0; i !== this.removingItems.length; i++) {
if (this.removingItems[i] === item) {
removingItems.splice(i, 1);
break;
}
}
}
function finishAnimations() {
this.removingItems.forEach(item => item.destroyAll())
this.removingItems = [];
if (this.activeItem) {
this.activeItem.finishIfRunning();
}
}
Component.onDestruction: {
this.removingItems.forEach(item => item.destroyAll());
this.activeItem?.destroyAll();
this.pendingItem?.destroy();
}
}

View file

@ -0,0 +1,47 @@
import Quickshell
import QtQuick
QtObject {
id: root
required property Item item;
property Animation activeAnimation: null;
signal animationCompleted(self: SlideViewItem);
property Connections __animConnection: Connections {
target: activeAnimation
function onStopped() {
root.activeAnimation.destroy();
root.animationCompleted(root);
}
}
function createAnimation(component: Component) {
this.stopIfRunning();
this.activeAnimation = component.createObject(this, { target: this.item });
this.activeAnimation.running = true;
}
function stopIfRunning() {
if (this.activeAnimation) {
this.activeAnimation.stop();
this.activeAnimation = null;
}
}
function finishIfRunning() {
if (this.activeAnimation) {
// animator types dont handle complete correctly.
this.activeAnimation.complete();
this.activeAnimation.stop();
this.item.x = 0;
this.item.y = 0;
this.activeAnimation = null;
}
}
function destroyAll() {
this.item.destroy();
this.destroy();
}
}

View file

@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
@ -5,7 +6,8 @@ import Quickshell
import "systray" as SysTray import "systray" as SysTray
import "audio" as Audio import "audio" as Audio
import "mpris" as Mpris import "mpris" as Mpris
import "workspaces" as Workspaces import "power" as Power
import "root:notifications" as Notifs
BarContainment { BarContainment {
id: root id: root
@ -13,6 +15,7 @@ BarContainment {
property bool isSoleBar: Quickshell.screens.length == 1; property bool isSoleBar: Quickshell.screens.length == 1;
ColumnLayout { ColumnLayout {
anchors { anchors {
left: parent.left left: parent.left
right: parent.right right: parent.right
@ -21,24 +24,32 @@ BarContainment {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0
Loader { Notifs.NotificationWidget {
active: isSoleBar
Layout.preferredHeight: active ? implicitHeight : 0;
Layout.fillWidth: true Layout.fillWidth: true
bar: root
sourceComponent: Workspaces.Widget {
bar: root
wsBaseIndex: 1
}
} }
Workspaces.Widget { ColumnLayout {
bar: root spacing: 0
Layout.fillWidth: true
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1; Loader {
hideWhenEmpty: isSoleBar active: root.isSoleBar
Layout.preferredHeight: active ? implicitHeight : 0;
Layout.fillWidth: true
sourceComponent: Workspaces {
bar: root
wsBaseIndex: 1
}
}
Workspaces {
bar: root
Layout.fillWidth: true
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
hideWhenEmpty: root.isSoleBar
}
} }
} }
} }
@ -65,8 +76,15 @@ BarContainment {
Layout.fillWidth: true Layout.fillWidth: true
} }
ClockWidget { Power.Power {
bar: root
Layout.fillWidth: true Layout.fillWidth: true
} }
ClockWidget {
bar: root
Layout.fillWidth: true
}
} }
} }

View file

@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Effects
FullwidthMouseArea {
id: root
property bool showPressed: mouseArea.pressed;
property real baseMargin: 0;
property bool directScale: false;
readonly property Item contentItem: mContentItem;
default property alias contentItemData: mContentItem.data;
property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
Behavior on targetBrightness { SmoothedAnimation { velocity: 600 } }
property real targetMargins: root.showPressed ? 3 : 0;
Behavior on targetMargins { SmoothedAnimation { velocity: 25 } }
hoverEnabled: true
Item {
id: mContentItem
anchors.fill: parent;
anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins);
scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0;
opacity: root.enabled ? 1.0 : 0.5;
Behavior on opacity { SmoothedAnimation { velocity: 5 } }
layer.enabled: root.targetBrightness != 0
layer.effect: MultiEffect { brightness: root.targetBrightness / 1000 }
}
}

View file

@ -1,8 +1,8 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import "root:." import ".."
import "root:lock" as Lock import "../lock" as Lock
PanelWindow { PanelWindow {
id: root id: root
@ -20,8 +20,8 @@ PanelWindow {
exclusiveZone: width - margins.left exclusiveZone: width - margins.left
color: "transparent" color: "transparent"
WlrLayershell.namespace: "shell:bar"
WlrLayershell.namespace: "shell:bar"
readonly property var tooltip: tooltip; readonly property var tooltip: tooltip;
Tooltip { Tooltip {

View file

@ -1,31 +0,0 @@
import QtQuick
BarWidgetInner {
implicitHeight: 50
SequentialAnimation on implicitHeight {
loops: Animation.Infinite
PropertyAnimation { to: 70; duration: 1000 }
PropertyAnimation { to: 40; duration: 1000 }
}
property int len: 1
Text {
anchors.centerIn: parent
text: `8${'='.repeat(len)}D`
font.pointSize: 16
color: "white"
PropertyAnimation on rotation {
loops: Animation.Infinite
to: 365
duration: 1000
}
}
MouseArea {
anchors.fill: parent
onClicked: len += 1;
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
BarButton {
id: root
required property string image;
property alias cache: imageComponent.cache;
property alias asynchronous: imageComponent.asynchronous;
property bool scaleIcon: !asynchronous
Image {
id: imageComponent
anchors.fill: parent
source: root.image
sourceSize.width: scaleIcon ? width : (root.width - baseMargin)
sourceSize.height: scaleIcon ? height : (root.height - baseMargin)
cache: false
}
}

View file

@ -1,41 +1,67 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import ".." import ".."
OverlayWidget { BarWidgetInner {
expandedWidth: 600 id: root
expandedHeight: 600 required property var bar;
BarWidgetInner { implicitHeight: layout.implicitHeight
implicitHeight: layout.implicitHeight
ColumnLayout { SystemClock {
id: layout id: clock
spacing: 0 precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes;
}
anchors { BarButton {
right: parent.right id: button
left: parent.left anchors.fill: parent
fillWindowWidth: true
acceptedButtons: Qt.NoButton
ColumnLayout {
id: layout
spacing: 0
anchors {
right: parent.right
left: parent.left
}
Text {
Layout.alignment: Qt.AlignHCenter
text: {
const hours = clock.hours.toString().padStart(2, '0')
const minutes = clock.minutes.toString().padStart(2, '0')
return `${hours}\n${minutes}`
}
font.pointSize: 18
color: "white"
}
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Loader {
active: tooltip.visible
sourceComponent: Label {
text: {
// SystemClock can send an update slightly (<50ms) before the
// time changes. We use its readout so the widget and tooltip match.
const hours = clock.hours.toString().padStart(2, '0');
const minutes = clock.minutes.toString().padStart(2, '0');
const seconds = clock.seconds.toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}\n` + new Date().toLocaleString(Qt.locale("en_US"), "dddd, MMMM d, yyyy");
}
} }
Text {
Layout.alignment: Qt.AlignHCenter
text: ShellGlobals.time.getHours().toString().padStart(2, '0')
font.pointSize: 18
color: "#a0ffffff"
}
Text {
Layout.alignment: Qt.AlignHCenter
text: ShellGlobals.time.getMinutes().toString().padStart(2, '0')
font.pointSize: 18
color: "#a0ffffff"
}
}
MouseArea {
anchors.fill: parent
onClicked: expanded = !expanded
} }
} }
} }

View file

@ -1,97 +0,0 @@
import QtQuick
import Quickshell
import ".."
Item {
required property var bar;
required property real expandedWidth;
required property real expandedHeight;
required default property Item widget;
property bool expanded: false;
onExpandedChanged: {
animateTo(expanded ? 1.0 : 0.0)
if (expanded) popupSurface.activeOverlay = this
}
readonly property bool fullyCollapsed: animationProgress == 0.0;
onFullyCollapsedChanged: {
if (fullyCollapsed && popupSurface.activeOverlay == this) {
popupSurface.activeOverlay = null;
}
/*if (fullyCollapsed) {
widget.x = Qt.binding(() => this.x)
}*/
}
readonly property rect collapsedLayerRect: {
void [barWindow.windowTransform, popupSurface.windowTransform, y, parent.y];
return this.mapToItem(popupSurface.contentItem, 0, 0, width, height);
}
onCollapsedLayerRectChanged: console.log(`clr: ${collapsedLayerRect}`)
onLayerRectChanged: console.log(`lr: ${layerRect}`)
onYChanged: console.log(`y: ${y}`)
readonly property rect expandedLayerRect: bar.widgetSurface.expandedPosition(this)
readonly property rect layerRect: {
const [p, xCurve, yCurve] = [animationProgress, ShellGlobals.popoutXCurve, ShellGlobals.popoutYCurve];
return Qt.rect(
xCurve.interpolate(p, collapsedLayerRect.x, expandedLayerRect.x),
yCurve.interpolate(p, collapsedLayerRect.y, expandedLayerRect.y),
xCurve.interpolate(p, collapsedLayerRect.width, expandedLayerRect.width),
yCurve.interpolate(p, collapsedLayerRect.height, expandedLayerRect.height),
);
}
implicitWidth: widget.implicitWidth
implicitHeight: widget.implicitHeight
Component.onCompleted: {
popupSurface.connectOverlay(this);
widget.x = Qt.binding(() => layerRect.x);
widget.y = Qt.binding(() => layerRect.y);
widget.width = Qt.binding(() => layerRect.width);
widget.height = Qt.binding(() => layerRect.height);
}
Component.onDestruction: {
popupSurface.disconnectOverlay(this)
}
function animateTo(target: real) {
animationProgressInternal = target * 1000
}
property real animationProgress: animationProgressInternal * 0.001
property real animationProgressInternal: 0.0 // animations seem to only have int precision
Behavior on animationProgressInternal {
SmoothedAnimation { velocity: 3000 }
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPressed: expanded = false
Rectangle {
anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutline
border.width: 1
radius: 5
color: "transparent"
opacity: mouseArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
SmoothedAnimation { velocity: 4 }
}
}
}
}

View file

@ -0,0 +1,53 @@
import QtQuick
Item {
id: root
property bool fillWindowWidth: false;
property real extraVerticalMargin: 0;
property alias mouseArea: mouseArea;
property alias hoverEnabled: mouseArea.hoverEnabled;
property alias acceptedButtons: mouseArea.acceptedButtons;
property alias pressedButtons: mouseArea.pressedButtons;
property alias containsMouse: mouseArea.containsMouse;
property alias isPressed: mouseArea.pressed;
signal clicked(event: MouseEvent);
signal entered();
signal exited();
signal pressed(event: MouseEvent);
signal released(event: MouseEvent);
signal wheel(event: WheelEvent);
MouseArea {
id: mouseArea
anchors {
fill: parent
// not much point in finding exact values
leftMargin: root.fillWindowWidth ? -50 : 0
rightMargin: root.fillWindowWidth ? -50 : 0
topMargin: -root.extraVerticalMargin
bottomMargin: -root.extraVerticalMargin
}
Component.onCompleted: {
this.clicked.connect(root.clicked);
//this.entered.connect(root.entered);
//this.exited.connect(root.exited);
//this.pressed.connect(root.pressed);
this.released.connect(root.released);
//this.wheel.connect(root.wheel);
}
// for some reason MouseArea.pressed is both a prop and signal so connect doesn't work
onPressed: event => root.pressed(event);
// connecting to onwheel seems to implicitly accept it. undo that.
onWheel: event => {
event.accepted = false;
root.wheel(event);
}
}
}

View file

@ -1,17 +0,0 @@
import QtQuick
Item {
default property Item item;
property int expandedWidth;
property int expandedHeight;
implicitHeight: item.implicitHeight
implicitWidth: item.implicitWidth
Component.onCompleted: {
item.width = Qt.binding(() => this.width)
item.height = Qt.binding(() => this.height)
}
children: [ item ]
}

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import "root:/"
Scope { Scope {
id: root id: root
@ -11,11 +12,14 @@ Scope {
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip; readonly property TooltipItem activeItem: activeMenu ?? activeTooltip;
property TooltipItem lastActiveItem: null; property TooltipItem lastActiveItem: null;
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem;
property real hangTime: lastActiveItem?.hangTime ?? 0;
property Item tooltipItem: null; property Item tooltipItem: null;
onActiveItemChanged: { onActiveItemChanged: {
if (activeItem != null) { if (activeItem != null) {
hangTimer.stop();
activeItem.targetVisible = true; activeItem.targetVisible = true;
if (tooltipItem) { if (tooltipItem) {
@ -24,10 +28,12 @@ Scope {
} }
if (lastActiveItem != null && lastActiveItem != activeItem) { if (lastActiveItem != null && lastActiveItem != activeItem) {
lastActiveItem.targetVisible = false; if (activeItem != null) lastActiveItem.targetVisible = false;
else if (root.hangTime == 0) doLastHide();
else hangTimer.start();
} }
lastActiveItem = activeItem; if (activeItem != null) lastActiveItem = activeItem;
} }
function setItem(item: TooltipItem) { function setItem(item: TooltipItem) {
@ -46,28 +52,59 @@ Scope {
} }
} }
function doLastHide() {
lastActiveItem.targetVisible = false;
}
function onHidden(item: TooltipItem) {
if (item == lastActiveItem) {
lastActiveItem = null;
}
}
Timer {
id: hangTimer
interval: root.hangTime
onTriggered: doLastHide();
}
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0;
Behavior on scaleMul { SmoothedAnimation { velocity: 5 } }
LazyLoader { LazyLoader {
id: popupLoader id: popupLoader
activeAsync: activeItem != null activeAsync: shownItem != null
PopupWindow { PopupWindow {
id: popup id: popup
parentWindow: bar
relativeX: bar.tooltipXOffset anchor {
relativeY: 0 window: bar
height: bar.height rect.x: bar.tooltipXOffset
width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth) rect.y: tooltipItem.highestAnimY
adjustment: PopupAdjustment.None
}
HyprlandWindow.opacity: root.scaleMul
//height: bar.height
width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
height: {
const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY
//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
return h
}
visible: true visible: true
color: "transparent" color: "transparent"
//color: "#20000000" //color: "#20000000"
mask: Region { mask: Region {
item: (activeItem?.hoverable ?? false) ? tooltipItem : null item: (shownItem?.hoverable ?? false) ? tooltipItem : null
} }
HyprlandFocusGrab { HyprlandFocusGrab {
active: activeItem?.isMenu ?? false active: activeItem?.isMenu ?? false
windows: [ popup, bar ] windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ]
onActiveChanged: { onActiveChanged: {
if (!active && activeItem?.isMenu) { if (!active && activeItem?.isMenu) {
activeMenu.close() activeMenu.close()
@ -75,30 +112,76 @@ Scope {
} }
} }
/*Rectangle {
color: "#10ff0000"
//y: tooltipItem.highestAnimY
height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
width: parent.width
}
Rectangle {
color: "#1000ff00"
//y: tooltipItem.highestAnimY
height: popup.height
width: parent.width
}*/
Item { Item {
id: tooltipItem id: tooltipItem
Component.onCompleted: { Component.onCompleted: {
root.tooltipItem = this; root.tooltipItem = this;
if (root.activeItem) { if (root.shownItem) {
root.activeItem.parent = this; root.shownItem.parent = this;
}
//highestAnimY = targetY - targetHeight / 2;
//lowestAnimY = targetY + targetHeight / 2;
}
transform: Scale {
origin.x: 0
origin.y: tooltipItem.height / 2
xScale: 0.9 + scaleMul * 0.1
yScale: xScale
}
clip: width != targetWidth || height != targetHeight
// bkg
BarWidgetInner {
anchors.fill: parent
color: ShellGlobals.colors.bar
}
readonly property var targetWidth: shownItem?.implicitWidth ?? 0;
readonly property var targetHeight: shownItem?.implicitHeight ?? 0;
property var largestAnimWidth: 0;
property var highestAnimY: 0; // unused due to reposition timing issues
property var lowestAnimY: bar.height;
onTargetWidthChanged: {
if (targetWidth > largestAnimWidth) {
largestAnimWidth = targetWidth;
} }
} }
readonly property var targetWidth: activeItem?.implicitWidth ?? 0; onTargetYChanged: updateYBounds();
readonly property var targetHeight: activeItem?.implicitHeight ?? 0; onTargetHeightChanged: updateYBounds();
function updateYBounds() {
if (targetY - targetHeight / 2 < highestAnimY) {
//highestAnimY = targetY - targetHeight / 2
}
property var lastTargetWidthTracker: 0; if (targetY + targetHeight / 2 > lowestAnimY) {
property var lastTargetWidth: 0; //lowestAnimY = targetY + targetHeight / 2
}
onTargetWidthChanged: {
lastTargetWidth = lastTargetWidthTracker;
lastTargetWidthTracker = targetWidth;
} }
readonly property real targetY: { readonly property real targetY: {
if (activeItem == null) return 0; if (shownItem == null) return 0;
const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y; const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
return bar.boundedY(target, activeItem.implicitHeight / 2); return bar.boundedY(target, shownItem.implicitHeight / 2);
} }
property var w: -1 property var w: -1
@ -107,15 +190,24 @@ Scope {
property var y1: -1 property var y1: -1
property var y2: -1 property var y2: -1
y: y1 y: y1 - popup.anchor.rect.y
height: y2 - y1 height: y2 - y1
SmoothedAnimation { readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
target: tooltipItem;
property: "y1"; onAnyAnimsRunningChanged: {
if (!anyAnimsRunning) {
largestAnimWidth = targetWidth
//highestAnimY = y1;
//lowestAnimY = y2;
}
}
SmoothedAnimation on y1 {
id: y1Anim
to: tooltipItem.targetY - tooltipItem.targetHeight / 2; to: tooltipItem.targetY - tooltipItem.targetHeight / 2;
onToChanged: { onToChanged: {
if (tooltipItem.y1 == -1 || !(activeItem?.animateSize ?? true)) { if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
stop(); stop();
tooltipItem.y1 = to; tooltipItem.y1 = to;
} else { } else {
@ -125,12 +217,11 @@ Scope {
} }
} }
SmoothedAnimation { SmoothedAnimation on y2 {
target: tooltipItem id: y2Anim
property: "y2"
to: tooltipItem.targetY + tooltipItem.targetHeight / 2; to: tooltipItem.targetY + tooltipItem.targetHeight / 2;
onToChanged: { onToChanged: {
if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) { if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
stop(); stop();
tooltipItem.y2 = to; tooltipItem.y2 = to;
} else { } else {
@ -140,13 +231,11 @@ Scope {
} }
} }
SmoothedAnimation { SmoothedAnimation on w {
id: widthAnim id: widthAnim
target: tooltipItem
property: "w"
to: tooltipItem.targetWidth; to: tooltipItem.targetWidth;
onToChanged: { onToChanged: {
if (tooltipItem.w == -1) { if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
stop(); stop();
tooltipItem.w = to; tooltipItem.w = to;
} else { } else {

View file

@ -1,12 +1,12 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "root:/"
Item { Item {
id: root id: root
required property var tooltip; required property var tooltip;
required property Item owner; required property Item owner;
property bool isMenu: false; property bool isMenu: false;
property list<QtObject> grabWindows;
property bool hoverable: isMenu; property bool hoverable: isMenu;
property bool animateSize: true; property bool animateSize: true;
property bool show: false; property bool show: false;
@ -17,47 +17,35 @@ Item {
signal close(); signal close();
readonly property alias contentItem: contentItem;
default property alias data: contentItem.data; default property alias data: contentItem.data;
property Component backgroundComponent: BarWidgetInner { property Component backgroundComponent: null
color: ShellGlobals.colors.bar
anchors.fill: parent
}
onShowChanged: { onShowChanged: {
if (show) { if (show) tooltip.setItem(this);
hangTimer.stop(); else tooltip.removeItem(this);
tooltip.setItem(this);
} else if (hangTime == 0) {
tooltip.removeItem(this);
} else hangTimer.start();
}
Timer {
id: hangTimer
interval: hangTime
onTriggered: tooltip.removeItem(root);
} }
property bool targetVisible: false property bool targetVisible: false
property real targetOpacity: 0 property real targetOpacity: 0
opacity: targetOpacity / 1000 opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
Behavior on targetOpacity { Behavior on targetOpacity {
id: opacityAnimation id: opacityAnimation
SmoothedAnimation { velocity: 5000 } SmoothedAnimation { velocity: 5 }
} }
function snapOpacity(opacity: real) { function snapOpacity(opacity: real) {
opacityAnimation.enabled = false; opacityAnimation.enabled = false;
targetOpacity = opacity * 1000 targetOpacity = opacity;
opacityAnimation.enabled = true; opacityAnimation.enabled = true;
} }
onTargetVisibleChanged: { onTargetVisibleChanged: {
if (targetVisible) { if (targetVisible) {
visible = true; visible = true;
targetOpacity = 1000; targetOpacity = 1;
} else { } else {
close() close()
targetOpacity = 0; targetOpacity = 0;
@ -68,20 +56,21 @@ Item {
if (!targetVisible && targetOpacity == 0) { if (!targetVisible && targetOpacity == 0) {
visible = false; visible = false;
this.parent = null; this.parent = null;
if (tooltip) tooltip.onHidden(this);
} }
} }
anchors.fill: parent anchors.fill: parent
visible: false visible: false
clip: true //clip: true
implicitHeight: contentItem.implicitHeight + 10 implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
implicitWidth: contentItem.implicitWidth + 10 implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
readonly property Item item: contentItem; readonly property Item item: contentItem;
Loader { Loader {
anchors.fill: parent anchors.fill: parent
active: root.visible || root.preloadBackground active: root.backgroundComponent && (root.visible || root.preloadBackground)
asynchronous: !root.visible && root.preloadBackground asynchronous: !root.visible && root.preloadBackground
sourceComponent: backgroundComponent sourceComponent: backgroundComponent
} }
@ -91,7 +80,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: 5 anchors.margins: 5
implicitHeight: childrenRect.height implicitHeight: children[0].implicitHeight
implicitWidth: childrenRect.width implicitWidth: children[0].implicitWidth
} }
} }

View file

@ -1,17 +1,21 @@
pragma ComponentBehavior: Bound;
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Hyprland import Quickshell.Hyprland
import ".." import ".."
import "root:." import "root:."
MouseArea { FullwidthMouseArea {
id: root id: root
required property var bar; required property var bar;
required property int wsBaseIndex; required property int wsBaseIndex;
property int wsCount: 10; property int wsCount: 10;
property bool hideWhenEmpty: false; property bool hideWhenEmpty: false;
implicitHeight: column.implicitHeight + 10; implicitHeight: column.implicitHeight + 10;
fillWindowWidth: true
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
onWheel: event => { onWheel: event => {
@ -29,9 +33,6 @@ MouseArea {
property int existsCount: 0; property int existsCount: 0;
visible: !hideWhenEmpty || existsCount > 0; visible: !hideWhenEmpty || existsCount > 0;
property real animPos: 0;
Behavior on animPos { SmoothedAnimation { velocity: 100 } }
// destructor takes care of nulling // destructor takes care of nulling
signal workspaceAdded(workspace: HyprlandWorkspace); signal workspaceAdded(workspace: HyprlandWorkspace);
@ -45,20 +46,22 @@ MouseArea {
} }
Repeater { Repeater {
model: 10 model: root.wsCount
MouseArea { FullwidthMouseArea {
id: wsItem id: wsItem
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`); onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 15 implicitHeight: 15
fillWindowWidth: true
required property int index; required property int index;
property int wsIndex: wsBaseIndex + index; property int wsIndex: root.wsBaseIndex + index;
property HyprlandWorkspace workspace: null; property HyprlandWorkspace workspace: null;
property bool exists: workspace != null; property bool exists: workspace != null;
property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace; property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace;
onActiveChanged: { onActiveChanged: {
if (active) root.currentIndex = wsIndex; if (active) root.currentIndex = wsIndex;
@ -78,21 +81,21 @@ MouseArea {
} }
} }
property real animActive: active ? 100 : 0 property real animActive: active ? 1 : 0
Behavior on animActive { NumberAnimation { duration: 100 } } Behavior on animActive { NumberAnimation { duration: 150 } }
property real animExists: exists ? 100 : 0 property real animExists: exists ? 1 : 0
Behavior on animExists { NumberAnimation { duration: 100 } } Behavior on animExists { NumberAnimation { duration: 100 } }
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
height: 10 height: 10
width: parent.width width: parent.width
scale: 1 + animActive * 0.003 scale: 1 + wsItem.animActive * 0.3
radius: height / 2 radius: height / 2
border.color: ShellGlobals.colors.widgetOutline border.color: ShellGlobals.colors.widgetOutline
border.width: 1 border.width: 1
color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive); color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
} }
} }
} }

View file

@ -12,14 +12,20 @@ ClickableIcon {
implicitHeight: width; implicitHeight: width;
acceptedButtons: Qt.LeftButton | Qt.RightButton; acceptedButtons: Qt.LeftButton | Qt.RightButton;
showPressed: mixerOpen fillWindowWidth: true
showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton)
onPressed: event => {
event.accepted = true;
if (event.button === Qt.RightButton) {
mixerOpen = !mixerOpen;
}
}
onClicked: event => { onClicked: event => {
event.accepted = true;
if (event.button === Qt.LeftButton) { if (event.button === Qt.LeftButton) {
event.accepted = true;
node.audio.muted = !node.audio.muted; node.audio.muted = !node.audio.muted;
} else if (event.button === Qt.RightButton) {
mixerOpen = !mixerOpen;
} }
} }
@ -71,7 +77,16 @@ ClickableIcon {
sourceComponent: Mixer { sourceComponent: Mixer {
width: 550 width: 550
trackedNode: node trackedNode: node
nodeList: Pipewire.nodes.values.filter(n => n.audio && !n.isStream && n.isSink == node.isSink)
nodeImage: root.image nodeImage: root.image
onSelected: n => {
if (node.isSink) {
Pipewire.preferredDefaultAudioSink = n;
} else {
Pipewire.preferredDefaultAudioSource = n;
}
}
} }
} }
} }

View file

@ -1,12 +1,18 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import ".." import ".."
import "../.." import "../.."
ColumnLayout { ColumnLayout {
id: root
required property PwNode trackedNode; required property PwNode trackedNode;
required property string nodeImage; required property string nodeImage;
required property list<PwNode> nodeList;
signal selected(node: PwNode);
PwNodeLinkTracker { PwNodeLinkTracker {
id: linkTracker id: linkTracker
@ -15,10 +21,13 @@ ColumnLayout {
PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] } PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
MixerEntry { MixerEntry/*WithSelect*/ {
id: nodeEntry id: nodeEntry
node: trackedNode node: trackedNode
//nodeList: root.nodeList
image: nodeImage image: nodeImage
Component.onCompleted: this.selected.connect(root.selected);
} }
Rectangle { Rectangle {
@ -49,7 +58,7 @@ ColumnLayout {
// special cases :( // special cases :(
if (icon == "firefox") icon = "firefox-devedition"; if (icon == "firefox") icon = "firefox-devedition";
return `image://icon/${icon}` return Quickshell.iconPath(icon)
} }
} }
} }

View file

@ -1,51 +1,11 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
RowLayout { MixerEntryBase {
id: root id: root
required property PwNode node;
required property string image;
property int state: PwLinkState.Unlinked;
PwObjectTracker { objects: [ node ] } headerComponent: Text {
color: "white"
ClickableIcon { elide: Text.ElideRight
image: root.image text: root.getNodeName(root.node)
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,50 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
RowLayout {
id: root
required property PwNode node;
required property string image;
required property Item headerComponent;
property int state: PwLinkState.Unlinked;
function getNodeName(node: PwNode): string {
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
const mediaName = node.properties["media.name"];
return mediaName != undefined ? `${name} - ${mediaName}` : name + node.id;
}
PwObjectTracker { objects: [ node ] }
ClickableIcon {
image: root.image
asynchronous: true
implicitHeight: 40
implicitWidth: height
}
ColumnLayout {
Item {
id: container
Layout.fillWidth: true
implicitWidth: headerComponent.implicitWidth
implicitHeight: headerComponent.implicitHeight
children: [ headerComponent ]
Binding { root.headerComponent.anchors.fill: container }
}
VolumeSlider {
Layout.fillWidth: true
value: node.audio.volume
onValueChanged: node.audio.volume = value
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.Pipewire
MixerEntryBase {
id: root
required property list<PwNode> nodeList;
signal selected(node: PwNode);
headerComponent: ComboBox {
model: nodeList.map(node => root.getNodeName(node));
currentIndex: nodeList.findIndex(node => node == root.node)
onActivated: index => root.selected(nodeList[index])
}
}

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 650 B

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 474 B

View file

@ -82,7 +82,7 @@ BarWidgetInner {
x: blurRadius x: blurRadius
width: blur.width - blurRadius * 2 width: blur.width - blurRadius * 2
height: blur.height height: blur.height
clip: true
GaussianBlur { GaussianBlur {
source: blurSource source: blurSource
x: -parent.x x: -parent.x
@ -139,23 +139,27 @@ BarWidgetInner {
readonly property Rectangle overlay: overlayItem; readonly property Rectangle overlay: overlayItem;
Rectangle { Rectangle {
id: overlayItem id: overlayItem
visible: false visible: true
anchors.fill: parent anchors.fill: parent
border.color: ShellGlobals.colors.widgetOutlineSeparate radius: root.radius
border.width: 0//1
radius: 0//root.radius
color: "transparent" color: "transparent"
Rectangle {
anchors.fill: parent
radius: root.radius
color: "transparent"
border.color: ShellGlobals.colors.widgetOutlineSeparate;
border.width: 1
}
} }
// slightly offset on the corners :/ // slightly offset on the corners :/
layer.enabled: true layer.enabled: true
layer.effect: ShaderEffect { layer.effect: OpacityMask {
fragmentShader: "radial_clip.frag.qsb" maskSource: Rectangle {
// +1 seems to match Rectangle width: root.width
property real radius: root.radius + 1 height: root.height
property size size: Qt.size(root.width, root.height) radius: root.radius
property real borderWidth: 1//.5 }
property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
property color tint: overlayItem.color
} }
} }

View file

@ -1,9 +1,11 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
import QtQml.Models
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Hyprland
import "../.." import "../.."
Singleton { Singleton {
@ -15,51 +17,58 @@ Singleton {
property bool __reverse: false; property bool __reverse: false;
property var activeTrack; property var activeTrack;
Component.onCompleted: {
for (const player of Mpris.players.values) { Instantiator {
if (player.playbackState == MprisPlaybackState.Playing) { model: Mpris.players;
if (root.trackedPlayer == null) {
root.trackedPlayer = player; Connections {
required property MprisPlayer modelData;
target: modelData;
Component.onCompleted: {
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData;
} }
} }
player.playbackStateChanged.connect(() => { Component.onDestruction: {
if (root.trackedPlayer !== player) root.trackedPlayer = player; if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
}); for (const player of Mpris.players.values) {
if (player.playbackState.isPlaying) {
root.trackedPlayer = player;
break;
}
}
if (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0];
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData;
}
} }
} }
Connections { Connections {
target: activePlayer target: activePlayer
function onTrackChanged() { function onPostTrackChanged() {
root.updateTrack(); root.updateTrack();
} }
}
// Change the tracked player when one changes playback state or is created in a playing state. function onTrackArtUrlChanged() {
Connections { console.log("arturl:", activePlayer.trackArtUrl)
target: Mpris.players; //root.updateTrack();
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
// cantata likes to send cover updates *BEFORE* updating the track info.
// as such, art url changes shouldn't be able to break the reverse animation
const r = root.__reverse;
root.updateTrack();
root.__reverse = r;
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;
}
}
} }
} }
} }
@ -67,27 +76,23 @@ Singleton {
onActivePlayerChanged: this.updateTrack(); onActivePlayerChanged: this.updateTrack();
function updateTrack() { function updateTrack() {
const metadata = this.activePlayer?.metadata ?? {}; //console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
this.activeTrack = { this.activeTrack = {
artUrl: metadata["mpris:artUrl"] ?? "", uniqueId: this.activePlayer?.uniqueId ?? 0,
title: metadata["xesam:title"] ?? "", artUrl: this.activePlayer?.trackArtUrl ?? "",
artist: metadata["xesam:artist"] ?? "", title: this.activePlayer?.trackTitle || "Unknown Title",
artist: this.activePlayer?.trackArtist || "Unknown Artist",
album: this.activePlayer?.trackAlbum || "Unknown Album",
}; };
this.trackChanged(__reverse); this.trackChanged(__reverse);
this.__reverse = false; this.__reverse = false;
} }
property bool isPlaying: this.activePlayer && this.activePlayer.playbackState == MprisPlaybackState.Playing; property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying;
property bool canPlay: this.activePlayer?.canPlay ?? false; property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false;
function play() { function togglePlaying() {
if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing; if (this.canTogglePlaying) this.activePlayer.togglePlaying();
}
property bool canPause: this.activePlayer?.canPause ?? false;
function pause() {
if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
} }
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false; property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
@ -125,7 +130,7 @@ Singleton {
} }
function setActivePlayer(player: MprisPlayer) { function setActivePlayer(player: MprisPlayer) {
const targetPlayer = player ?? MprisPlayer.players[0]; const targetPlayer = player ?? Mpris.players[0];
console.log(`setactive: ${targetPlayer} from ${activePlayer}`) console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
if (targetPlayer && this.activePlayer) { if (targetPlayer && this.activePlayer) {
@ -138,31 +143,17 @@ Singleton {
this.trackedPlayer = targetPlayer; this.trackedPlayer = targetPlayer;
} }
Shortcut { IpcHandler {
name: "music-pauseall"; target: "mpris"
onPressed: {
for (let i = 0; i < Mpris.players.length; i++) { function pauseAll(): void {
const player = Mpris.players[i]; for (const player of Mpris.players.values) {
if (player.canPause) player.playbackState = MprisPlaybackState.Paused; if (player.canPause) player.pause();
} }
} }
}
Shortcut { function playPause(): void { root.togglePlaying(); }
name: "music-playpause"; function previous(): void { root.previous(); }
onPressed: { function next(): void { root.next(); }
if (root.isPlaying) root.pause();
else root.play();
}
}
Shortcut {
name: "music-previous";
onPressed: root.previous();
}
Shortcut {
name: "music-next";
onPressed: root.next();
} }
} }

View file

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

View file

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

View file

@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
@ -7,9 +9,10 @@ import Quickshell.Services.Mpris
import ".." import ".."
import "../.." import "../.."
MouseArea { FullwidthMouseArea {
id: root id: root
hoverEnabled: true hoverEnabled: true
fillWindowWidth: true
required property var bar; required property var bar;
implicitHeight: column.implicitHeight + 10 implicitHeight: column.implicitHeight + 10
@ -27,12 +30,12 @@ MouseArea {
property alias widgetOpen: persist.widgetOpen; property alias widgetOpen: persist.widgetOpen;
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onClicked: widgetOpen = !widgetOpen onPressed: widgetOpen = !widgetOpen
onWheel: event => { onWheel: event => {
event.accepted = true; event.accepted = true;
if (MprisController.canChangeVolume) { if (MprisController.canChangeVolume) {
this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05)); root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
} }
} }
@ -42,7 +45,7 @@ MouseArea {
id: widget id: widget
anchors.fill: parent anchors.fill: parent
property real scaleMul: root.pressed || widgetOpen ? 100 : 1 property real scaleMul: widgetOpen ? 100 : 1
Behavior on scaleMul { SmoothedAnimation { velocity: 600 } } Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width) scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
@ -56,8 +59,10 @@ MouseArea {
BackgroundArt { BackgroundArt {
id: bkg id: bkg
anchors.fill: parent anchors.fill: parent
overlay.color: "#30000000"
function updateArt(reverse: bool) { function updateArt(reverse: bool) {
console.log("update art", MprisController.activeTrack.artUrl)
this.setArt(MprisController.activeTrack.artUrl, reverse, false) this.setArt(MprisController.activeTrack.artUrl, reverse, false)
} }
@ -86,6 +91,7 @@ MouseArea {
implicitHeight: width implicitHeight: width
scaleIcon: false scaleIcon: false
baseMargin: 3 baseMargin: 3
hoverEnabled: false
enabled: MprisController.canGoPrevious; enabled: MprisController.canGoPrevious;
onClicked: MprisController.previous(); onClicked: MprisController.previous();
} }
@ -95,11 +101,9 @@ MouseArea {
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`; image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
implicitHeight: width implicitHeight: width
scaleIcon: false scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay; hoverEnabled: false
onClicked: { enabled: MprisController.canTogglePlaying;
if (MprisController.isPlaying) MprisController.pause(); onClicked: MprisController.togglePlaying();
else MprisController.play();
}
} }
ClickableIcon { ClickableIcon {
@ -108,51 +112,162 @@ MouseArea {
implicitHeight: width implicitHeight: width
scaleIcon: false scaleIcon: false
baseMargin: 3 baseMargin: 3
hoverEnabled: false
enabled: MprisController.canGoNext; enabled: MprisController.canGoNext;
onClicked: MprisController.next(); onClicked: MprisController.next();
} }
} }
property var tooltip: TooltipItem { property Scope positionInfo: Scope {
id: positionInfo
property var player: root.activePlayer;
property int position: Math.floor(player.position);
property int length: Math.floor(player.length);
FrameAnimation {
id: posTracker;
running: positionInfo.player.isPlaying && (tooltip.visible || rightclickMenu.visible);
onTriggered: positionInfo.player.positionChanged();
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip id: tooltip
tooltip: bar.tooltip tooltip: bar.tooltip
owner: root owner: root
show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false) show: root.containsMouse
//implicitHeight: root.height - 10 /*ColumnLayout {
//implicitWidth: childrenRect.width ColumnLayout {
visible: MprisController.activePlayer != null
Label { text: MprisController.activeTrack?.title ?? "" }
Label {
text: {
const artist = MprisController.activeTrack?.artist ?? "";
const album = MprisController.activeTrack?.album ?? "";
return artist + (album ? ` - ${album}` : "");
}
}
Label { text: MprisController.activePlayer?.identity ?? "" }
}
Label {
visible: MprisController.activePlayer == null
text: "No media playing"
}
Rectangle { implicitHeight: 10; color: "white"; Layout.fillWidth: true }
}*/
contentItem.anchors.margins: 0
Item { Item {
implicitWidth: 200 id: ttcontent
implicitHeight: 100 width: parent.width
} height: Math.max(parent.height, implicitHeight)
implicitWidth: cl.implicitWidth + 10
implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0)
/*Loader { ColumnLayout {
active: tooltip.visible id: cl
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 5
}
sourceComponent: ColumnLayout { //visible: MprisController.activePlayer != null
height: root.height - 10
RowLayout { FontMetrics { id: fontmetrics }
Image {
Layout.fillHeight: true component FullheightLabel: Item {
source: mainPlayer.metadata["mpris:artUrl"] ?? "" implicitHeight: fontmetrics.height
implicitWidth: label.implicitWidth
property alias text: label.text
cache: false
fillMode: Image.PreserveAspectCrop
sourceSize.width: height
sourceSize.height: height
}
Label { Label {
text: mainPlayer.identity id: label
anchors.verticalCenter: parent.verticalCenter
} }
} }
Slider { FullheightLabel {
Layout.fillWidth: true visible: MprisController.activePlayer != null
text: MprisController.activeTrack?.title ?? ""
}
FullheightLabel {
visible: MprisController.activePlayer != null
text: MprisController.activeTrack?.artist ?? ""
/*text: {
const artist = MprisController.activeTrack?.artist ?? "";
const album = MprisController.activeTrack?.album ?? "";
return artist + (album ? ` - ${album}` : "");
}*/
}
Label {
text: {
if (!MprisController.activePlayer) return "No media playing";
return MprisController.activePlayer?.identity + " - "
+ positionInfo.timeStr(positionInfo.position) + " / "
+ positionInfo.timeStr(positionInfo.length);
}
} }
} }
}*/
Rectangle {
id: ttprect
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
color: "#30ceffff"
implicitHeight: 8
visible: MprisController.activePlayer != null
Rectangle {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
color: "#80ceffff"
width: parent.width * (root.activePlayer.position / root.activePlayer.length)
}
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: ttcontent.width
height: ttcontent.height
bottomLeftRadius: 5
bottomRightRadius: 5
}
}
}
} }
property var rightclickMenu: TooltipItem { property var rightclickMenu: TooltipItem {
@ -182,6 +297,7 @@ MouseArea {
target: MprisController target: MprisController
function onTrackChanged(reverse: bool) { function onTrackChanged(reverse: bool) {
console.log(`track changed: rev: ${reverse}`)
popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false); popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
} }
} }
@ -191,71 +307,41 @@ MouseArea {
} }
} }
contentItem {
implicitWidth: 500
implicitHeight: 650
}
Loader { Loader {
active: rightclickMenu.visible
width: 500 width: 500
height: 650 height: 650
active: rightclickMenu.visible
sourceComponent: ColumnLayout { sourceComponent: ColumnLayout {
property var player: activePlayer;
anchors.fill: parent; anchors.fill: parent;
property int position: 0; property var player: root.activePlayer;
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 { Connections {
target: MprisController target: MprisController
function onTrackChanged(reverse: bool) { function onTrackChanged(reverse: bool) {
trackStack.updateTrack(reverse, false); 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 { Item {
id: playerSelectorContainment id: playerSelectorContainment
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: playerSelector.implicitHeight + 20 implicitHeight: playerSelector.implicitHeight + 20
implicitWidth: playerSelector.implicitWidth implicitWidth: playerSelector.implicitWidth
ScrollView { RowLayout { //ScrollView {
id: playerSelector id: playerSelector
anchors.centerIn: parent anchors.centerIn: parent
width: Math.min(implicitWidth, playerSelectorContainment.width) width: Math.min(implicitWidth, playerSelectorContainment.width)
RowLayout { //RowLayout {
Repeater { Repeater {
model: Mpris.players model: Mpris.players
@ -281,8 +367,7 @@ MouseArea {
source: { source: {
const entry = DesktopEntries.byId(modelData.desktopEntry); const entry = DesktopEntries.byId(modelData.desktopEntry);
console.log(`ent ${entry} id ${modelData.desktopEntry}`) console.log(`ent ${entry} id ${modelData.desktopEntry}`)
if (!entry) return "image://icon/"; return Quickshell.iconPath(entry?.icon);
return `image://icon/${entry.icon}`;
} }
//asynchronous: true //asynchronous: true
@ -292,18 +377,18 @@ MouseArea {
} }
} }
} }
} //}
} }
} }
} }
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.bottomMargin: 10 Layout.bottomMargin: 20
Label { Label {
anchors.centerIn: parent anchors.centerIn: parent
text: activePlayer.identity text: root.activePlayer.identity
} }
} }
@ -311,7 +396,13 @@ MouseArea {
id: trackStack id: trackStack
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 400 implicitHeight: 400
clip: animating || (lastFlicked?.contentX ?? 0) != 0
// inverse of default tooltip margin - 1px for border
Layout.leftMargin: -4
Layout.rightMargin: -4
property Flickable lastFlicked;
property bool reverse: false; property bool reverse: false;
Component.onCompleted: updateTrack(false, true); Component.onCompleted: updateTrack(false, true);
@ -333,14 +424,16 @@ MouseArea {
// but may take longer if the image is huge. // but may take longer if the image is huge.
readonly property bool svReady: img.status === Image.Ready; readonly property bool svReady: img.status === Image.Ready;
contentWidth: width + 1 contentWidth: width + 1
onDragStarted: trackStack.lastFlicked = this
onDragEnded: { onDragEnded: {
return; //return;
console.log(`dragend ${contentX}`) console.log(`dragend ${contentX}`)
if (Math.abs(contentX) > 75) { if (Math.abs(contentX) > 75) {
if (contentX < 0) MprisController.previous(); if (contentX < 0) MprisController.previous();
else if (contentX > 0) MprisController.next(); else if (contentX > 0) MprisController.next();
} }
} }
ColumnLayout { ColumnLayout {
id: trackContent id: trackContent
width: flickable.width width: flickable.width
@ -348,7 +441,7 @@ MouseArea {
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 300//img.implicitHeight implicitHeight: 302//img.implicitHeight
implicitWidth: img.implicitWidth implicitWidth: img.implicitWidth
Image { Image {
@ -362,27 +455,50 @@ MouseArea {
sourceSize.height: 300 sourceSize.height: 300
sourceSize.width: 300 sourceSize.width: 300
layer.enabled: true
layer.effect: OpacityMask {
cached: true
maskSource: Rectangle {
width: img.width
height: img.height
radius: 5
}
}
} }
} }
Item { component CenteredText: Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 20
property alias text: label.text
property alias font: label.font
Label { Label {
id: label
visible: text != ""
anchors.centerIn: parent anchors.centerIn: parent
text: track.title elide: Text.ElideRight
width: Math.min(parent.width - 20, implicitWidth)
} }
} }
Item { CenteredText {
Layout.fillWidth: true
Layout.topMargin: 20 Layout.topMargin: 20
text: track.title
font.pointSize: albumLabel.font.pointSize + 1
}
Label { CenteredText {
anchors.centerIn: parent id: albumLabel
text: track.artist Layout.topMargin: 18
} text: track.album
opacity: 0.8
}
CenteredText {
Layout.topMargin: 25
text: track.artist
} }
Item { Layout.fillHeight: true } Item { Layout.fillHeight: true }
@ -462,11 +578,8 @@ MouseArea {
implicitWidth: 80 implicitWidth: 80
implicitHeight: width implicitHeight: width
scaleIcon: false scaleIcon: false
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay enabled: MprisController.canTogglePlaying;
onClicked: { onClicked: MprisController.togglePlaying();
if (MprisController.isPlaying) MprisController.pause();
else MprisController.play();
}
} }
ClickableIcon { ClickableIcon {
@ -491,33 +604,70 @@ MouseArea {
} }
RowLayout { RowLayout {
Layout.margins: 5
Label { Label {
Layout.preferredWidth: lengthLabel.implicitWidth Layout.preferredWidth: lengthLabel.implicitWidth
text: timeStr(position) text: positionInfo.timeStr(positionInfo.position)
} }
MediaSlider { MediaSlider {
id: slider id: slider
property bool bindSlider: true;
property real boundAnimStart: 0;
property real boundAnimFactor: 1;
property real lastPosition: 0;
property real lastLength: 0;
property real boundPosition: {
const ppos = player.position / player.length;
const bpos = boundAnimStart;
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
}
NumberAnimation {
id: boundAnim
target: slider
property: "boundAnimFactor"
from: 0
to: 1
duration: 600
easing.type: Easing.OutExpo
}
Connections {
target: player
function onPositionChanged() {
if (false && player.position == 0 && slider.lastPosition != 0 && !boundAnim.running) {
slider.boundAnimStart = slider.lastPosition / slider.lastLength;
boundAnim.start();
}
slider.lastPosition = player.position;
slider.lastLength = player.length;
}
}
Layout.fillWidth: true Layout.fillWidth: true
property var bindSlider: true;
enabled: player.canSeek enabled: player.canSeek
from: 0 from: 0
to: player.length to: 1
onPressedChanged: { onPressedChanged: {
if (!pressed) player.position = value; if (!pressed) player.position = value * player.length;
bindSlider = !pressed; bindSlider = !pressed;
} }
Binding { Binding {
when: slider.bindSlider when: slider.bindSlider
slider.value: player.position slider.value: slider.boundPosition
} }
} }
Label { Label {
id: lengthLabel id: lengthLabel
text: timeStr(length) text: positionInfo.timeStr(positionInfo.length)
} }
} }
} }

View file

@ -0,0 +1,45 @@
import QtQuick
import Quickshell.Services.UPower
import "root:."
Item {
id: root
required property UPowerDevice device;
property real scale: 1;
readonly property bool isCharging: root.device.state == UPowerDeviceState.Charging;
readonly property bool isPluggedIn: isCharging || root.device.state == UPowerDeviceState.PendingCharge;
readonly property bool isLow: root.device.percentage <= 0.20;
width: 35 * root.scale
height: 35 * root.scale
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 4 * root.scale
}
width: 13 * root.scale
height: 23 * root.device.percentage * root.scale
radius: 2 * root.scale
color: root.isPluggedIn ? "#359040"
: ShellGlobals.interpolateColors(Math.min(1.0, Math.min(0.5, root.device.percentage) * 2), "red", "white")
}
Image {
id: img
anchors.fill: parent;
source: root.isCharging ? "root:icons/battery-charging.svg"
: root.isPluggedIn ? "root:icons/battery-plus.svg"
: root.isLow ? "root:icons/battery-warning.svg"
: "root:icons/battery-empty.svg"
sourceSize.width: parent.width
sourceSize.height: parent.height
visible: true
}
}

View file

@ -0,0 +1,224 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Widgets
import ".."
import "root:."
import "root:components"
import "power"
BarWidgetInner {
id: root
required property var bar;
readonly property var chargeState: UPower.displayDevice.state
readonly property bool isCharging: chargeState == UPowerDeviceState.Charging;
readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge;
readonly property real percentage: UPower.displayDevice.percentage
readonly property bool isLow: percentage <= 0.20
readonly property UPowerDevice batteryDevice: UPower.devices.values
.find(device => device.isLaptopBattery);
function statusStr() {
return root.isPluggedIn ? `Plugged in, ${root.isCharging ? "Charging" : "Not Charging"}`
: "Discharging";
}
property bool showMenu: false;
implicitHeight: width
color: isLow ? "#45ff6060" : ShellGlobals.colors.widget
BarButton {
id: button
anchors.fill: parent
baseMargin: 5
fillWindowWidth: true
acceptedButtons: Qt.RightButton
directScale: true
showPressed: root.showMenu
onPressed: {
root.showMenu = !root.showMenu
}
BatteryIcon {
device: UPower.displayDevice
}
}
property TooltipItem tooltip: TooltipItem {
id: tooltip
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Loader {
active: tooltip.visible
sourceComponent: Label {
text: {
const status = root.statusStr();
const percentage = Math.round(root.percentage * 100);
let str = `${percentage}% - ${status}`;
return str;
}
}
}
}
property TooltipItem rightclickMenu: TooltipItem {
id: rightclickMenu
tooltip: bar.tooltip
owner: root
isMenu: true
show: root.showMenu
onClose: root.showMenu = false
Loader {
active: rightclickMenu.visible
sourceComponent: ColumnLayout {
spacing: 10
FontMetrics { id: fm }
component SmallLabel: Label {
font.pointSize: fm.font.pointSize * 0.8
color: "#d0eeffff"
}
RowLayout {
IconImage {
source: "root:icons/gauge.svg"
implicitSize: 32
}
ColumnLayout {
spacing: 0
Label { text: "Power Profile" }
OptionSlider {
values: ["Power Save", "Balanced", "Performance"]
index: PowerProfiles.profile
onIndexChanged: PowerProfiles.profile = this.index;
implicitWidth: 350
}
}
}
RowLayout {
IconImage {
Layout.alignment: Qt.AlignTop
source: "root:icons/battery-empty.svg"
implicitSize: 32
}
ColumnLayout {
spacing: 0
RowLayout {
Label { text: "Battery" }
Item { Layout.fillWidth: true }
Label {
text: `${root.statusStr()} -`
color: "#d0eeffff"
}
Label { text: `${Math.round(root.percentage * 100)}%` }
}
ProgressBar {
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.fillWidth: true
value: UPower.displayDevice.percentage
}
RowLayout {
visible: remainingTimeLbl.text !== ""
SmallLabel { text: "Time remaining" }
Item { Layout.fillWidth: true }
SmallLabel {
id: remainingTimeLbl
text: {
const device = UPower.displayDevice;
const time = device.timeToEmpty || device.timeToFull;
if (time === 0) return "";
const minutes = Math.floor(time / 60).toString().padStart(2, '0');
return `${minutes} minutes`
}
}
}
RowLayout {
visible: root.batteryDevice.healthSupported
SmallLabel { text: "Health" }
Item { Layout.fillWidth: true }
SmallLabel {
text: `${Math.floor((root.batteryDevice?.healthPercentage ?? 0))}%`
}
}
}
}
Repeater {
model: ScriptModel {
// external devices
values: UPower.devices.values.filter(device => !device.powerSupply)
}
RowLayout {
required property UPowerDevice modelData;
IconImage {
Layout.alignment: Qt.AlignTop
source: {
switch (modelData.type) {
case UPowerDeviceType.Headset: return "root:icons/headset.svg";
}
return Quickshell.iconPath(modelData.iconName)
}
implicitSize: 32
}
ColumnLayout {
spacing: 0
RowLayout {
Label { text: modelData.model }
Item { Layout.fillWidth: true }
Label { text: `${Math.round(modelData.percentage * 100)}%` }
}
ProgressBar {
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.fillWidth: true
value: modelData.percentage
}
RowLayout {
visible: modelData.healthSupported
SmallLabel { text: "Health" }
Item { Layout.fillWidth: true }
SmallLabel {
text: `${Math.floor(modelData.healthPercentage)}%`
}
}
}
}
}
}
}
}
}

View file

@ -6,7 +6,7 @@ Item {
property bool expanded: false; property bool expanded: false;
readonly property bool open: progress != 0; readonly property bool open: progress != 0;
readonly property bool animating: internalProgress != -1 && internalProgress != 101; readonly property bool animating: internalProgress != (expanded ? 101 : -1);
implicitHeight: 16 implicitHeight: 16
implicitWidth: 16 implicitWidth: 16

View file

@ -1,14 +1,15 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Widgets
import Quickshell.DBusMenu import Quickshell.DBusMenu
import "../.." import "../.."
MouseArea { MouseArea {
id: root id: root
required property var entry; required property QsMenuEntry entry;
property alias expanded: childrenRevealer.expanded; property alias expanded: childrenRevealer.expanded;
property bool animating: childrenRevealer.animating || childrenList.animating; property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false);
// appears it won't actually create the handler when only used from MenuItemList. // appears it won't actually create the handler when only used from MenuItemList.
onExpandedChanged: {} onExpandedChanged: {}
onAnimatingChanged: {} onAnimatingChanged: {}
@ -22,7 +23,7 @@ MouseArea {
onClicked: { onClicked: {
if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded
else { else {
entry.click(); entry.triggered();
if (entry.toggleType == ToggleButtonType.None) close(); if (entry.toggleType == ToggleButtonType.None) close();
} }
} }
@ -34,19 +35,21 @@ MouseArea {
spacing: 0 spacing: 0
RowLayout { RowLayout {
id: innerRow
Item { Item {
implicitWidth: 22 implicitWidth: 22
implicitHeight: 22 implicitHeight: 22
MenuCheckBox { MenuCheckBox {
anchors.centerIn: parent anchors.centerIn: parent
visible: entry.toggleType == ToggleButtonType.CheckBox visible: entry.buttonType == QsMenuButtonType.CheckBox
checkState: entry.checkState checkState: entry.checkState
} }
MenuRadioButton { MenuRadioButton {
anchors.centerIn: parent anchors.centerIn: parent
visible: entry.toggleType == ToggleButtonType.RadioButton visible: entry.buttonType == QsMenuButtonType.RadioButton
checkState: entry.checkState checkState: entry.checkState
} }
@ -59,7 +62,7 @@ MouseArea {
} }
Text { Text {
text: entry.cleanLabel text: entry.text
color: entry.enabled ? "white" : "#bbbbbb" color: entry.enabled ? "white" : "#bbbbbb"
} }
@ -68,25 +71,32 @@ MouseArea {
implicitWidth: 22 implicitWidth: 22
implicitHeight: 22 implicitHeight: 22
Image { IconImage {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: entry.icon != ""
source: entry.icon source: entry.icon
sourceSize.height: parent.height visible: source != ""
sourceSize.width: parent.height implicitSize: parent.height
} }
} }
} }
Item { Loader {
id: childMenuLoader
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: childrenList.implicitHeight * childrenRevealer.progress Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
readonly property real widthDifference: {
Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
}
Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
active: root.expanded || root.animating
clip: true clip: true
MenuItemList { sourceComponent: MenuView {
id: childrenList id: childrenList
items: entry.children menu: entry
onClose: root.close() onClose: root.close()
anchors { anchors {

View file

@ -6,17 +6,19 @@ import "../.."
ColumnLayout { ColumnLayout {
id: root id: root
required property var items; property alias menu: menuView.menu;
property Item animatingItem: null; property Item animatingItem: null;
property bool animating: animatingItem != null; property bool animating: animatingItem != null;
signal close(); signal close();
signal submenuExpanded(item: var); signal submenuExpanded(item: var);
QsMenuOpener { id: menuView }
spacing: 0 spacing: 0
Repeater { Repeater {
model: items model: menuView.children;
Loader { Loader {
required property var modelData; required property var modelData;

View file

@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Effects import QtQuick.Effects
@ -5,102 +7,102 @@ import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import ".." import ".."
OverlayWidget { BarWidgetInner {
id: root id: root
expandedWidth: 600
expandedHeight: 800
required property var bar; required property var bar;
implicitHeight: column.implicitHeight + 10
BarWidgetInner { ColumnLayout {
implicitHeight: column.implicitHeight + 10 id: column
implicitHeight: childrenRect.height
spacing: 5
ColumnLayout { anchors {
id: column fill: parent
implicitHeight: childrenRect.height margins: 5
spacing: 5 }
anchors { Repeater {
fill: parent model: SystemTray.items;
margins: 5
}
Repeater { Item {
model: SystemTray.items; id: item
required property SystemTrayItem modelData;
Item { property bool targetMenuOpen: false;
required property var modelData;
readonly property alias menu: menuWatcher.menu;
SystemTrayMenuWatcher { Layout.fillWidth: true
id: menuWatcher; implicitHeight: width
trayItem: modelData;
ClickableIcon {
id: mouseArea
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: height
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
image: item.modelData.icon
showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
fillWindowWidth: true
extraVerticalMargin: column.spacing / 2
onClicked: event => {
event.accepted = true;
if (event.button == Qt.LeftButton) {
item.modelData.activate();
} else if (event.button == Qt.MiddleButton) {
item.modelData.secondaryActivate();
}
} }
property bool targetMenuOpen: false; onPressed: event => {
onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen if (event.button == Qt.RightButton && item.modelData.hasMenu) {
item.targetMenuOpen = !item.targetMenuOpen;
Layout.fillWidth: true
implicitHeight: width
ClickableIcon {
id: mouseArea
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
} }
width: height }
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onWheel: event => {
event.accepted = true;
const points = event.angleDelta.y / 120
item.modelData.scroll(points, false);
}
image: modelData.icon property var tooltip: TooltipItem {
showPressed: targetMenuOpen tooltip: root.bar.tooltip
owner: mouseArea
onClicked: event => { show: mouseArea.containsMouse
event.accepted = true;
if (event.button == Qt.LeftButton) { Text {
modelData.activate(); id: tooltipText
} else if (event.button == Qt.MiddleButton) { text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
modelData.secondaryActivate(); color: "white"
} else if (event.button == Qt.RightButton && menu != null) {
targetMenuOpen = !targetMenuOpen;
}
} }
}
onWheel: event => { property var rightclickMenu: TooltipItem {
event.accepted = true; id: rightclickMenu
const points = event.angleDelta.y / 120 tooltip: root.bar.tooltip
modelData.scroll(points, false); owner: mouseArea
}
property var tooltip: TooltipItem { isMenu: true
tooltip: bar.tooltip show: item.targetMenuOpen
owner: mouseArea animateSize: !(menuContentLoader?.item?.animating ?? false)
show: mouseArea.containsMouse onClose: item.targetMenuOpen = false;
Text { Loader {
id: tooltipText id: menuContentLoader
text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id active: item.targetMenuOpen || rightclickMenu.visible || mouseArea.containsMouse
color: "white"
}
}
property var rightclickMenu: TooltipItem { sourceComponent: MenuView {
tooltip: bar.tooltip menu: item.modelData.menu
owner: mouseArea onClose: item.targetMenuOpen = false;
isMenu: true
show: targetMenuOpen && menu.showChildren
animateSize: !rightclickItems.animating
onClose: targetMenuOpen = false;
MenuItemList {
id: rightclickItems
items: menu == null ? [] : menu.children
onClose: targetMenuOpen = false;
} }
} }
} }

View file

@ -0,0 +1,90 @@
import QtQuick
import Quickshell
Scope {
id: root
required property MouseArea target;
property real velocityX: 0;
property real velocityY: 0;
signal flickStarted();
signal flickCompleted();
property real dragStartX: 0;
property real dragStartY: 0;
property real dragDeltaX: 0;
property real dragDeltaY: 0;
property real dragEndX: 0;
property real dragEndY: 0;
property var sampleIdx: -1
property var tSamples: []
property var xSamples: []
property var ySamples: []
ElapsedTimer { id: velocityTimer }
function resetSamples() {
velocityTimer.restart();
sampleIdx = -1;
tSamples = [];
xSamples = [];
ySamples = [];
}
function sample() {
const deltaT = velocityTimer.elapsed();
sampleIdx++;
if (sampleIdx > 5) {
sampleIdx = 0;
}
tSamples[sampleIdx] = deltaT;
xSamples[sampleIdx] = dragDeltaX;
ySamples[sampleIdx] = dragDeltaY;
}
function updateVelocity() {
let firstIdx = sampleIdx + 1;
if (firstIdx > tSamples.length - 1) firstIdx = 0;
const deltaT = tSamples[sampleIdx] - tSamples[firstIdx];
const deltaX = xSamples[sampleIdx] - xSamples[firstIdx];
const deltaY = ySamples[sampleIdx] - ySamples[firstIdx];
root.velocityX = deltaX / deltaT;
root.velocityY = deltaY / deltaT;
}
Connections {
target: root.target;
function onPressed(event: MouseEvent) {
root.resetSamples();
root.dragDeltaX = 0;
root.dragDeltaY = 0;
root.dragStartX = event.x;
root.dragStartY = event.y;
root.flickStarted();
}
function onReleased(event: MouseEvent) {
root.dragDeltaX = event.x - root.dragStartX;
root.dragDeltaY = event.y - root.dragStartY;
root.dragEndX = event.x;
root.dragEndY = event.y;
root.sample();
root.updateVelocity();
root.flickCompleted();
}
function onPositionChanged(event: MouseEvent) {
root.dragDeltaX = event.x - root.dragStartX;
root.dragDeltaY = event.y - root.dragStartY;
root.sample();
}
}
}

View file

@ -0,0 +1,99 @@
pragma ComponentBehavior: Bound;
import QtQuick
Item {
id: root
property list<string> values;
property int index: 0;
implicitWidth: 300
implicitHeight: 40
MouseArea {
id: mouseArea
anchors.fill: parent
property real halfHandle: handle.width / 2;
property real activeWidth: groove.width - handle.width;
property real valueOffset: mouseArea.halfHandle + (root.index / (root.values.length - 1)) * mouseArea.activeWidth;
Repeater {
model: root.values
Item {
id: delegate
required property int index;
required property string modelData;
anchors.top: groove.bottom
anchors.topMargin: 2
x: mouseArea.halfHandle + (delegate.index / (root.values.length - 1)) * mouseArea.activeWidth
Rectangle {
id: mark
color: "#60eeffff"
width: 1
height: groove.height
}
Text {
anchors.top: mark.bottom
x: delegate.index === 0 ? -4
: delegate.index === root.values.length - 1 ? -this.width + 4
: -(this.width / 2);
text: delegate.modelData
color: "#a0eeffff"
}
}
}
Rectangle {
id: grooveFill
anchors {
left: groove.left
top: groove.top
bottom: groove.bottom
}
radius: 5
color: "#80ceffff"
width: mouseArea.valueOffset
}
Rectangle {
id: groove
anchors {
left: parent.left
right: parent.right
}
y: 5
implicitHeight: 7
color: "transparent"
border.color: "#20eeffff"
border.width: 1
radius: 5
}
Rectangle {
id: handle
anchors.verticalCenter: groove.verticalCenter
height: 15
width: height
radius: height * 0.5
x: mouseArea.valueOffset - width * 0.5
}
}
Binding {
when: mouseArea.pressed
root.index: Math.max(0, Math.min(root.values.length - 1, Math.round((mouseArea.mouseX / root.width) * (root.values.length - 1))));
restoreMode: Binding.RestoreBinding
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
Item {
id: root
property real from: 0.0
property real to: 1.0
property real value: 0.0
implicitHeight: 7
implicitWidth: 200
Rectangle {
id: grooveFill
anchors {
left: groove.left
top: groove.top
bottom: groove.bottom
}
radius: 5
color: "#80ceffff"
width: root.width * ((root.value - root.from) / (root.to - root.from))
}
Rectangle {
id: groove
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
height: 7
color: "transparent"
border.color: "#20eeffff"
border.width: 1
radius: 5
}
}

View file

@ -0,0 +1,36 @@
import QtQuick
Item {
id: root
onChildrenChanged: recalc();
Instantiator {
model: root.children
Connections {
required property Item modelData;
target: modelData;
function onImplicitHeightChanged() {
root.recalc();
}
function onImplicitWidthChanged() {
root.recalc();
}
}
}
function recalc() {
let y = 0
let w = 0
for (const child of this.children) {
child.y = y;
y += child.implicitHeight
if (child.implicitWidth > w) w = child.implicitWidth;
}
this.implicitHeight = y;
this.implicitWidth = w;
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Greetd
import ".."
import "lock"
ShellRoot {
GreeterContext {
id: context
onLaunch: {
lock.locked = false;
Greetd.launch(["hyprland"]);
}
}
WlSessionLock {
id: lock
locked: true
WlSessionLockSurface {
id: lockSurface
BackgroundImage {
id: backgroundImage
anchors.fill: parent
screen: lockSurface.screen
asynchronous: true
}
LockContent {
anchors.fill: parent
state: context.state
}
}
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M150.81,131.79a8,8,0,0,1,.35,7.79l-16,32a8,8,0,0,1-14.32-7.16L131.06,144H112a8,8,0,0,1-7.16-11.58l16-32a8,8,0,1,1,14.32,7.16L124.94,128H144A8,8,0,0,1,150.81,131.79ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>

After

Width:  |  Height:  |  Size: 499 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>

After

Width:  |  Height:  |  Size: 348 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Zm-28,76H136V112a8,8,0,0,0-16,0v20H100a8,8,0,0,0,0,16h20v20a8,8,0,0,0,16,0V148h20a8,8,0,0,0,0-16Z"></path></svg>

After

Width:  |  Height:  |  Size: 443 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M120,136V96a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,24a12,12,0,1,0,12,12A12,12,0,0,0,128,160ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>

After

Width:  |  Height:  |  Size: 424 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216Z"></path></svg>

After

Width:  |  Height:  |  Size: 348 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M53.92,34.62A8,8,0,1,0,42.08,45.38L58.82,63.8A79.59,79.59,0,0,0,48,104c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.8a40,40,0,0,0,78.4,0h15.44l19.44,21.38a8,8,0,1,0,11.84-10.76ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a63.65,63.65,0,0,1,6.26-27.62L168.09,184Zm166-4.73a8.13,8.13,0,0,1-2.93.55,8,8,0,0,1-7.44-5.08C196.35,156.19,192,129.75,192,104A64,64,0,0,0,96.43,48.31a8,8,0,0,1-7.9-13.91A80,80,0,0,1,208,104c0,35.35,8.05,58.59,10.52,64.88A8,8,0,0,1,214,179.25Z"></path></svg>

After

Width:  |  Height:  |  Size: 641 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a64,64,0,1,1,128,0c0,36.05,8.28,66.73,16,80Z"></path></svg>

After

Width:  |  Height:  |  Size: 424 B

View file

@ -1 +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> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" 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>

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 445 B

Before After
Before After

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M72,128a134.63,134.63,0,0,1-14.16,60.47,8,8,0,1,1-14.32-7.12A118.8,118.8,0,0,0,56,128,71.73,71.73,0,0,1,83,71.8,8,8,0,1,1,93,84.29,55.76,55.76,0,0,0,72,128Zm56-8a8,8,0,0,0-8,8,184.12,184.12,0,0,1-23,89.1,8,8,0,0,0,14,7.76A200.19,200.19,0,0,0,136,128,8,8,0,0,0,128,120Zm0-32a40,40,0,0,0-40,40,8,8,0,0,0,16,0,24,24,0,0,1,48,0,214.09,214.09,0,0,1-20.51,92A8,8,0,1,0,146,226.83,230,230,0,0,0,168,128,40,40,0,0,0,128,88Zm0-64A104.11,104.11,0,0,0,24,128a87.76,87.76,0,0,1-5,29.33,8,8,0,0,0,15.09,5.33A103.9,103.9,0,0,0,40,128a88,88,0,0,1,176,0,282.24,282.24,0,0,1-5.29,54.45,8,8,0,0,0,6.3,9.4,8.22,8.22,0,0,0,1.55.15,8,8,0,0,0,7.84-6.45A298.37,298.37,0,0,0,232,128,104.12,104.12,0,0,0,128,24ZM94.4,152.17A8,8,0,0,0,85,158.42a151,151,0,0,1-17.21,45.44,8,8,0,0,0,13.86,8,166.67,166.67,0,0,0,19-50.25A8,8,0,0,0,94.4,152.17ZM128,56a72.85,72.85,0,0,0-9,.56,8,8,0,0,0,2,15.87A56.08,56.08,0,0,1,184,128a252.12,252.12,0,0,1-1.92,31A8,8,0,0,0,189,168a8.39,8.39,0,0,0,1,.06,8,8,0,0,0,7.92-7,266.48,266.48,0,0,0,2-33A72.08,72.08,0,0,0,128,56Zm57.93,128.25a8,8,0,0,0-9.75,5.75c-1.46,5.69-3.15,11.4-5,17a8,8,0,0,0,5,10.13,7.88,7.88,0,0,0,2.55.42,8,8,0,0,0,7.58-5.46c2-5.92,3.79-12,5.35-18.05A8,8,0,0,0,185.94,184.26Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M207.06,72.67A111.24,111.24,0,0,0,128,40h-.4C66.07,40.21,16,91,16,153.13V176a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V152A111.25,111.25,0,0,0,207.06,72.67ZM224,176H119.71l54.76-75.3a8,8,0,0,0-12.94-9.42L99.92,176H32V153.13c0-3.08.15-6.12.43-9.13H56a8,8,0,0,0,0-16H35.27c10.32-38.86,44-68.24,84.73-71.66V80a8,8,0,0,0,16,0V56.33A96.14,96.14,0,0,1,221,128H200a8,8,0,0,0,0,16h23.67c.21,2.65.33,5.31.33,8Z"></path></svg>

After

Width:  |  Height:  |  Size: 523 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M201.89,54.66A103.43,103.43,0,0,0,128.79,24H128A104,104,0,0,0,24,128v56a24,24,0,0,0,24,24H64a24,24,0,0,0,24-24V144a24,24,0,0,0-24-24H40.36A88.12,88.12,0,0,1,190.54,65.93,87.39,87.39,0,0,1,215.65,120H192a24,24,0,0,0-24,24v40a24,24,0,0,0,24,24h24a24,24,0,0,1-24,24H136a8,8,0,0,0,0,16h56a40,40,0,0,0,40-40V128A103.41,103.41,0,0,0,201.89,54.66ZM64,136a8,8,0,0,1,8,8v40a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V136Zm128,56a8,8,0,0,1-8-8V144a8,8,0,0,1,8-8h24v56Z"></path></svg>

After

Width:  |  Height:  |  Size: 570 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"></path></svg>

After

Width:  |  Height:  |  Size: 252 B

View file

@ -1 +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> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" 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>

Before

Width:  |  Height:  |  Size: 353 B

After

Width:  |  Height:  |  Size: 354 B

Before After
Before After

View file

@ -1 +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> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" 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>

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 386 B

Before After
Before After

View file

@ -2,7 +2,7 @@
<svg <svg
width="32" width="32"
height="32" height="32"
fill="#ffffff" fill="#f0f0f0"
viewBox="0 0 256 256" viewBox="0 0 256 256"
data-darkreader-inline-fill="" data-darkreader-inline-fill=""
version="1.1" version="1.1"

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 580 B

Before After
Before After

View file

@ -1 +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> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" 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>

Before

Width:  |  Height:  |  Size: 521 B

After

Width:  |  Height:  |  Size: 521 B

Before After
Before After

View file

@ -2,7 +2,7 @@
<svg <svg
width="32" width="32"
height="32" height="32"
fill="#ffffff" fill="#f0f0f0"
viewBox="0 0 256 256" viewBox="0 0 256 256"
data-darkreader-inline-fill="" data-darkreader-inline-fill=""
version="1.1" version="1.1"

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 957 B

Before After
Before After

View file

@ -1 +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> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" 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>

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 627 B

Before After
Before After

View file

@ -1 +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> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" 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>

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

Before After
Before After

View file

@ -2,7 +2,7 @@
<svg <svg
width="32" width="32"
height="32" height="32"
fill="#ffffff" fill="#f0f0f0"
viewBox="0 0 256 256" viewBox="0 0 256 256"
data-darkreader-inline-fill="" data-darkreader-inline-fill=""
version="1.1" version="1.1"

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 847 B

Before After
Before After

View file

@ -1 +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> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" 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>

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 787 B

Before After
Before After

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 335 B

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 339 B

View file

@ -0,0 +1,281 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import ".."
Singleton {
PersistentProperties {
id: persist
property bool launcherOpen: false;
}
IpcHandler {
target: "launcher"
function open(): void {
persist.launcherOpen = true;
}
function close(): void {
persist.launcherOpen = false;
}
function toggle(): void {
persist.launcherOpen = !persist.launcherOpen
}
}
LazyLoader {
id: loader
activeAsync: persist.launcherOpen
PanelWindow {
width: 450
height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:launcher"
Rectangle {
//anchors.fill: parent
height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10)
Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
width: 450
color: ShellGlobals.colors.bar
radius: 5
border.color: ShellGlobals.colors.barOutline
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
anchors.bottomMargin: 0
spacing: 0
Rectangle {
id: searchContainer
Layout.fillWidth: true
implicitHeight: searchbox.implicitHeight + 10
color: "#30c0ffff"
radius: 3
border.color: "#50ffffff"
RowLayout {
id: searchbox
anchors.fill: parent
anchors.margins: 5
IconImage {
implicitSize: parent.height
source: "root:icons/magnifying-glass.svg"
}
TextInput {
id: search
Layout.fillWidth: true
color: "white"
focus: true
Keys.forwardTo: [list]
Keys.onEscapePressed: persist.launcherOpen = false
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
if (event.key == Qt.Key_J) {
list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1;
event.accepted = true;
} else if (event.key == Qt.Key_K) {
list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1;
event.accepted = true;
}
}
}
onAccepted: {
if (list.currentItem) {
list.currentItem.clicked(null);
}
}
onTextChanged: {
list.currentIndex = 0;
}
}
}
}
ListView {
id: list
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
cacheBuffer: 0 // works around QTBUG-131106
//reuseItems: true
model: ScriptModel {
values: DesktopEntries.applications.values
.map(object => {
const stxt = search.text.toLowerCase();
const ntxt = object.name.toLowerCase();
let si = 0;
let ni = 0;
let matches = [];
let startMatch = -1;
for (let si = 0; si != stxt.length; ++si) {
const sc = stxt[si];
while (true) {
// Drop any entries with letters that don't exist in order
if (ni == ntxt.length) return null;
const nc = ntxt[ni++];
if (nc == sc) {
if (startMatch == -1) startMatch = ni;
break;
} else {
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch,
});
startMatch = -1;
}
}
}
}
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch + 1,
});
}
return {
object: object,
matches: matches,
};
})
.filter(entry => entry !== null)
.sort((a, b) => {
let ai = 0;
let bi = 0;
let s = 0;
while (ai != a.matches.length && bi != b.matches.length) {
const am = a.matches[ai];
const bm = b.matches[bi];
s = bm.length - am.length;
if (s != 0) return s;
s = am.index - bm.index;
if (s != 0) return s;
++ai;
++bi;
}
s = a.matches.length - b.matches.length;
if (s != 0) return s;
s = a.object.name.length - b.object.name.length;
if (s != 0) return s;
return a.object.name.localeCompare(b.object.name);
})
.map(entry => entry.object);
onValuesChanged: list.currentIndex = 0
}
topMargin: 7
bottomMargin: list.count == 0 ? 0 : 7
add: Transition {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100 }
}
displaced: Transition {
NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; to: 1; duration: 100 }
}
move: Transition {
NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; to: 1; duration: 100 }
}
remove: Transition {
NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; to: 0; duration: 100 }
}
highlight: Rectangle {
radius: 5
color: "#20e0ffff"
border.color: "#30ffffff"
border.width: 1
}
keyNavigationEnabled: true
keyNavigationWraps: true
highlightMoveVelocity: -1
highlightMoveDuration: 100
preferredHighlightBegin: list.topMargin
preferredHighlightEnd: list.height - list.bottomMargin
highlightRangeMode: ListView.ApplyRange
snapMode: ListView.SnapToItem
readonly property real delegateHeight: 44
delegate: MouseArea {
required property DesktopEntry modelData;
implicitHeight: list.delegateHeight
implicitWidth: ListView.view.width
onClicked: {
modelData.execute();
persist.launcherOpen = false;
}
RowLayout {
id: delegateLayout
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 5
}
IconImage {
Layout.alignment: Qt.AlignVCenter
asynchronous: true
implicitSize: 30
source: Quickshell.iconPath(modelData.icon)
}
Text {
text: modelData.name
color: "#f0f0f0"
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
}
}
function init() {}
}

View file

@ -3,8 +3,10 @@ pragma Singleton
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.Pam
import ".." import ".."
import "../.." import "../.."
@ -61,18 +63,15 @@ Singleton {
root.oldWorkspaces = ({}); root.oldWorkspaces = ({});
} }
Shortcut { IpcHandler {
name: "lock" target: "lockscreen"
onPressed: { function lock(): void { root.locked = true; }
if (root.locked) root.locked = false;
else root.locked = true;
}
} }
LazyLoader { LazyLoader {
id: lockContextLoader id: lockContextLoader
LockContext { SessionLockContext {
onUnlocked: root.locked = false; onUnlocked: root.locked = false;
} }
} }
@ -82,7 +81,7 @@ Singleton {
onSecureChanged: { onSecureChanged: {
if (secure) { if (secure) {
Qt.callLater(() => root.workspaceLockAnimation()); root.workspaceLockAnimation();
} }
} }
@ -108,7 +107,7 @@ Singleton {
LockContent { LockContent {
id: lockContent id: lockContent
context: lockContextLoader.item; state: lockContextLoader.item.state;
visible: false visible: false
width: lockSurface.width width: lockSurface.width
@ -128,7 +127,6 @@ Singleton {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
lockContent.y = -lockSurface.height lockContent.y = -lockSurface.height
console.log(`y ${lockContent.y}`)
lockContent.visible = true; lockContent.visible = true;
lockAnim.running = true; lockAnim.running = true;
} }

View file

@ -0,0 +1,37 @@
import QtQuick
import Quickshell
import Quickshell.Services.Greetd
Scope {
id: root
signal launch();
property LockState state: LockState {
onTryPasswordUnlock: {
this.isUnlocking = true;
Greetd.createSession("admin");
}
}
Connections {
target: Greetd
function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) {
if (responseRequired) {
Greetd.respond(root.state.currentText);
} // else ignore - only supporting passwords
}
function onAuthFailure() {
root.state.currentText = "";
root.state.error = "Invalid password";
root.state.failed = true;
root.state.isUnlocking = false;
}
function onReadyToLaunch() {
root.state.isUnlocking = false;
root.launch();
}
}
}

View file

@ -44,6 +44,8 @@ Item {
anchors.margins: 15 anchors.margins: 15
source: root.icon source: root.icon
sourceSize.width: width
sourceSize.height: height
} }
} }
} }

View file

@ -1,113 +1,202 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Quickshell
import ".." import ".."
Item { Item {
id: root id: root
required property LockContext context; required property LockState state;
property real focusAnim: focusAnimInternal * 0.001 property real focusAnim: focusAnimInternal * 0.001
property int focusAnimInternal: Window.active ? 1000 : 0 property int focusAnimInternal: Window.active ? 1000 : 0
Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } } Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } }
Rectangle { MouseArea {
anchors.horizontalCenter: parent.horizontalCenter anchors.fill: parent
y: parent.height / 2 + textBox.height hoverEnabled: true
id: sep
implicitHeight: 6 property real startMoveX: 0
implicitWidth: 800 property real startMoveY: 0
radius: height / 2
color: ShellGlobals.colors.widget
}
ColumnLayout { // prevents wakeups from bumping the mouse
implicitWidth: sep.implicitWidth onPositionChanged: event => {
anchors.horizontalCenter: parent.horizontalCenter if (root.state.fadedOut) {
anchors.bottom: sep.top if (root.state.mouseMoved()) {
spacing: 0 const xOffset = Math.abs(event.x - startMoveX);
const yOffset = Math.abs(event.y - startMoveY);
Text { const distanceSq = (xOffset * xOffset) + (yOffset * yOffset);
id: timeText if (distanceSq > (100 * 100)) root.state.fadeIn();
Layout.alignment: Qt.AlignHCenter } else {
startMoveX = event.x;
font { startMoveY = event.y;
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 { Item {
Layout.alignment: Qt.AlignHCenter id: content
implicitHeight: childrenRect.height * focusAnim width: parent.width
implicitWidth: sep.implicitWidth height: parent.height
clip: true y: root.state.fadeOutMul * (height / 2 + childrenRect.height)
TextInput { Rectangle {
id: textBox anchors.horizontalCenter: parent.horizontalCenter
focus: true y: parent.height / 2 + textBox.height
width: parent.width id: sep
color: enabled ? implicitHeight: 6
root.context.failed ? "#ffa0a0" : "white" implicitWidth: 800
: "#80ffffff"; radius: height / 2
color: ShellGlobals.colors.widget
}
font.pointSize: 24 ColumnLayout {
horizontalAlignment: TextInput.AlignHCenter implicitWidth: sep.implicitWidth
echoMode: TextInput.Password anchors.horizontalCenter: parent.horizontalCenter
inputMethodHints: Qt.ImhSensitiveData anchors.bottom: sep.top
spacing: 0
onTextChanged: root.context.currentText = text; SystemClock {
id: clock
precision: SystemClock.Minutes
}
Window.onActiveChanged: { Text {
if (Window.active) { id: timeText
text = root.context.currentText; Layout.alignment: Qt.AlignHCenter
font {
pointSize: 120
hintingPreference: Font.PreferFullHinting
family: "Noto Sans"
}
color: "white"
renderType: Text.NativeRendering
text: {
const hours = clock.hours.toString().padStart(2, '0');
const minutes = clock.minutes.toString().padStart(2, '0');
return `${hours}:${minutes}`;
} }
} }
onAccepted: { Item {
if (text != "") root.context.tryUnlock(); Layout.alignment: Qt.AlignHCenter
} implicitHeight: textBox.height * focusAnim
implicitWidth: sep.implicitWidth
clip: true
enabled: !root.context.isUnlocking; TextInput {
id: textBox
focus: true
width: parent.width
color: enabled ?
root.state.failed ? "#ffa0a0" : "white"
: "#80ffffff";
font.pointSize: 24
horizontalAlignment: TextInput.AlignHCenter
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
cursorVisible: text != ""
onCursorVisibleChanged: cursorVisible = text != ""
onTextChanged: {
root.state.currentText = text;
cursorVisible = text != ""
}
Window.onActiveChanged: {
if (Window.active) {
text = root.state.currentText;
}
}
Connections {
target: root.state
function onCurrentTextChanged() {
textBox.text = root.state.currentText;
}
}
onAccepted: {
if (text != "") root.state.tryPasswordUnlock();
}
enabled: !root.state.isUnlocking;
}
Text {
anchors.fill: textBox
font: textBox.font
color: root.state.failed ? "#ffa0a0" : "#80ffffff";
horizontalAlignment: TextInput.AlignHCenter
visible: !textBox.cursorVisible
text: root.state.failed ? root.state.error
: root.state.fprintAvailable ? "Touch sensor or enter password" : "Enter password";
}
Rectangle {
Layout.fillHeight: true
implicitWidth: height
color: "transparent"
visible: root.state.fprintAvailable
anchors {
right: textBox.right
top: textBox.top
bottom: textBox.bottom
}
Image {
anchors.fill: parent
anchors.margins: 5
source: "root:icons/fingerprint.svg"
sourceSize.width: width
sourceSize.height: height
}
}
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: sep.bottom
implicitHeight: (75 + 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.state.fadeOut();
}
LockButton {
icon: "root:icons/pause.svg"
show: root.state.mediaPlaying;
onClicked: root.state.pauseMedia();
}
}
} }
} }
} }
Item { Rectangle {
anchors.horizontalCenter: parent.horizontalCenter id: darkenOverlay
anchors.top: sep.bottom anchors.fill: parent
implicitHeight: (childrenRect.height + 30) * focusAnim color: "black"
implicitWidth: sep.implicitWidth opacity: root.state.fadeOutMul
clip: true visible: opacity != 0
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

@ -1,57 +0,0 @@
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

@ -0,0 +1,70 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Mpris
Scope {
signal tryPasswordUnlock();
property string currentText: "";
property string error: "";
property bool isUnlocking: false;
property bool failed: false;
property bool fprintAvailable: false;
property bool fadedOut: false
property real fadeOutMul: 0
NumberAnimation on fadeOutMul {
id: fadeAnim
duration: 600
easing.type: Easing.BezierSpline
easing.bezierCurve: [0.0, 0.75, 0.15, 1.0, 1.0, 1.0]
onStopped: {
if (fadedOut) Hyprland.dispatch("dpms off");
}
}
onCurrentTextChanged: {
failed = false;
error = "";
if (fadedOut) {
fadeIn();
}
}
function fadeOut() {
if (fadedOut) return;
fadedOut = true;
fadeAnim.to = 1;
fadeAnim.restart();
}
function fadeIn() {
if (!fadedOut) return;
Hyprland.dispatch("dpms on");
fadedOut = false;
fadeAnim.to = 0;
fadeAnim.restart();
}
ElapsedTimer { id: mouseTimer }
// returns if mouse move should be continued, false should restart
function mouseMoved(): bool {
return mouseTimer.restart() < 0.2;
}
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;
}
});
}
}

View file

@ -0,0 +1,44 @@
import Quickshell
import Quickshell.Services.Pam
Scope {
id: root
signal unlocked();
property LockState state: LockState {
onTryPasswordUnlock: {
root.state.isUnlocking = true;
pam.start();
}
}
PamContext {
id: pam
configDirectory: "pam"
config: "password.conf"
onPamMessage: {
if (this.responseRequired) {
this.respond(root.state.currentText);
} else if (this.messageIsError) {
root.state.currentText = "";
root.state.failed = true;
root.state.error = this.message;
} // else ignore
}
onCompleted: status => {
const success = status == PamResult.Success;
if (!success) {
root.state.currentText = "";
root.state.error = "Invalid password";
}
root.state.failed = !success;
root.state.isUnlocking = false;
if (success) root.unlocked();
}
}
}

View file

@ -0,0 +1 @@
auth required /run/current-system/sw/lib/security/pam_fprintd.so

View file

@ -0,0 +1 @@
auth required pam_unix.so

View file

@ -0,0 +1,39 @@
import QtQuick
Canvas {
id: root
property real ringFill: 1.0
onRingFillChanged: requestPaint();
renderStrategy: Canvas.Cooperative
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.lineWidth = 2;
ctx.strokeStyle = "#70ffffff";
ctx.beginPath();
const half = Math.round(root.width / 2);
const start = -Math.PI * 0.5;
const endM = ringFill == 0.0 || ringFill == 1.0 ? ringFill : 1.0 - ringFill
ctx.arc(half, half, half - ctx.lineWidth, start, start + 2 * Math.PI * endM, true);
ctx.stroke();
const xMin = Math.min(root.width * 0.3);
const xMax = Math.max(root.width * 0.7);
ctx.strokeStyle = "white";
ctx.beginPath();
ctx.moveTo(xMin, xMin);
ctx.lineTo(xMax, xMax);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(xMax, xMin);
ctx.lineTo(xMin, xMax);
ctx.stroke();
}
}

View file

@ -0,0 +1,35 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
TrackedNotification {
id: root
required property Notification notif;
renderComponent: StandardNotificationRenderer {
notif: root.notif
backer: root
}
function handleDiscard() {
if (!lock.retained) notif.dismiss();
root.discarded();
}
function handleDismiss() {
//handleDiscard();
}
RetainableLock {
id: lock
object: root.notif
locked: true
onRetainedChanged: {
if (retained) root.discard();
}
}
expireTimeout: notif.expireTimeout
}

View file

@ -0,0 +1,243 @@
import QtQuick
import Quickshell
import "../components"
Item {
id: root
enum FlingState {
Inert,
Returning,
Flinging,
Dismissing
}
implicitWidth: display.implicitWidth
// note: can be 0, use ZHVStack
implicitHeight: (display.implicitHeight + padding * 2) * display.meshFactor
z: 1.0 - display.meshFactor
property var view;
property Item contentItem;
property real padding: 5;
property real edgeXOffset;
property bool canOverlap: display.rotation > 2 || Math.abs(display.displayY) > 10 || display.displayX < -60
property bool canDismiss: display.state != FlickableNotification.Dismissing && display.state != FlickableNotification.Flinging;
property alias displayContainer: displayContainer;
signal leftViewBounds();
signal dismissed();
signal discarded();
signal startedFlick();
function playEntry(delay: real) {
if (display.state != FlickableNotification.Flinging) {
display.displayX = -display.width + edgeXOffset
root.playReturn(delay);
}
}
function playDismiss(delay: real) {
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
display.state = FlickableNotification.Dismissing;
display.animationDelay = delay;
}
}
function playDiscard(delay: real) {
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
display.velocityX = 500;
display.velocityY = 1500;
display.state = FlickableNotification.Flinging;
display.animationDelay = delay;
}
}
function playReturn(delay: real) {
if (display.state != FlickableNotification.Flinging) {
display.state = FlickableNotification.Returning;
display.animationDelay = delay;
}
}
MouseArea {
id: mouseArea
width: display.width
height: display.height
enabled: display.state == FlickableNotification.Inert || display.state == FlickableNotification.Returning
FlickMonitor {
id: flickMonitor
target: mouseArea
onDragDeltaXChanged: {
const delta = dragDeltaX;
display.displayX = delta < 0 ? delta : Math.pow(delta, 0.8);
display.updateMeshFactor(true);
updateDragY();
}
onDragDeltaYChanged: {
updateDragY();
display.state = FlickableNotification.Inert;
}
function updateDragY() {
//const xMul = 1//dragDeltaX < 0 ? 0 : Math.min(1, Math.pow(dragDeltaX / 200, 0.8));
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
const xMul = d
const targetY = dragDeltaY;
display.displayY = root.padding + targetY * xMul;
}
onFlickStarted: {
display.initialAnimComplete = true;
root.startedFlick();
}
onFlickCompleted: {
display.releaseY = dragEndY;
if (velocityX > 1000 || (velocityX > -100 && display.displayX > display.width * 0.4)) {
display.velocityX = Math.max(velocityX * 0.8, 1000);
display.velocityY = velocityY * 0.6;
display.state = FlickableNotification.Flinging;
root.discarded();
} else if (velocityX < -1500 || (velocityX < 100 && display.displayX < -(display.width * 0.4))) {
display.velocityX = Math.min(velocityX * 0.8, -700)
display.velocityY = 0
display.state = FlickableNotification.Dismissing;
root.dismissed();
} else {
display.velocityX = 0;
display.velocityY = 0;
display.state = FlickableNotification.Returning;
}
}
}
Item {
id: displayContainer
layer.enabled: view && view.topNotification == root
opacity: layer.enabled ? 0 : 1 // shader ignores it
width: Math.ceil(display.width + display.xPadding * 2)
height: Math.ceil(display.height + display.yPadding * 2)
x: Math.floor(display.targetContainmentX)
y: Math.floor(display.targetContainmentY)
Item {
id: display
//anchors.centerIn: parent
x: xPadding + (targetContainmentX - displayContainer.x)
y: yPadding + (targetContainmentY - displayContainer.y)
//visible: meshFactor > 0.95
children: [root.contentItem]
implicitWidth: root.contentItem?.width ?? 0
implicitHeight: root.contentItem?.height ?? 0
property var state: FlickableNotification.Inert;
property real meshFactor: 1;
property real velocityX;
property real velocityY;
property real releaseY;
property real animationDelay;
property bool initialAnimComplete;
property real displayX;
property real displayY;
property real tiltSize: Math.max(width, height) * 1.2;
property real xPadding: (tiltSize - width) / 2;
property real yPadding: (tiltSize - height) / 2;
property real targetContainmentX: display.displayX - display.xPadding
property real targetContainmentY: root.padding + display.displayY - display.yPadding
function updateMeshFactor(canRemesh: bool) {
let meshFactor = (display.implicitWidth - Math.abs(display.displayX)) / display.implicitWidth;
meshFactor = 0.8 + (meshFactor * 0.2);
meshFactor = Math.max(0, meshFactor);
if (canRemesh) this.meshFactor = meshFactor;
else this.meshFactor = Math.min(this.meshFactor, meshFactor);
}
function unmesh(delta: real) {
if (meshFactor > 0) {
this.meshFactor = Math.max(0, this.meshFactor - delta * 5);
}
}
rotation: display.displayX < 0 ? 0 : display.displayX * (initialAnimComplete ? 0.1 : 0.02)
property real lastX;
FrameAnimation {
function dampingVelocity(currentVelocity, delta) {
const spring = 1.0;
const damping = 0.1;
const springForce = spring * delta;
const dampingForce = -damping * currentVelocity;
return currentVelocity + (springForce + dampingForce);
}
running: display.state != FlickableNotification.Inert
onTriggered: {
let frameTime = this.frameTime;
if (display.animationDelay != 0) {
const usedDelay = Math.min(display.animationDelay, frameTime);
frameTime -= usedDelay;
display.animationDelay -= usedDelay;
if (frameTime == 0) return;
}
if (display.state == FlickableNotification.Flinging) {
display.velocityY += frameTime * 100000 * (1 / display.velocityX * 100);
//display.velocityX -= display.velocityX * 0.98 * frameTime
display.unmesh(frameTime);
} else if (display.state == FlickableNotification.Dismissing) {
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
display.displayY = root.padding + display.releaseY * d;
display.velocityY = 0;
display.velocityX += frameTime * -20000;
if (display.displayX + display.width > 0) display.updateMeshFactor(false);
else display.unmesh(frameTime);
} else {
const deltaX = 0 - display.displayX;
const deltaY = root.padding - display.displayY;
display.velocityX = dampingVelocity(display.velocityX, deltaX);
display.velocityY = dampingVelocity(display.velocityY, deltaY);
if (Math.abs(display.velocityX) < 0.01 && Math.abs(deltaX) < 1
&& Math.abs(display.velocityY) < 0.01 && Math.abs(deltaY) < 1) {
display.state = FlickableNotification.Inert;
display.displayX = 0;
display.displayY = root.padding;
display.velocityX = 0;
display.velocityY = 0;
display.initialAnimComplete = true;
}
display.updateMeshFactor(true);
}
display.displayX += display.velocityX * frameTime;
display.displayY += display.velocityY * frameTime;
// todo: actually base this on the viewport
if (display.displayX > 10000 || display.displayY > 10000 || (display.displayX + display.width < root.edgeXOffset && display.meshFactor == 0) || display.displayY < -10000) root.leftViewBounds();
}
}
}
}
}
}

View file

@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import "../components"
import "../shaders" as Shaders
Item {
id: root
property list<Item> notifications: [];
property list<Item> heightStack: [];
property alias stack: stack;
property alias topNotification: stack.topNotification;
function addNotificationInert(notification: TrackedNotification): Item {
const harness = stack._harnessComponent.createObject(stack, {
backer: notification,
view: root,
});
harness.contentItem = notification.renderComponent.createObject(harness);
notifications = [...notifications, harness];
heightStack = [harness, ...heightStack];
return harness;
}
function addNotification(notification: TrackedNotification) {
const harness = root.addNotificationInert(notification);
harness.playEntry(0);
}
function dismissAll() {
let delay = 0;
for (const notification of root.notifications) {
if (!notification.canDismiss) continue;
notification.playDismiss(delay);
notification.dismissed();
delay += 0.025;
}
}
function discardAll() {
let delay = 0;
for (const notification of root.notifications) {
if (!notification.canDismiss) continue;
notification.playDismiss(delay);
notification.discarded();
delay += 0.025;
}
}
function addSet(notifications: list<TrackedNotification>) {
let delay = 0;
for (const notification of notifications) {
if (notification.visualizer) {
notification.visualizer.playReturn(delay);
} else {
const harness = root.addNotificationInert(notification);
harness.playEntry(delay);
}
delay += 0.025;
}
}
Item {
anchors.fill: parent
layer.enabled: stack.topNotification != null
layer.effect: Shaders.MaskedOverlay {
overlayItem: stack.topNotification?.displayContainer ?? null
overlayPos: Qt.point(stack.x + stack.topNotification.x + overlayItem.x, stack.y + stack.topNotification.y + overlayItem.y)
}
ZHVStack {
id: stack
property Item topNotification: {
if (root.heightStack.length < 2) return null;
const top = root.heightStack[0] ?? null;
return top && top.canOverlap ? top : null;
};
property Component _harnessComponent: FlickableNotification {
id: notification
required property TrackedNotification backer;
edgeXOffset: -stack.x
onDismissed: backer.handleDismiss();
onDiscarded: backer.handleDiscard();
onLeftViewBounds: {
root.notifications = root.notifications.filter(n => n != this);
root.heightStack = root.heightStack.filter(n => n != this);
this.destroy();
}
onStartedFlick: {
root.heightStack = [this, ...root.heightStack.filter(n => n != this)];
}
Component.onCompleted: backer.visualizer = this;
Connections {
target: backer
function onDismiss() {
notification.playDismiss(0);
notification.dismissed();
}
function onDiscard() {
notification.playDismiss(0);
notification.discarded();
}
}
}
}
}
}

View file

@ -0,0 +1,74 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
Singleton {
id: root
property list<TrackedNotification> notifications;
property Component notifComponent: DaemonNotification {}
property bool showTrayNotifs: false;
property bool dnd: false;
property bool hasNotifs: root.notifications.length != 0
property var lastHoveredNotif;
property var overlay;
signal notif(notif: TrackedNotification);
signal showAll(notifications: list<TrackedNotification>);
signal dismissAll(notifications: list<TrackedNotification>);
signal discardAll(notifications: list<TrackedNotification>);
NotificationServer {
imageSupported: true
actionsSupported: true
actionIconsSupported: true
onNotification: notification => {
notification.tracked = true;
const notif = root.notifComponent.createObject(null, { notif: notification });
root.notifications = [...root.notifications, notif];
root.notif(notif);
}
}
Instantiator {
model: root.notifications
Connections {
required property TrackedNotification modelData;
target: modelData;
function onDiscarded() {
root.notifications = root.notifications.filter(n => n != target);
modelData.untrack();
}
function onDiscard() {
if (!modelData.visualizer) modelData.discarded();
}
}
}
onShowTrayNotifsChanged: {
if (showTrayNotifs) {
for (const notif of root.notifications) {
notif.inTray = true;
}
root.showAll(root.notifications);
} else {
root.dismissAll(root.notifications);
}
}
function sendDiscardAll() {
root.discardAll(root.notifications);
}
}

View file

@ -0,0 +1,39 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
PanelWindow {
WlrLayershell.namespace: "shell:notifications"
exclusionMode: ExclusionMode.Ignore
color: "transparent"
anchors {
left: true
top: true
bottom: true
right: true
}
property Component notifComponent: DaemonNotification {}
NotificationDisplay {
id: display
anchors.fill: parent
stack.y: 5 + 55//(NotificationManager.showTrayNotifs ? 55 : 0)
stack.x: 72
}
visible: display.stack.children.length != 0
mask: Region { item: display.stack }
Component.onCompleted: {
NotificationManager.overlay = this;
NotificationManager.notif.connect(display.addNotification);
NotificationManager.showAll.connect(display.addSet);
NotificationManager.dismissAll.connect(display.dismissAll);
NotificationManager.discardAll.connect(display.discardAll);
}
}

View file

@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Controls
import "root:bar"
BarWidgetInner {
id: root
required property var bar;
property bool controlsOpen: false;
onControlsOpenChanged: NotificationManager.showTrayNotifs = controlsOpen;
Connections {
target: NotificationManager
function onHasNotifsChanged() {
if (!NotificationManager.hasNotifs) {
root.controlsOpen = false;
}
}
}
implicitHeight: width
BarButton {
id: button
anchors.fill: parent
baseMargin: 8
fillWindowWidth: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
showPressed: root.controlsOpen || (pressedButtons & ~Qt.RightButton)
Image {
anchors.fill: parent
source: NotificationManager.hasNotifs
? "root:icons/bell-fill.svg"
: "root:icons/bell.svg"
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
onPressed: event => {
if (event.button == Qt.RightButton && NotificationManager.hasNotifs) {
root.controlsOpen = !root.controlsOpen;
}
}
}
property var tooltip: TooltipItem {
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Label {
anchors.verticalCenter: parent.verticalCenter
text: {
const count = NotificationManager.notifications.length;
return count == 0 ? "No notifications"
: count == 1 ? "1 notification"
: `${count} notifications`;
}
}
}
property var rightclickMenu: TooltipItem {
tooltip: bar.tooltip
owner: root
isMenu: true
grabWindows: [NotificationManager.overlay]
show: root.controlsOpen
onClose: root.controlsOpen = false
Item {
implicitWidth: 440
implicitHeight: root.implicitHeight - 10
MouseArea {
id: closeArea
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
implicitWidth: 30
implicitHeight: 30
hoverEnabled: true
onPressed: {
NotificationManager.sendDiscardAll()
}
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: width * 0.5
antialiasing: true
color: "#60ffffff"
opacity: closeArea.containsMouse ? 1 : 0
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
}
CloseButton {
anchors.fill: parent
ringFill: root.backer.timePercentage
}
}
}
}
}

View file

@ -0,0 +1,203 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Notifications
import ".."
Rectangle {
id: root
required property Notification notif;
required property var backer;
color: notif.urgency == NotificationUrgency.Critical ? "#30ff2030" : "#30c0ffff"
radius: 5
implicitWidth: 450
implicitHeight: c.implicitHeight
HoverHandler {
onHoveredChanged: {
backer.pauseCounter += hovered ? 1 : -1;
}
}
Rectangle {
id: border
anchors.fill: parent
color: "transparent"
border.width: 2
border.color: ShellGlobals.colors.widgetOutline
radius: root.radius
}
ColumnLayout {
id: c
anchors.fill: parent
spacing: 0
ColumnLayout {
Layout.margins: 10
RowLayout {
Image {
visible: source != ""
source: notif.appIcon ? Quickshell.iconPath(notif.appIcon) : ""
fillMode: Image.PreserveAspectFit
antialiasing: true
sourceSize.width: 30
sourceSize.height: 30
Layout.preferredWidth: 30
Layout.preferredHeight: 30
}
Label {
visible: text != ""
text: notif.summary
font.pointSize: 20
elide: Text.ElideRight
Layout.maximumWidth: root.implicitWidth - 100 // QTBUG-127649
}
Item { Layout.fillWidth: true }
MouseArea {
id: closeArea
Layout.preferredWidth: 30
Layout.preferredHeight: 30
hoverEnabled: true
onPressed: root.backer.discard();
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: width * 0.5
antialiasing: true
color: "#60ffffff"
opacity: closeArea.containsMouse ? 1 : 0
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
}
CloseButton {
anchors.fill: parent
ringFill: root.backer.timePercentage
}
}
}
Item {
Layout.topMargin: 3
visible: bodyLabel.text != "" || notifImage.visible
implicitWidth: bodyLabel.width
implicitHeight: Math.max(notifImage.size, bodyLabel.implicitHeight)
Image {
id: notifImage
readonly property int size: visible ? 14 * 8 : 0
y: bodyLabel.y + bodyLabel.topPadding
visible: source != ""
source: notif.image
fillMode: Image.PreserveAspectFit
cache: false
antialiasing: true
width: size
height: size
sourceSize.width: size
sourceSize.height: size
}
Label {
id: bodyLabel
width: root.implicitWidth - 20
text: notif.body
wrapMode: Text.Wrap
onLineLaidOut: line => {
if (!notifImage.visible) return;
const isize = notifImage.size + 6;
if (line.y + line.height <= notifImage.y + isize) {
line.x += isize;
line.width -= isize;
}
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: root.border.width
spacing: 0
visible: notif.actions.length != 0
Rectangle {
height: border.border.width
Layout.fillWidth: true
color: border.border.color
antialiasing: true
}
RowLayout {
spacing: 0
Repeater {
model: notif.actions
Item {
required property NotificationAction modelData;
required property int index;
Layout.fillWidth: true
implicitHeight: 35
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: -implicitWidth * 0.5
}
visible: index != 0
implicitWidth: root.border.width
color: ShellGlobals.colors.widgetOutline
antialiasing: true
}
MouseArea {
id: actionArea
anchors.fill: parent
onClicked: {
modelData.invoke();
}
Rectangle {
anchors.fill: parent
color: actionArea.pressed && actionArea.containsMouse ? "#20000000" : "transparent"
}
RowLayout {
anchors.centerIn: parent
Image {
visible: notif.hasActionIcons
source: Quickshell.iconPath(modelData.identifier)
fillMode: Image.PreserveAspectFit
antialiasing: true
sourceSize.height: 25
sourceSize.width: 25
}
Label { text: modelData.text }
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,5 @@
import QtQuick
TrackedNotification {
renderComponent: StandardNotificationRenderer {}
}

View file

@ -0,0 +1,65 @@
import QtQuick
import Quickshell
Scope {
id: root
required property Component renderComponent;
property bool inTray: false;
property bool destroyOnInvisible: false;
property int visualizerCount: 0;
property FlickableNotification visualizer;
signal dismiss();
signal discard();
signal discarded();
function handleDismiss() {}
function handleDiscard() {}
onVisualizerChanged: {
if (!visualizer) {
expireAnim.stop();
timePercentage = 1;
}
if (!visualizer && destroyOnInvisible) this.destroy();
}
function untrack() {
destroyOnInvisible = true;
if (!visualizer) this.destroy();
}
property int expireTimeout: -1
property real timePercentage: 1
property int pauseCounter: 0
readonly property bool shouldPause: root.pauseCounter != 0 || (NotificationManager.lastHoveredNotif?.pauseCounter ?? 0) != 0
onPauseCounterChanged: {
if (pauseCounter > 0) {
NotificationManager.lastHoveredNotif = this;
}
}
NumberAnimation on timePercentage {
id: expireAnim
running: expireTimeout != 0
paused: running && root.shouldPause && to == 0
duration: expireTimeout == -1 ? 10000 : expireTimeout
to: 0
onFinished: {
if (!inTray) root.dismiss();
}
}
onInTrayChanged: {
if (inTray) {
expireAnim.stop();
expireAnim.duration = 300 * (1 - timePercentage);
expireAnim.to = 1;
expireAnim.start();
}
}
}

View file

@ -0,0 +1,95 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../components"
ShellRoot {
Component {
id: demoNotif
FlickableNotification {
contentItem: Rectangle {
color: "white"
border.color: "blue"
border.width: 2
radius: 10
width: 400
height: 150
}
onLeftViewBounds: this.destroy()
}
}
property Component testComponent: TrackedNotification {
id: notification
renderComponent: Rectangle {
color: "white"
border.color: "blue"
border.width: 2
radius: 10
width: 400
height: 150
ColumnLayout {
Button {
text: "dismiss"
onClicked: notification.dismiss();
}
Button {
text: "discard"
onClicked: notification.discard();
}
}
}
function handleDismiss() {
console.log(`dismiss (sub)`)
}
function handleDiscard() {
console.log(`discard (sub)`)
}
Component.onDestruction: console.log(`destroy (sub)`)
};
property Component realComponent: DaemonNotification {
id: dn
}
Daemon {
onNotification: notification => {
notification.tracked = true;
const o = realComponent.createObject(null, { notif: notification });
display.addNotification(o);
}
}
FloatingWindow {
color: "transparent"
ColumnLayout {
x: 5
Button {
visible: false
text: "add notif"
onClicked: {
//const notif = demoNotif.createObject(stack);
//stack.children = [...stack.children, notif];
const notif = testComponent.createObject(null);
display.addNotification(notif);
}
}
//ZHVStack { id: stack }
NotificationDisplay { id: display }
}
}
}

View file

@ -1,4 +1,5 @@
// very bad code DO NOT COPY // very bad code DO NOT COPY
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
@ -25,7 +26,7 @@ Scope {
Process { Process {
id: grimProc id: grimProc
command: ["grim", "-l", "0", path] command: ["grim", "-l", "0", root.path]
onExited: code => { onExited: code => {
if (code == 0) { if (code == 0) {
root.visible = true root.visible = true
@ -40,12 +41,11 @@ Scope {
id: magickProc id: magickProc
command: [ command: [
"magick", "magick",
path, root.path,
"-crop", "-crop", `${selection.w}x${selection.h}+${selection.x}+${selection.y}`,
`${selection.w}x${selection.h}+${selection.x}+${selection.y}`, "-quality", "70",
"-quality", "-page", "0x0+0+0", // removes page size and shot position
"70", root.path,
path,
] ]
onExited: wlCopy.running = true; onExited: wlCopy.running = true;
@ -53,14 +53,14 @@ Scope {
Process { Process {
id: wlCopy id: wlCopy
command: ["sh", "-c", `wl-copy < '${path}'`] command: ["sh", "-c", `wl-copy < '${root.path}'`]
onExited: shootingComplete = true; onExited: root.shootingComplete = true;
} }
Process { Process {
id: cleanupProc id: cleanupProc
command: ["rm", path] command: ["rm", root.path]
} }
QtObject { QtObject {

View file

@ -1,13 +0,0 @@
import Quickshell
import Quickshell.Wayland
PanelWindow {
visible: false
anchors {
left: true
right: true
top: true
bottom: true
}
}

View file

@ -0,0 +1,25 @@
import QtQuick
ShaderEffect {
property Item overlayItem;
property point overlayPos: Qt.point(overlayItem.x, overlayItem.y);
fragmentShader: Qt.resolvedUrl("masked_overlay.frag.qsb")
property point pOverlayPos: Qt.point(
overlayPos.x / width,
overlayPos.y / height
);
property point pOverlaySize: Qt.point(
overlayItem.width / width,
overlayItem.height / height
);
property point pMergeInset: Qt.point(
3 / width,
3 / height
);
property real pMergeCutoff: 0.15
}

View file

@ -0,0 +1,40 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 1) out vec4 fragColor;
layout(binding = 1) uniform sampler2D source;
layout(binding = 2) uniform sampler2D overlayItem;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
vec2 pOverlayPos;
vec2 pOverlaySize;
vec2 pMergeInset;
float pMergeCutoff;
};
void main() {
vec2 overlayCoord = (qt_TexCoord0 - pOverlayPos) / pOverlaySize;
if (overlayCoord.x >= 0 && overlayCoord.y >= 0 && overlayCoord.x < 1 && overlayCoord.y < 1) {
fragColor = texture(overlayItem, overlayCoord);
if (fragColor.a != 0) {
vec4 baseColor = texture(source, qt_TexCoord0);
// imperfect but visually good enough for now. if more is needed we'll probably need a mask tex
if (baseColor.a != 0
&& fragColor.a < pMergeCutoff
&& (texture(overlayItem, overlayCoord + vec2(0, pMergeInset.y)).a == 0
|| texture(overlayItem, overlayCoord + vec2(pMergeInset.x, 0)).a == 0
|| texture(overlayItem, overlayCoord + vec2(0, -pMergeInset.y)).a == 0
|| texture(overlayItem, overlayCoord + vec2(-pMergeInset.x, 0)).a == 0)) {
fragColor += baseColor * (1 - fragColor.a);
}
fragColor *= qt_Opacity;
return;
}
}
fragColor = texture(source, qt_TexCoord0) * qt_Opacity;
}

Some files were not shown because too many files have changed in this diff Show more