better quickshell

This commit is contained in:
2026-03-06 14:12:18 +01:00
parent d4f83bc941
commit ca514ac2fa
158 changed files with 14613 additions and 7286 deletions
+3 -7
View File
@@ -15,8 +15,6 @@
<summary>Niri & Quickshell</summary>
https://github.com/user-attachments/assets/7e2db305-58bc-4b3d-9c65-7dc0461aead7
<figure>
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop-alt.jpg?raw=true"/>
</figure>
@@ -48,11 +46,11 @@ https://github.com/user-attachments/assets/7e2db305-58bc-4b3d-9c65-7dc0461aead7
- Shell: **Fish**
- Prompt: **Oh My Posh**
- Terminal: **Kitty** & (**WezTerm** | Ghostty)
- Power Menu: **Wlogout**
- Power Menu: **Wlogout** & Quickshell
- Colorscheme: **Catppuccin Mocha**
- App Launcher: **Rofi** | Fuzzel
- Desktop Widgets: Eww | **Quickshell**
- Wallpaper Daemon: **Awww** (previously Swww)
- Wallpaper Daemon: Awww | **Quickshell**
- Notification Daemon: Mako | **Quickshell**
(**bold**: currently preferred)
@@ -67,7 +65,7 @@ Ported from Hyprland, and shares some of the desktop components such as hyprlock
## Quickshell
Not based on, but heavily depends on many modules from [noctalia-shell](https://github.com/noctalia-dev/noctalia-shell). A thousand thanks to their great work.
Not based on, but heavily depends on many modules from (an old version of) [noctalia-shell](https://github.com/noctalia-dev/noctalia-shell). A thousand thanks to their great work.
This setup is currently only adapted for Niri.
@@ -80,8 +78,6 @@ This setup is currently only adapted for Niri.
## Wallpaper & Colortheme
- [WallReel](https://github.com/Uyanide/WallReel): an Image Carousel implemented with QtQuick to browse and set wallpapers from.
- [wallpaper-daemon](./config/scripts/.local/scripts/wallpaper-daemon): automatic blur (only works in niri).
- [change-wallpaper](./config/scripts/.local/scripts/change-wallpaper): script that changes wallpaper with a few extra features.
- [change-colortheme](./config/scripts/.local/scripts/change-colortheme): script that extract colors from the current wallpaper and generate a catppuccin color scheme accordingly.
- [backgrounds](https://github.com/Uyanide/backgrounds) collection for personal use (mostly waifus).
+14 -14
View File
@@ -21,11 +21,12 @@ binds {
Mod+Return { spawn "kitty"; }
Mod+Shift+W { spawn "wallreel"; }
Mod+O { spawn-sh "pkill -x -n pwvucontrol || pwvucontrol"; }
Ctrl+Alt+Delete { spawn-sh "pkill -x wlogout || wlogout"; }
// Quickshell
Mod+Space { spawn "qs" "ipc" "call" "panels" "toggleControlCenter"; }
Mod+Shift+D { spawn "qs" "ipc" "call" "panels" "toggleCalendar"; }
Mod+Shift+L { spawn "qs" "ipc" "call" "lyrics" "toggleBarLyrics"; }
Mod+Space { spawn "qs" "ipc" "call" "bars" "toggleLeft"; }
Mod+N { spawn "qs" "ipc" "call" "bars" "toggleRight"; }
Mod+Shift+L { spawn "qs" "ipc" "call" "bars" "toggleLyrics"; }
Mod+Shift+K { spawn-sh "quickshell-kill || quickshell"; }
Mod+I { spawn "qs" "ipc" "call" "idleInhibitor" "toggleInhibitor"; }
Mod+Alt+R { spawn "qs" "ipc" "call" "recording" "startOrStopRecording"; }
@@ -38,7 +39,6 @@ binds {
// Actions
Mod+V { spawn-sh "fzfclip-wrap"; }
Mod+Period { spawn-sh "pkill -x rofi || rofi-emoji"; }
Ctrl+Alt+Delete { spawn-sh "pkill -x wlogout || wlogout -p layer-shell"; }
Print { spawn "niri" "msg" "action" "screenshot-screen"; }
Mod+Shift+S { spawn "niri" "msg" "action" "screenshot"; }
Mod+Ctrl+Shift+S { spawn "niri" "msg" "action" "screenshot-window"; }
@@ -48,18 +48,18 @@ binds {
Mod+L { spawn "loginctl" "lock-session"; }
// Media
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"; }
XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%-"; }
XF86AudioMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; }
XF86AudioPlay allow-when-locked=true { spawn-sh "playerctl play-pause"; }
XF86AudioPause allow-when-locked=true { spawn-sh "playerctl play-pause"; }
XF86AudioNext allow-when-locked=true { spawn-sh "playerctl next"; }
XF86AudioPrev allow-when-locked=true { spawn-sh "playerctl previous"; }
XF86AudioRaiseVolume allow-when-locked=true { spawn "qs" "ipc" "call" "media" "volumeUp"; }
XF86AudioLowerVolume allow-when-locked=true { spawn "qs" "ipc" "call" "media" "volumeDown"; }
XF86AudioMute allow-when-locked=true { spawn "qs" "ipc" "call" "media" "toggleOutputMute"; }
XF86AudioMicMute allow-when-locked=true { spawn "qs" "ipc" "call" "media" "toggleInputMute"; }
XF86AudioPlay allow-when-locked=true { spawn "qs" "ipc" "call" "media" "playPause"; }
XF86AudioPause allow-when-locked=true { spawn "qs" "ipc" "call" "media" "playPause"; }
XF86AudioNext allow-when-locked=true { spawn "qs" "ipc" "call" "media" "next"; }
XF86AudioPrev allow-when-locked=true { spawn "qs" "ipc" "call" "media" "previous"; }
// Brightness
XF86MonBrightnessUp allow-when-locked=true { spawn "set-brightness" "+10%"; }
XF86MonBrightnessDown allow-when-locked=true { spawn "set-brightness" "10%-"; }
XF86MonBrightnessUp allow-when-locked=true { spawn "qs" "ipc" "call" "brightness" "brightnessUp"; }
XF86MonBrightnessDown allow-when-locked=true { spawn "qs" "ipc" "call" "brightness" "brightnessDown"; }
// Window management
Mod+Tab repeat=false { toggle-overview; }
+1 -4
View File
@@ -1,9 +1,6 @@
// Switch configs
spawn-at-startup "config-switch" "niri"
// Wallpaper
spawn-at-startup "wallpaper-daemon"
// Not necessary maybe ...
spawn-at-startup "fcitx5"
@@ -23,7 +20,7 @@ spawn-at-startup "wl-paste" "--type" "image" "--watch" "cliphist" "store"
spawn-at-startup "solaar" "-w" "hide"
// Some other heavy apps
spawn-at-startup "sunshine"
// spawn-at-startup "sunshine"
// spawn-at-startup "spotify"
// spawn-at-startup "thunderbird"
+1 -1
View File
@@ -60,7 +60,7 @@ animations {
}
layer-rule {
match namespace="^swww-daemonbackdrop$"
match namespace="backdrop$"
place-within-backdrop true
}
@@ -1,2 +1 @@
file:///home/kolkas/Desktop
file:///home/kolkas/Nextcloud

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,7 @@
ip.json
spotify-lyrics-offset.txt
notifications.json
images
location.json
network.json
shell-state.json
@@ -1,3 +1 @@
# some sensitive files
GeoInfoToken.txt
IpAliases.json
settings.json
@@ -1,10 +0,0 @@
{
"location": "Munich",
"notifications": {
"doNotDisturb": false
},
"primaryColor": "#89b4fa",
"showLyricsBar": false,
"sunsetDefaultEnabled": true,
"wifiEnabled": true
}
@@ -0,0 +1,5 @@
{
"colors": {
"mPrimary": "#89b4fa"
}
}
@@ -0,0 +1,216 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Templates as T
import qs.Constants
ScrollView {
id: root
property color handleColor: Qt.alpha(Colors.mHover, 0.8)
property color handleHoverColor: handleColor
property color handlePressedColor: handleColor
property color trackColor: "transparent"
property real handleWidth: Math.round(6 * Style.uiScaleRatio)
property real handleRadius: Style.radiusM
property int verticalPolicy: ScrollBar.AsNeeded
property int horizontalPolicy: ScrollBar.AsNeeded
property bool preventHorizontalScroll: horizontalPolicy === ScrollBar.AlwaysOff
property int boundsBehavior: Flickable.StopAtBounds
readonly property bool verticalScrollable: (contentItem.contentHeight > contentItem.height) || (verticalPolicy == ScrollBar.AlwaysOn)
readonly property bool horizontalScrollable: (contentItem.contentWidth > contentItem.width) || (horizontalPolicy == ScrollBar.AlwaysOn)
property bool showGradientMasks: true
property color gradientColor: Colors.mSurfaceVariant
property int gradientHeight: 16
property bool reserveScrollbarSpace: true
property real userRightPadding: 0
// Scroll speed multiplier for mouse wheel (1.0 = default, higher = faster)
property real wheelScrollMultiplier: 2.0
rightPadding: userRightPadding + (reserveScrollbarSpace && verticalScrollable ? handleWidth + Style.marginXS : 0)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
// Configure the internal flickable when it becomes available
Component.onCompleted: {
configureFlickable();
createGradients();
}
// Dynamically create gradient overlays to avoid interfering with ScrollView content management
function createGradients() {
if (!showGradientMasks)
return;
Qt.createQmlObject(`
import QtQuick
import qs.Constants
Rectangle {
x: root.leftPadding
y: root.topPadding
width: root.availableWidth
height: root.gradientHeight
z: 1
visible: root.showGradientMasks && root.verticalScrollable
opacity: root.contentItem.contentY <= 1 ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: root.gradientColor }
GradientStop { position: 1.0; color: "transparent" }
}
}
`, root, "topGradient");
Qt.createQmlObject(`
import QtQuick
import qs.Constants
Rectangle {
x: root.leftPadding
y: root.height - root.bottomPadding - height + 1
width: root.availableWidth
height: root.gradientHeight + 1
z: 1
visible: root.showGradientMasks && root.verticalScrollable
opacity: (root.contentItem.contentY + root.contentItem.height >= root.contentItem.contentHeight - 1) ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: root.gradientColor }
}
}
`, root, "bottomGradient");
}
// Reference to the internal Flickable for wheel handling
property Flickable _internalFlickable: null
// Function to configure the underlying Flickable
function configureFlickable() {
// Find the internal Flickable (it's usually the first child)
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.toString().indexOf("Flickable") !== -1) {
// Configure the flickable to prevent horizontal scrolling
child.boundsBehavior = root.boundsBehavior;
root._internalFlickable = child;
if (root.preventHorizontalScroll) {
child.flickableDirection = Flickable.VerticalFlick;
child.contentWidth = Qt.binding(() => child.width);
}
break;
}
}
}
WheelHandler {
enabled: root.wheelScrollMultiplier !== 1.0 && root._internalFlickable !== null
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
if (!root._internalFlickable)
return;
const flickable = root._internalFlickable;
const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2;
const newY = flickable.contentY - (delta * root.wheelScrollMultiplier);
flickable.contentY = Math.max(0, Math.min(newY, flickable.contentHeight - flickable.height));
event.accepted = true;
}
}
// Watch for changes in horizontalPolicy
onHorizontalPolicyChanged: {
preventHorizontalScroll = (horizontalPolicy === ScrollBar.AlwaysOff);
configureFlickable();
}
ScrollBar.vertical: ScrollBar {
parent: root
x: root.mirrored ? 0 : root.width - width
y: root.topPadding
height: root.availableHeight
policy: root.verticalPolicy
interactive: root.verticalScrollable
contentItem: Rectangle {
implicitWidth: root.handleWidth
implicitHeight: 100
radius: root.handleRadius
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.verticalScrollable ? (parent.active ? 1.0 : 0.0) : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: root.handleWidth
implicitHeight: 100
color: root.trackColor
opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.verticalScrollable ? (parent.active ? 0.3 : 0.0) : 0.0
radius: root.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
ScrollBar.horizontal: ScrollBar {
parent: root
x: root.leftPadding
y: root.height - height
width: root.availableWidth
policy: root.horizontalPolicy
interactive: root.horizontalScrollable
contentItem: Rectangle {
implicitWidth: 100
implicitHeight: root.handleWidth
radius: root.handleRadius
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.horizontalScrollable ? (parent.active ? 1.0 : 0.0) : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: 100
implicitHeight: root.handleWidth
color: root.trackColor
opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.horizontalScrollable ? (parent.active ? 0.3 : 0.0) : 0.0
radius: root.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
@@ -0,0 +1,29 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Components
import qs.Constants
// Rounded group container using the variant surface color.
// To be used in side panels and settings panes to group fields or buttons.
Rectangle {
id: root
property bool compact: false
color: compact ? Colors.transparent : Colors.mSurfaceVariant
radius: Style.radiusM
layer.enabled: !compact
layer.effect: MultiEffect {
shadowEnabled: true
blurMax: Style.shadowBlurMax
shadowBlur: Style.shadowBlur
shadowOpacity: Style.shadowOpacity
shadowColor: Colors.mShadow
shadowHorizontalOffset: Style.shadowHorizontalOffset
shadowVerticalOffset: Style.shadowVerticalOffset
}
}
@@ -1,12 +1,11 @@
import QtQuick
import qs.Constants
import qs.Noctalia
Item {
id: root
property bool running: true
property color color: Color.mPrimary
property color color: Colors.mPrimary
property int size: Style.baseWidgetSize
property int strokeWidth: Style.borderL
property int duration: Style.animationSlow * 2
@@ -0,0 +1,133 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Constants
Rectangle {
id: root
// Public properties
property string text: ""
property string icon: ""
property string tooltipText
property color backgroundColor: Colors.mPrimary
property color textColor: Colors.mOnPrimary
property color hoverColor: Colors.mHover
property color textHoverColor: Colors.mOnHover
property real fontSize: Style.fontSizeM
property int fontWeight: Style.fontWeightSemiBold
property real iconSize: Style.fontSizeL
property bool outlined: false
property int horizontalAlignment: Qt.AlignHCenter
property real buttonRadius: Style.radiusS
// Internal properties
property bool hovered: false
readonly property color contentColor: {
if (!root.enabled)
return Colors.mOnSurfaceVariant;
if (root.hovered)
return root.textHoverColor;
if (root.outlined)
return root.backgroundColor;
return root.textColor;
}
// Signals
signal clicked()
signal rightClicked()
signal middleClicked()
signal entered()
signal exited()
// Dimensions
implicitWidth: contentRow.implicitWidth + (fontSize * 2)
implicitHeight: contentRow.implicitHeight + (fontSize)
// Appearance
radius: root.buttonRadius
color: {
if (!root.enabled)
return outlined ? "transparent" : Qt.lighter(Colors.mSurfaceVariant, 1.2);
if (root.hovered)
return hoverColor;
return root.outlined ? "transparent" : root.backgroundColor;
}
border.width: outlined ? Style.borderS : 0
border.color: {
if (!root.enabled)
return Colors.mOutline;
if (root.hovered)
return hoverColor;
return root.outlined ? root.backgroundColor : "transparent";
}
opacity: enabled ? 1 : 0.6
// Content
RowLayout {
id: contentRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: root.horizontalAlignment === Qt.AlignLeft ? parent.left : undefined
anchors.horizontalCenter: root.horizontalAlignment === Qt.AlignHCenter ? parent.horizontalCenter : undefined
anchors.leftMargin: root.horizontalAlignment === Qt.AlignLeft ? Style.marginL : 0
spacing: Style.marginXS
// Icon (optional)
UIcon {
Layout.alignment: Qt.AlignVCenter
visible: root.icon !== ""
iconName: root.icon
iconSize: root.iconSize
color: contentColor
}
// Text
UText {
Layout.alignment: Qt.AlignVCenter
visible: root.text !== ""
text: root.text
pointSize: root.fontSize
font.weight: root.fontWeight
color: contentColor
}
}
// Mouse interaction
MouseArea {
id: mouseArea
anchors.fill: parent
enabled: root.enabled
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onEntered: {
root.hovered = true;
root.entered();
}
onExited: {
root.hovered = false;
root.exited();
}
onPressed: (mouse) => {
if (mouse.button === Qt.LeftButton)
root.clicked();
else if (mouse.button == Qt.RightButton)
root.rightClicked();
else if (mouse.button == Qt.MiddleButton)
root.middleClicked();
}
onCanceled: {
root.hovered = false;
}
}
}
@@ -0,0 +1,427 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Utils
Item {
id: root
property var now: Time.now
// Style: "analog" or "digital"
property string clockStyle: "analog"
// Show seconds progress ring (digital only)
property bool showProgress: true
// Colors properties
property color backgroundColor: Colors.mPrimary
property color clockColor: Colors.mOnPrimary
property color secondHandColor: Colors.mError
property color progressColor: root.secondHandColor
// Font size properties for digital clock
property real hoursFontSize: Style.fontSizeXS
property real minutesFontSize: Style.fontSizeXXS
property int hoursFontWeight: Style.fontWeightBold
property int minutesFontWeight: Style.fontWeightBold
// Scale ratio for canvas line widths (used by desktop widget scaling)
property real scaleRatio: 1
height: Math.round((Style.fontSizeXXXL * 1.9) / 2) * 2
width: root.height
Loader {
id: clockLoader
anchors.fill: parent
sourceComponent: {
if (root.clockStyle === "analog")
return analogClockComponent;
if (root.clockStyle === "binary")
return binaryClockComponent;
return digitalClockComponent;
}
onLoaded: {
item.now = Qt.binding(function() {
return root.now;
});
item.backgroundColor = Qt.binding(function() {
return root.backgroundColor;
});
item.clockColor = Qt.binding(function() {
return root.clockColor;
});
if (item.hasOwnProperty("secondHandColor"))
item.secondHandColor = Qt.binding(function() {
return root.secondHandColor;
});
if (item.hasOwnProperty("progressColor"))
item.progressColor = Qt.binding(function() {
return root.progressColor;
});
if (item.hasOwnProperty("hoursFontSize"))
item.hoursFontSize = Qt.binding(function() {
return root.hoursFontSize;
});
if (item.hasOwnProperty("minutesFontSize"))
item.minutesFontSize = Qt.binding(function() {
return root.minutesFontSize;
});
if ("hoursFontWeight" in item)
item.hoursFontWeight = Qt.binding(function() {
return root.hoursFontWeight;
});
if ("minutesFontWeight" in item)
item.minutesFontWeight = Qt.binding(function() {
return root.minutesFontWeight;
});
if (item.hasOwnProperty("scaleRatio"))
item.scaleRatio = Qt.binding(function() {
return root.scaleRatio;
});
if ("showProgress" in item)
item.showProgress = Qt.binding(function() {
return root.showProgress;
});
}
}
Component {
id: analogClockComponent
UClockAnalog {
}
}
Component {
id: digitalClockComponent
UClockDigital {
}
}
Component {
id: binaryClockComponent
UClockBinary {
}
}
// Analog Clock Component
component UClockAnalog: Item {
property var now
property color backgroundColor: Colors.mPrimary
property color clockColor: Colors.mOnPrimary
property color secondHandColor: Colors.mError
property real scaleRatio: 1
anchors.fill: parent
Canvas {
id: clockCanvas
anchors.fill: parent
onPaint: {
var currentTime = Time.now;
var hours = currentTime.getHours();
var minutes = currentTime.getMinutes();
var seconds = currentTime.getSeconds();
const markAlpha = 0.7;
var ctx = getContext("2d");
ctx.reset();
ctx.translate(width / 2, height / 2);
var radius = Math.min(width, height) / 2;
// Hour marks
ctx.strokeStyle = Qt.alpha(clockColor, markAlpha);
ctx.lineWidth = 2 * scaleRatio;
var scaleFactor = 0.7;
for (var i = 0; i < 12; i++) {
var scaleFactor = 0.8;
if (i % 3 === 0)
scaleFactor = 0.65;
ctx.save();
ctx.rotate(i * Math.PI / 6);
ctx.beginPath();
ctx.moveTo(0, -radius * scaleFactor);
ctx.lineTo(0, -radius);
ctx.stroke();
ctx.restore();
}
// Hour hand
ctx.save();
var hourAngle = (hours % 12 + minutes / 60) * Math.PI / 6;
ctx.rotate(hourAngle);
ctx.strokeStyle = clockColor;
ctx.lineWidth = 3 * scaleRatio;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -radius * 0.6);
ctx.stroke();
ctx.restore();
// Minute hand
ctx.save();
var minuteAngle = (minutes + seconds / 60) * Math.PI / 30;
ctx.rotate(minuteAngle);
ctx.strokeStyle = clockColor;
ctx.lineWidth = 2 * scaleRatio;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -radius * 0.9);
ctx.stroke();
ctx.restore();
// Second hand
ctx.save();
var secondAngle = seconds * Math.PI / 30;
ctx.rotate(secondAngle);
ctx.strokeStyle = secondHandColor;
ctx.lineWidth = 1.6 * scaleRatio;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -radius);
ctx.stroke();
ctx.restore();
// Center dot
ctx.beginPath();
ctx.arc(0, 0, 3 * scaleRatio, 0, 2 * Math.PI);
ctx.fillStyle = clockColor;
ctx.fill();
}
Component.onCompleted: requestPaint()
Connections {
function onNowChanged() {
clockCanvas.requestPaint();
}
target: Time
}
}
}
// Digital Clock Component
component UClockDigital: Item {
property var now
property color backgroundColor: Colors.mPrimary
property color clockColor: Colors.mOnPrimary
property color progressColor: Colors.mError
property real hoursFontSize: Style.fontSizeXS
property real minutesFontSize: Style.fontSizeXXS
property int hoursFontWeight: Style.fontWeightBold
property int minutesFontWeight: Style.fontWeightBold
property real scaleRatio: 1
property bool showProgress: true
anchors.fill: parent
// Digital clock's seconds circular progress
Canvas {
id: secondsProgress
property real progress: now.getSeconds() / 60
anchors.fill: parent
visible: showProgress
onProgressChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - 3 * scaleRatio;
ctx.reset();
// Background circle
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.lineWidth = 2.5 * scaleRatio;
ctx.strokeStyle = Qt.alpha(clockColor, 0.15);
ctx.stroke();
// Progress arc
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
ctx.lineWidth = 2.5 * scaleRatio;
ctx.strokeStyle = progressColor;
ctx.lineCap = "round";
ctx.stroke();
}
Connections {
function onNowChanged() {
const total = now.getSeconds() * 1000 + now.getMilliseconds();
secondsProgress.progress = total / 60000;
}
target: Time
}
}
// Digital clock
ColumnLayout {
anchors.centerIn: parent
spacing: -Style.marginXXS
UText {
text: Qt.formatTime(now, "HH")
pointSize: hoursFontSize
font.weight: hoursFontWeight
color: clockColor
Layout.alignment: Qt.AlignHCenter
}
UText {
text: Qt.formatTime(now, "mm")
pointSize: minutesFontSize
font.weight: minutesFontWeight
color: clockColor
Layout.alignment: Qt.AlignHCenter
}
}
}
// Binary Clock Component
component UClockBinary: Item {
// BCD (Binary Coded Decimal) Format:
// H1 H2 : M1 M2 : S1 S2
// H1 (tens): 0-2 (2 bits)
// H2 (ones): 0-9 (4 bits)
// M1 (tens): 0-5 (3 bits)
// M2 (ones): 0-9 (4 bits)
// S1 (tens): 0-5 (3 bits)
// S2 (ones): 0-9 (4 bits)
property var now
property color backgroundColor
property color clockColor: Colors.mOnPrimary
readonly property int h: now.getHours()
readonly property int m: now.getMinutes()
readonly property int s: now.getSeconds()
anchors.fill: parent
RowLayout {
anchors.centerIn: parent
spacing: parent.width * 0.05
// Hours
RowLayout {
spacing: parent.parent.width * 0.02
BinaryColumn {
value: Math.floor(h / 10)
bits: 2
dotSize: root.width * 0.08
activeColor: clockColor
Layout.alignment: Qt.AlignBottom
}
BinaryColumn {
value: h % 10
bits: 4
dotSize: root.width * 0.08
activeColor: clockColor
Layout.alignment: Qt.AlignBottom
}
}
// Minutes
RowLayout {
spacing: parent.parent.width * 0.02
BinaryColumn {
value: Math.floor(m / 10)
bits: 3
dotSize: root.width * 0.08
activeColor: clockColor
Layout.alignment: Qt.AlignBottom
}
BinaryColumn {
value: m % 10
bits: 4
dotSize: root.width * 0.08
activeColor: clockColor
Layout.alignment: Qt.AlignBottom
}
}
// Seconds
RowLayout {
spacing: parent.parent.width * 0.02
BinaryColumn {
value: Math.floor(s / 10)
bits: 3
dotSize: root.width * 0.08
activeColor: clockColor
Layout.alignment: Qt.AlignBottom
}
BinaryColumn {
value: s % 10
bits: 4
dotSize: root.width * 0.08
activeColor: clockColor
Layout.alignment: Qt.AlignBottom
}
}
}
}
component BinaryColumn: Column {
property int value: 0
property int bits: 4
property real dotSize: 10
property color activeColor: "white"
spacing: dotSize * 0.4
Repeater {
model: bits
Rectangle {
property int bitIndex: (bits - 1) - index
property bool isActive: (value >> bitIndex) & 1
width: dotSize
height: dotSize
radius: dotSize / 2
color: isActive ? activeColor : Qt.alpha(activeColor, 0.2)
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
}
}
}
@@ -2,16 +2,55 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
import qs.Noctalia
import qs.Services
import qs.Utils
/*
* UContextMenu - Popup-based context menu for use inside panels and dialogs
*
* Use this component when you need a context menu inside:
* - Settings panels
* - Dialogs
* - Repeater delegates
* - Any nested component context
*
* For bar widgets and top-level window contexts, use NPopupContextMenu instead,
* which provides better screen boundary handling and compositor integration.
*
* Usage:
* UContextMenu {
* id: contextMenu
* parent: Overlay.overlay
* model: [
* { "label": "Action 1", "action": "action1", "icon": "icon-name" },
* { "label": "Action 2", "action": "action2" }
* ]
* onTriggered: action => { Logger.i("MyModule", "Selected:", action) }
* }
*
* MouseArea {
* onClicked: contextMenu.openAtItem(parent, mouse.x, mouse.y)
* }
*/
Popup {
id: root
property alias model: listView.model
property var model: []
property real itemHeight: 36
property real itemPadding: Style.marginM
property int verticalPolicy: ScrollBar.AsNeeded
property int horizontalPolicy: ScrollBar.AsNeeded
// Filter out hidden items to avoid spacing artifacts from zero-height items
readonly property var filteredModel: {
if (!model || model.length === 0)
return [];
var filtered = [];
for (var i = 0; i < model.length; i++) {
if (model[i].visible !== false)
filtered.push(model[i]);
}
return filtered;
}
signal triggered(string action)
@@ -30,22 +69,24 @@ Popup {
width: 180
padding: Style.marginS
onOpened: PanelService.willOpenPopup(root)
onClosed: PanelService.willClosePopup(root)
background: Rectangle {
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS)
color: Colors.mSurfaceVariant
border.color: Colors.mOutline
border.width: Style.borderS
radius: Style.radiusM
}
contentItem: NListView {
contentItem: UListView {
id: listView
implicitHeight: contentHeight
implicitHeight: Math.max(contentHeight, root.itemHeight)
spacing: Style.marginXXS
interactive: contentHeight > root.height
verticalPolicy: root.verticalPolicy
horizontalPolicy: root.horizontalPolicy
reserveScrollbarSpace: false
model: root.filteredModel
delegate: ItemDelegate {
id: menuItem
@@ -53,9 +94,8 @@ Popup {
// Store reference to the popup
property var popup: root
width: listView.width
height: modelData.visible !== false ? root.itemHeight : 0
visible: modelData.visible !== false
width: listView.availableWidth
height: root.itemHeight
opacity: modelData.enabled !== false ? 1 : 0.5
enabled: modelData.enabled !== false
onClicked: {
@@ -66,27 +106,19 @@ Popup {
}
background: Rectangle {
color: menuItem.hovered && menuItem.enabled ? Color.mTertiary : Color.transparent
color: menuItem.hovered && menuItem.enabled ? Colors.mHover : "transparent"
radius: Style.radiusS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
contentItem: RowLayout {
spacing: Style.marginS
// Optional icon
NIcon {
UIcon {
visible: modelData.icon !== undefined
icon: modelData.icon || ""
pointSize: Style.fontSizeM
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
iconName: modelData.icon || ""
iconSize: Style.fontSizeM
color: menuItem.hovered && menuItem.enabled ? Colors.mOnHover : Colors.mOnSurface
Layout.leftMargin: root.itemPadding
Behavior on color {
@@ -98,10 +130,10 @@ Popup {
}
NText {
UText {
text: modelData.label || modelData.text || ""
pointSize: Style.fontSizeM
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
color: menuItem.hovered && menuItem.enabled ? Colors.mOnHover : Colors.mOnSurface
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0
@@ -12,22 +12,22 @@ Rectangle {
GradientStop {
position: 0
color: Color.transparent
color: Colors.transparent
}
GradientStop {
position: 0.1
color: Color.mOutline
color: Colors.mOutline
}
GradientStop {
position: 0.9
color: Color.mOutline
color: Colors.mOutline
}
GradientStop {
position: 1
color: Color.transparent
color: Colors.transparent
}
}
@@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Effects
import qs.Constants
// Unified shadow system
Item {
id: root
required property var source
property bool autoPaddingEnabled: false
property real shadowHorizontalOffset: Style.shadowHorizontalOffset
property real shadowVerticalOffset: Style.shadowVerticalOffset
property real shadowOpacity: Style.shadowOpacity
property color shadowColor: Colors.mShadow
property real shadowBlur: Style.shadowBlur
layer.enabled: true
layer.effect: MultiEffect {
source: root.source
shadowEnabled: true
blurMax: Style.shadowBlurMax
shadowBlur: root.shadowBlur
shadowOpacity: root.shadowOpacity
shadowColor: root.shadowColor
shadowHorizontalOffset: root.shadowHorizontalOffset
shadowVerticalOffset: root.shadowVerticalOffset
autoPaddingEnabled: root.autoPaddingEnabled
}
}
@@ -0,0 +1,19 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
Text {
id: root
property string iconName: ""
property string textOverride: ""
property string fontFamily: Fonts.icon
property int iconSize: Style.fontSizeL
color: Colors.mPrimary
text: textOverride ? textOverride : Icons.get(iconName) || Icons.get(Icons.defaultIcon)
font.family: fontFamily
font.pointSize: iconSize
font.bold: false
}
@@ -0,0 +1,83 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Constants
Item {
id: root
property alias iconName: icon.iconName
property alias textOverride: icon.textOverride
property alias fontFamily: icon.fontFamily
property int baseSize: Style.fontSizeXXXL
property alias iconSize: icon.iconSize
property color colorFg: Colors.mPrimary
property color colorBg: Colors.transparent
property color colorFgHover: Colors.mOnPrimary
property color colorBgHover: colorFg
readonly property bool hovered: alwaysHover || mouseArea.containsMouse
property real radius: Style.radiusS
property bool disabledHover: false
property bool alwaysHover: false
signal entered()
signal exited()
signal clicked()
signal rightClicked()
signal middleClicked()
implicitWidth: baseSize
implicitHeight: baseSize
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: !disabledHover
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: root.entered()
onExited: root.exited()
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton)
root.rightClicked();
else if (mouse.button === Qt.MiddleButton)
root.middleClicked();
else if (mouse.button === Qt.LeftButton)
root.clicked();
}
}
Rectangle {
anchors.fill: parent
color: root.hovered ? colorBgHover : colorBg
radius: root.radius
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
}
UIcon {
id: icon
color: root.hovered ? colorFgHover : colorFg
anchors.centerIn: parent
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
}
}
@@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Constants
Item {
id: root
property real radius: 0
property string imagePath: ""
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL
property real borderWidth: 0
property color borderColor: "transparent"
property int imageFillMode: Image.PreserveAspectCrop
readonly property bool showFallback: (fallbackIcon !== undefined && fallbackIcon !== "") && (imagePath === undefined || imagePath === "" || imageSource.status === Image.Error)
readonly property int status: imageSource.status
Rectangle {
anchors.fill: parent
radius: root.radius
color: "transparent"
border.width: root.borderWidth
border.color: root.borderColor
Image {
id: imageSource
anchors.fill: parent
anchors.margins: root.borderWidth
visible: false
source: root.imagePath
mipmap: true
smooth: true
asynchronous: true
antialiasing: true
fillMode: root.imageFillMode
}
ShaderEffect {
property variant source: imageSource
property real itemWidth: width
property real itemHeight: height
property real sourceWidth: imageSource.sourceSize.width
property real sourceHeight: imageSource.sourceSize.height
property real cornerRadius: Math.max(0, root.radius - root.borderWidth)
property real imageOpacity: 1
property int fillMode: root.imageFillMode
anchors.fill: parent
anchors.margins: root.borderWidth
visible: !root.showFallback
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
supportsAtlasTextures: false
blending: true
}
UIcon {
anchors.fill: parent
anchors.margins: root.borderWidth
visible: root.showFallback
iconName: root.fallbackIcon
iconSize: root.fallbackIconSize
}
}
}
@@ -0,0 +1,57 @@
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Constants
ColumnLayout {
id: root
property string label: ""
property string description: ""
property string icon: ""
property color labelColor: Colors.mOnSurface
property color descriptionColor: Colors.mOnSurfaceVariant
property color iconColor: Colors.mOnSurface
property bool showIndicator: false
property string indicatorTooltip: ""
opacity: enabled ? 1 : 0.6
spacing: Style.marginXXS
visible: root.label != "" || root.description != ""
Layout.fillWidth: true
RowLayout {
spacing: Style.marginXS
Layout.fillWidth: true
visible: root.label !== ""
UIcon {
visible: root.icon !== ""
iconName: root.icon
iconSize: Style.fontSizeXXL
color: root.iconColor
Layout.rightMargin: Style.marginS
}
UText {
Layout.fillWidth: !root.showIndicator
text: root.label
pointSize: Style.fontSizeL
font.weight: Style.fontWeightSemiBold
color: labelColor
wrapMode: Text.WordWrap
}
}
UText {
visible: root.description !== ""
Layout.fillWidth: true
text: root.description
pointSize: Style.fontSizeS
color: root.descriptionColor
wrapMode: Text.WordWrap
textFormat: Text.StyledText
}
}
@@ -2,19 +2,31 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Templates as T
import qs.Constants
import qs.Noctalia
Item {
id: root
property color handleColor: Qt.alpha(Color.mTertiary, 0.8)
property color handleColor: Qt.alpha(Colors.mHover, 0.8)
property color handleHoverColor: handleColor
property color handlePressedColor: handleColor
property color trackColor: Color.transparent
property color trackColor: "transparent"
property real handleWidth: 6
property real handleRadius: Style.radiusM
property int verticalPolicy: ScrollBar.AsNeeded
property int horizontalPolicy: ScrollBar.AsNeeded
property int horizontalPolicy: ScrollBar.AlwaysOff
readonly property bool verticalScrollBarActive: {
if (listView.ScrollBar.vertical.policy === ScrollBar.AlwaysOff)
return false;
return listView.contentHeight > listView.height;
}
readonly property bool contentOverflows: listView.contentHeight > listView.height
property bool showGradientMasks: true
property color gradientColor: Colors.mSurfaceVariant
property int gradientHeight: 16
property bool reserveScrollbarSpace: true
// Available width for content (excludes scrollbar space when reserveScrollbarSpace is true)
readonly property real availableWidth: width - (reserveScrollbarSpace ? handleWidth + Style.marginXS : 0)
// Forward ListView properties
property alias model: listView.model
property alias delegate: listView.delegate
@@ -53,6 +65,8 @@ Item {
property alias dragging: listView.dragging
property alias horizontalVelocity: listView.horizontalVelocity
property alias verticalVelocity: listView.verticalVelocity
// Scroll speed multiplier for mouse wheel (1.0 = default, higher = faster)
property real wheelScrollMultiplier: 2
// Forward ListView methods
function positionViewAtIndex(index, mode) {
@@ -99,33 +113,110 @@ Item {
return listView.itemAtIndex(index);
}
// Dynamically create gradient overlays
function createGradients() {
if (!showGradientMasks)
return ;
Qt.createQmlObject(`
import QtQuick
import qs.Constants
Rectangle {
x: 0
y: 0
width: root.availableWidth
height: root.gradientHeight
z: 1
visible: root.showGradientMasks && root.contentOverflows
opacity: {
if (listView.contentY <= 1) return 0;
if (listView.currentItem && listView.currentItem.y - listView.contentY < root.gradientHeight) return 0;
return 1;
}
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: root.gradientColor }
GradientStop { position: 1.0; color: "transparent" }
}
}
`, root, "topGradient");
Qt.createQmlObject(`
import QtQuick
import qs.Constants
Rectangle {
x: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: -1
width: root.availableWidth
height: root.gradientHeight + 1
z: 1
visible: root.showGradientMasks && root.contentOverflows
opacity: {
if (listView.contentY + listView.height >= listView.contentHeight - 1) return 0;
if (listView.currentItem && listView.currentItem.y + listView.currentItem.height > listView.contentY + listView.height - root.gradientHeight) return 0;
return 1;
}
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: root.gradientColor }
}
}
`, root, "bottomGradient");
}
// Set reasonable implicit sizes for Layout usage
implicitWidth: 200
implicitHeight: 200
Component.onCompleted: {
createGradients();
}
ListView {
id: listView
anchors.fill: parent
// Enable clipping to keep content within bounds
anchors.rightMargin: root.reserveScrollbarSpace ? root.handleWidth + Style.marginXS : 0
clip: true
// Enable flickable for smooth scrolling
boundsBehavior: Flickable.StopAtBounds
WheelHandler {
enabled: !root.contentOverflows
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
event.accepted = true;
}
}
WheelHandler {
enabled: root.wheelScrollMultiplier !== 1 && root.contentOverflows
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2;
const newY = listView.contentY - (delta * root.wheelScrollMultiplier);
listView.contentY = Math.max(0, Math.min(newY, listView.contentHeight - listView.height));
event.accepted = true;
}
}
ScrollBar.vertical: ScrollBar {
parent: listView
x: listView.mirrored ? 0 : listView.width - width
parent: root
x: root.mirrored ? 0 : root.width - width
y: 0
height: listView.height
active: listView.ScrollBar.horizontal.active
height: root.height
policy: root.verticalPolicy
visible: policy === ScrollBar.AlwaysOn || root.verticalScrollBarActive
contentItem: Rectangle {
implicitWidth: root.handleWidth
implicitHeight: 100
radius: root.handleRadius
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
opacity: parent.policy === ScrollBar.AlwaysOn ? 1 : root.verticalScrollBarActive ? (parent.active ? 1 : 0) : 0
Behavior on opacity {
NumberAnimation {
@@ -147,58 +238,7 @@ Item {
implicitWidth: root.handleWidth
implicitHeight: 100
color: root.trackColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
radius: root.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
ScrollBar.horizontal: ScrollBar {
id: horizontalScrollBar
parent: listView
x: 0
y: listView.height - height
width: listView.width
active: listView.ScrollBar.vertical.active
policy: root.horizontalPolicy
contentItem: Rectangle {
implicitWidth: 100
implicitHeight: root.handleWidth
radius: root.handleRadius
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: 100
implicitHeight: root.handleWidth
color: root.trackColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.verticalScrollBarActive ? (parent.active ? 0.3 : 0) : 0
radius: root.handleRadius / 2
Behavior on opacity {
@@ -7,12 +7,12 @@ import qs.Constants
Item {
id: root
required property string symbol
property int symbolSize: Fonts.icon
required property string iconName
property int iconSize: Style.fontSizeM
property real maxValue: 100
property real value: 100
property string textValue: "" // override value in textDisplay if set
property color fillColor: Colors.primary
property color fillColor: Colors.mPrimary
property string textSuffix: ""
property bool pointerCursor: true
property bool expandOnValueChange: false
@@ -22,7 +22,7 @@ Item {
property bool _isFirst: true
property bool disableHover: false
property bool critical: false
property color criticalColor: Colors.red
property color criticalColor: Colors.mRed
readonly property real ratio: value / maxValue
property color realColor: critical ? criticalColor : fillColor
@@ -32,8 +32,8 @@ Item {
signal rightClicked()
signal middleClicked()
implicitHeight: parent.height - 5
implicitWidth: parent.height + (_expand ? textDisplay.width : 0)
implicitHeight: Math.max(iconSize, textLabel.implicitHeight) + 12
implicitWidth: height + textDisplay.implicitWidth
Loader {
id: connectionLoader
@@ -95,15 +95,14 @@ Item {
}
RowLayout {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.fill: parent
spacing: 0
Item {
id: progressDisplay
Layout.preferredHeight: parent.height
Layout.preferredWidth: parent.height
Layout.preferredHeight: root.height
Layout.preferredWidth: root.height
Canvas {
id: progressCircle
@@ -140,16 +139,15 @@ Item {
}
Text {
id: symbolText
UIcon {
id: symbolIcon
anchors.fill: parent
text: symbol
font.family: Fonts.nerd
font.pointSize: symbolSize
color: root.realColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconName: root.iconName
iconSize: root.iconSize
color: root.realColor
}
}
@@ -161,17 +159,15 @@ Item {
implicitWidth: root._expand ? textLabel.implicitWidth + 10 : 0
clip: true
Text {
UText {
id: textLabel
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
text: (textValue || Math.round(root.value)) + root.textSuffix
font.pointSize: Fonts.small
font.family: Fonts.primary
font.pointSize: Style.fontSizeS
color: root.realColor
opacity: root._expand ? 1 : 0
}
Behavior on implicitWidth {
@@ -6,10 +6,10 @@ import qs.Constants
T.ScrollView {
id: root
property color handleColor: Qt.alpha(Color.mTertiary, 0.8)
property color handleColor: Qt.alpha(Colors.mPrimary, 0.8)
property color handleHoverColor: handleColor
property color handlePressedColor: handleColor
property color trackColor: Color.transparent
property color trackColor: Colors.transparent
property real handleWidth: 6
property real handleRadius: Style.radiusM
property int verticalPolicy: ScrollBar.AsNeeded
@@ -0,0 +1,275 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Shapes
import qs.Constants
Slider {
id: root
property color fillColor: Colors.mPrimary
property var cutoutColor: Colors.mSurface
property bool snapAlways: true
property real heightRatio: 0.7
property string tooltipText
property string tooltipDirection: "auto"
property bool hovering: false
readonly property color effectiveFillColor: enabled ? fillColor : Colors.mOutline
readonly property real knobDiameter: Math.round((Style.baseWidgetSize * heightRatio) / 2) * 2
readonly property real trackHeight: Math.round((knobDiameter * 0.4) / 2) * 2
readonly property real trackRadius: Math.min(Style.radiusL, trackHeight / 2)
readonly property real cutoutExtra: Math.round((Style.baseWidgetSize * 0.1) / 2) * 2
padding: cutoutExtra / 2
snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease
implicitHeight: Math.max(trackHeight, knobDiameter)
background: Item {
id: bgContainer
readonly property real fillWidth: root.visualPosition * width
x: root.leftPadding
y: root.topPadding + Math.round((root.availableHeight - root.trackHeight) / 2)
implicitWidth: Style.sliderWidth
implicitHeight: root.trackHeight
width: root.availableWidth
height: root.trackHeight
// Background track
Shape {
anchors.fill: parent
visible: bgContainer.width > 0 && bgContainer.height > 0
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: bgPath
readonly property real w: bgContainer.width
readonly property real h: bgContainer.height
readonly property real r: root.trackRadius
strokeColor: Qt.alpha(Colors.mOutline, 0.5)
strokeWidth: Style.borderS
fillColor: Qt.alpha(Colors.mSurface, 0.5)
startX: r
startY: 0
PathLine {
x: bgPath.w - bgPath.r
y: 0
}
PathArc {
x: bgPath.w
y: bgPath.r
radiusX: bgPath.r
radiusY: bgPath.r
}
PathLine {
x: bgPath.w
y: bgPath.h - bgPath.r
}
PathArc {
x: bgPath.w - bgPath.r
y: bgPath.h
radiusX: bgPath.r
radiusY: bgPath.r
}
PathLine {
x: bgPath.r
y: bgPath.h
}
PathArc {
x: 0
y: bgPath.h - bgPath.r
radiusX: bgPath.r
radiusY: bgPath.r
}
PathLine {
x: 0
y: bgPath.r
}
PathArc {
x: bgPath.r
y: 0
radiusX: bgPath.r
radiusY: bgPath.r
}
}
}
LinearGradient {
id: fillGradient
x1: 0
y1: 0
x2: root.availableWidth
y2: 0
GradientStop {
position: 0
color: Qt.darker(effectiveFillColor, 1.2)
}
GradientStop {
position: 1
color: effectiveFillColor
}
}
// Active/filled track
Shape {
width: bgContainer.fillWidth
height: bgContainer.height
visible: bgContainer.fillWidth > 0 && bgContainer.height > 0
preferredRendererType: Shape.CurveRenderer
clip: true
ShapePath {
id: fillPath
readonly property real fullWidth: root.availableWidth
readonly property real h: root.trackHeight
readonly property real r: root.trackRadius
strokeColor: "transparent"
fillGradient: fillGradient
startX: r
startY: 0
PathLine {
x: fillPath.fullWidth - fillPath.r
y: 0
}
PathArc {
x: fillPath.fullWidth
y: fillPath.r
radiusX: fillPath.r
radiusY: fillPath.r
}
PathLine {
x: fillPath.fullWidth
y: fillPath.h - fillPath.r
}
PathArc {
x: fillPath.fullWidth - fillPath.r
y: fillPath.h
radiusX: fillPath.r
radiusY: fillPath.r
}
PathLine {
x: fillPath.r
y: fillPath.h
}
PathArc {
x: 0
y: fillPath.h - fillPath.r
radiusX: fillPath.r
radiusY: fillPath.r
}
PathLine {
x: 0
y: fillPath.r
}
PathArc {
x: fillPath.r
y: 0
radiusX: fillPath.r
radiusY: fillPath.r
}
}
}
// Circular cutout
Rectangle {
id: knobCutout
implicitWidth: root.knobDiameter + root.cutoutExtra
implicitHeight: root.knobDiameter + root.cutoutExtra
radius: Math.min(Style.radiusL, width / 2)
color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.mSurface
x: root.visualPosition * (root.availableWidth - root.knobDiameter) - root.cutoutExtra / 2
anchors.verticalCenter: parent.verticalCenter
}
}
handle: Item {
implicitWidth: knobDiameter
implicitHeight: knobDiameter
x: root.leftPadding + root.visualPosition * (root.availableWidth - width)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: knob
implicitWidth: knobDiameter
implicitHeight: knobDiameter
radius: Math.min(Style.radiusL, width / 2)
color: root.pressed ? Colors.mHover : Colors.mSurface
border.color: effectiveFillColor
border.width: Style.borderL
anchors.centerIn: parent
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
enabled: true
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.NoButton // Don't accept any mouse buttons - only hover
propagateComposedEvents: true
onEntered: {
root.hovering = true;
if (root.tooltipText)
TooltipService.show(knob, root.tooltipText, root.tooltipDirection);
}
onExited: {
root.hovering = false;
if (root.tooltipText)
TooltipService.hide();
}
}
// Hide tooltip when slider is pressed (anywhere on the slider)
Connections {
function onPressedChanged() {
if (root.pressed && root.tooltipText)
TooltipService.hide();
}
target: root
}
}
}
@@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
Rectangle {
id: root
objectName: "NTabBar"
// Public properties
property int currentIndex: 0
property real spacing: Style.marginXS
property real margins: 0
property real tabHeight: Style.baseWidgetSize
property bool distributeEvenly: false
default property alias content: tabRow.children
onDistributeEvenlyChanged: _applyDistribution()
Component.onCompleted: _applyDistribution()
function _updateFirstLast() {
// Defensive check for QML initialization timing
if (!tabRow || !tabRow.children) {
return;
}
var kids = tabRow.children;
var len = kids.length;
var firstVisible = -1;
var lastVisible = -1;
for (var i = 0; i < len; i++) {
var child = kids[i];
// Only consider items that have isFirst/isLast (actual tab buttons, not Repeaters)
if (child.visible && "isFirst" in child) {
if (firstVisible === -1)
firstVisible = i;
lastVisible = i;
}
}
for (var i = 0; i < len; i++) {
var child = kids[i];
if ("isFirst" in child)
child.isFirst = (i === firstVisible);
if ("isLast" in child)
child.isLast = (i === lastVisible);
}
}
function _applyDistribution() {
if (!tabRow || !tabRow.children) {
return;
}
if (!distributeEvenly) {
for (var i = 0; i < tabRow.children.length; i++) {
var child = tabRow.children[i];
child.Layout.fillWidth = true;
}
return;
}
for (var i = 0; i < tabRow.children.length; i++) {
var child = tabRow.children[i];
child.Layout.fillWidth = true;
child.Layout.preferredWidth = 1;
}
}
// Styling
implicitWidth: tabRow.implicitWidth + (margins * 2)
implicitHeight: tabHeight + (margins * 2)
color: Colors.mSurfaceVariant
radius: Style.radiusM
RowLayout {
id: tabRow
anchors.fill: parent
anchors.margins: margins
spacing: root.spacing
onChildrenChanged: {
for (var i = 0; i < children.length; i++) {
var child = children[i];
child.visibleChanged.connect(root._updateFirstLast);
}
root._updateFirstLast();
root._applyDistribution();
}
}
}
@@ -0,0 +1,108 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
import qs.Components
Rectangle {
id: root
// Public properties
property string text: ""
property string icon: ""
property bool checked: false
property int tabIndex: 0
property real pointSize: Style.fontSizeM
property bool isFirst: false
property bool isLast: false
// Internal state
property bool isHovered: false
signal clicked
// Sizing
Layout.fillHeight: true
implicitWidth: contentLayout.implicitWidth + Style.marginM * 2
topLeftRadius: isFirst ? Style.radiusM : Style.radiusXXXS
bottomLeftRadius: isFirst ? Style.radiusM : Style.radiusXXXS
topRightRadius: isLast ? Style.radiusM : Style.radiusXXXS
bottomRightRadius: isLast ? Style.radiusM : Style.radiusXXXS
color: root.isHovered ? Colors.mHover : (root.checked ? Colors.mPrimary : Colors.mSurface)
border.color: Colors.mOutline
border.width: Style.borderS
Behavior on color {
enabled: !Colors.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
// Content
RowLayout {
id: contentLayout
anchors.centerIn: parent
width: Math.min(implicitWidth, parent.width - Style.marginS * 2)
spacing: (root.icon !== "" && root.text !== "") ? Style.marginXS : 0
UIcon {
visible: root.icon !== ""
Layout.alignment: Qt.AlignVCenter
iconName: root.icon
iconSize: root.pointSize * 1.2
color: root.isHovered ? Colors.mOnHover : (root.checked ? Colors.mOnPrimary : Colors.mOnSurface)
Behavior on color {
enabled: !Colors.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
UText {
id: tabText
visible: root.text !== ""
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: root.text
pointSize: root.pointSize
font.weight: Style.fontWeightSemiBold
color: root.isHovered ? Colors.mOnHover : (root.checked ? Colors.mOnPrimary : Colors.mOnSurface)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Behavior on color {
enabled: !Colors.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
root.isHovered = true;
}
onExited: {
root.isHovered = false;
}
onClicked: {
root.clicked();
// Update parent NTabBar's currentIndex
if (root.parent && root.parent.parent && root.parent.parent.currentIndex !== undefined) {
root.parent.parent.currentIndex = root.tabIndex;
}
}
}
}
@@ -1,19 +1,17 @@
import QtQuick
import QtQuick.Layouts
import qs.Constants
import qs.Noctalia
Text {
id: root
property string family: Fonts.primary
property real pointSize: Style.fontSizeM
property real fontScale: 1
font.family: root.family
font.weight: Style.fontWeightMedium
font.pointSize: root.pointSize * fontScale
color: Color.mOnSurface
font.pointSize: root.pointSize
color: Colors.mOnSurface
elide: Text.ElideRight
wrapMode: Text.NoWrap
verticalAlignment: Text.AlignVCenter
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Constants
RowLayout {
@@ -8,38 +9,54 @@ RowLayout {
property string label: ""
property string description: ""
property string icon: ""
property bool checked: false
property bool hovering: false
property int baseSize: Math.round(Style.baseWidgetSize * 0.8)
property var defaultValue: undefined
property string settingsPath: ""
readonly property bool isValueChanged: (defaultValue !== undefined) && (checked !== defaultValue)
readonly property string indicatorTooltip: defaultValue !== undefined ? I18n.tr("panels.indicator.default-value", {
"value": typeof defaultValue === "boolean" ? (defaultValue ? "true" : "false") : String(defaultValue)
}) : ""
signal toggled(bool checked)
signal entered()
signal exited()
Layout.fillWidth: true
opacity: enabled ? 1 : 0.6
spacing: Style.marginM
NLabel {
ULabel {
Layout.fillWidth: true
label: root.label
description: root.description
icon: root.icon
iconColor: root.checked ? Colors.mPrimary : Colors.mOnSurface
visible: root.label !== "" || root.description !== ""
showIndicator: root.isValueChanged
indicatorTooltip: root.indicatorTooltip
}
Rectangle {
id: switcher
Layout.alignment: Qt.AlignVCenter
implicitWidth: Math.round(root.baseSize * 0.85) * 2
implicitHeight: Math.round(root.baseSize * 0.5) * 2
radius: height * 0.5
color: root.checked ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS)
radius: Math.min(Style.radiusL, height / 2)
color: root.checked ? Colors.mPrimary : Colors.mSurface
border.color: Colors.mOutline
border.width: Style.borderS
Rectangle {
implicitWidth: Math.round(root.baseSize * 0.4) * 2
implicitHeight: Math.round(root.baseSize * 0.4) * 2
radius: height * 0.5
color: root.checked ? Color.mOnPrimary : Color.mPrimary
border.color: root.checked ? Color.mSurface : Color.mSurface
border.width: Math.max(1, Style.borderM)
radius: Math.min(Style.radiusL, height / 2)
color: root.checked ? Colors.mOnPrimary : Colors.mPrimary
border.color: root.checked ? Colors.mSurface : Colors.mSurface
border.width: Style.borderM
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 0
x: root.checked ? switcher.width - width - 3 : 3
@@ -55,18 +72,28 @@ RowLayout {
}
MouseArea {
enabled: root.enabled
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
if (!enabled)
return ;
hovering = true;
root.entered();
}
onExited: {
if (!enabled)
return ;
hovering = false;
root.exited();
}
onClicked: {
if (!enabled)
return ;
root.toggled(!root.checked);
}
}
@@ -1,26 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Constants
pragma Singleton
Singleton {
id: root
// Compatibility colors for noctalia modules
readonly property color mPrimary: Colors.primary
readonly property color mOnPrimary: Colors.base
readonly property color mSecondary: Colors.primary
readonly property color mOnSecondary: Colors.base
readonly property color mTertiary: Colors.primary
readonly property color mOnTertiary: Colors.base
readonly property color mError: Colors.red
readonly property color mOnError: Colors.base
readonly property color mSurface: Colors.base
readonly property color mOnSurface: Colors.text
readonly property color mSurfaceVariant: Colors.surface
readonly property color mOnSurfaceVariant: Colors.overlay1
readonly property color mOutline: Colors.primary
readonly property color mShadow: Colors.crust
readonly property color transparent: "transparent"
}
@@ -1,40 +1,154 @@
import QtQuick
import Quickshell
import qs.Services
import Quickshell.Io
import qs.Constants
import qs.Utils
pragma Singleton
Singleton {
id: root
readonly property color primary: SettingsService.primaryColor
readonly property color transparent: "transparent"
readonly property color rosewater: "#f5e0dc"
readonly property color flamingo: "#f2cdcd"
readonly property color pink: "#f5c2e7"
readonly property color mauve: "#cba6f7"
readonly property color red: "#f38ba8"
readonly property color maroon: "#eba0ac"
readonly property color peach: "#fab387"
readonly property color yellow: "#f9e2af"
readonly property color green: "#a6e3a1"
readonly property color teal: "#94e2d5"
readonly property color sky: "#89dceb"
readonly property color sapphire: "#74c7ec"
readonly property color blue: "#89b4fa"
readonly property color lavender: "#b4befe"
readonly property color text: "#cdd6f4"
readonly property color subtext1: "#bac2de"
readonly property color subtext0: "#a6adc8"
readonly property color overlay2: "#9399b2"
readonly property color overlay1: "#7f849c"
readonly property color overlay0: "#6c7086"
readonly property color surface2: "#585b70"
readonly property color surface1: "#45475a"
readonly property color surface0: "#313244"
readonly property color surface: "#292a3c"
readonly property color base: "#1e1e2e"
readonly property color mantle: "#181825"
readonly property color crust: "#11111b"
readonly property color distroColor: "#74c7ec"
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"]
// Part of material3 color scheme
property color mPrimary: defaultColors.mPrimary
property color mOnPrimary: defaultColors.mOnPrimary
property color mError: defaultColors.mError
property color mOnError: defaultColors.mOnError
property color mSurface: defaultColors.mSurface
property color mOnSurface: defaultColors.mOnSurface
property color mSurfaceVariant: defaultColors.mSurfaceVariant
property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant
property color mOutline: defaultColors.mOutline
property color mShadow: defaultColors.mShadow
property color mHover: defaultColors.mHover
property color mOnHover: defaultColors.mOnHover
// Supplementary colors
property color mPink: defaultColors.mPink
property color mPurple: defaultColors.mPurple
property color mRed: defaultColors.mRed
property color mOrange: defaultColors.mOrange
property color mYellow: defaultColors.mYellow
property color mGreen: defaultColors.mGreen
property color mCyan: defaultColors.mCyan
property color mSky: defaultColors.mSky
property color mBlue: defaultColors.mBlue
property color mLavender: defaultColors.mLavender
// Special colors
property color distro: "#74c7ec"
property color transparent: "#00000000"
readonly property var cavaList: [mLavender, mBlue, mSky, mCyan, mGreen, mYellow, mOrange, mRed]
function reloadColors(newColors) {
if (typeof newColors === "string") {
try {
newColors = JSON.parse(newColors);
} catch (e) {
Logger.e("Colors", "Failed to parse colors.json, using default colors. Error:", e);
return ;
}
} else if (typeof newColors !== "object") {
Logger.w("Colors", "Invalid colors data, using default colors. Data:", newColors);
return ;
}
mPrimary = newColors.mPrimary || defaultColors.mPrimary;
mOnPrimary = newColors.mOnPrimary || defaultColors.mOnPrimary;
mError = newColors.mError || defaultColors.mError;
mOnError = newColors.mOnError || defaultColors.mOnError;
mSurface = newColors.mSurface || defaultColors.mSurface;
mOnSurface = newColors.mOnSurface || defaultColors.mOnSurface;
mSurfaceVariant = newColors.mSurfaceVariant || defaultColors.mSurfaceVariant;
mOnSurfaceVariant = newColors.mOnSurfaceVariant || defaultColors.mOnSurfaceVariant;
mOutline = newColors.mOutline || defaultColors.mOutline;
mShadow = newColors.mShadow || defaultColors.mShadow;
mHover = newColors.mHover || defaultColors.mHover;
mOnHover = newColors.mOnHover || defaultColors.mOnHover;
mPink = newColors.mPink || defaultColors.mPink;
mPurple = newColors.mPurple || defaultColors.mPurple;
mRed = newColors.mRed || defaultColors.mRed;
mOrange = newColors.mOrange || defaultColors.mOrange;
mYellow = newColors.mYellow || defaultColors.mYellow;
mGreen = newColors.mGreen || defaultColors.mGreen;
mCyan = newColors.mCyan || defaultColors.mCyan;
mSky = newColors.mSky || defaultColors.mSky;
mBlue = newColors.mBlue || defaultColors.mBlue;
mLavender = newColors.mLavender || defaultColors.mLavender;
}
function setColor(name, value) {
if (!adapter.colors)
adapter.colors = {
};
adapter.colors[name] = value;
colorFile.writeAdapter();
}
function unsetColor(name) {
if (!adapter.colors || !(name in adapter.colors))
return ;
delete adapter.colors[name];
colorFile.writeAdapter();
}
QtObject {
id: defaultColors
readonly property color mPrimary: "#89b4fa"
readonly property color mOnPrimary: "#11111b"
readonly property color mError: "#f38ba8"
readonly property color mOnError: "#11111b"
readonly property color mSurface: "#1e1e2e"
readonly property color mOnSurface: "#cdd6f4"
readonly property color mSurfaceVariant: "#313244"
readonly property color mOnSurfaceVariant: "#a6adc8"
readonly property color mOutline: "#585b70"
readonly property color mShadow: "#11111b"
readonly property color mHover: "#45475a"
readonly property color mOnHover: "#cdd6f4"
readonly property color mPink: "#f5c2e7"
readonly property color mPurple: "#cba6f7"
readonly property color mRed: "#f38ba8"
readonly property color mOrange: "#fab387"
readonly property color mYellow: "#f9e2af"
readonly property color mGreen: "#a6e3a1"
readonly property color mCyan: "#94e2d5"
readonly property color mSky: "#74c7ec"
readonly property color mBlue: "#89b4fa"
readonly property color mLavender: "#b4befe"
}
FileView {
id: colorFile
path: Paths.configDir + "colors.json"
printErrors: false
watchChanges: true
onFileChanged: reload()
JsonAdapter {
id: adapter
property var colors: ({
})
}
}
Connections {
function onColorsChanged() {
colorReloadTimer.restart();
}
target: adapter
}
Timer {
id: colorReloadTimer
interval: 50
running: true
repeat: false
onTriggered: reloadColors(adapter.colors)
}
}
@@ -8,9 +8,6 @@ Singleton {
readonly property string primary: "LXGW WenKai"
readonly property string nerd: "Meslo LGM Nerd Font Mono"
readonly property string icon: Icons.fontFamily
readonly property string sans: "LXGW WenKai"
readonly property int small: Style.fontSizeS
readonly property int medium: Style.fontSizeM
readonly property int large: Style.fontSizeL
readonly property int icon: 14 // for nerd font
}
@@ -1,53 +1,19 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Constants
import qs.Utils
pragma Singleton
Singleton {
id: root
// Nerd fonts icons
readonly property string distro: "󰣇"
readonly property string tray: ""
readonly property string idleInhibitorActivated: "󰅶"
readonly property string idleInhibitorDeactivated: "󰾪"
readonly property string powerMenu: "󰐥"
readonly property string volumeHigh: ""
readonly property string volumeMedium: ""
readonly property string volumeLow: ""
readonly property string volumeMuted: "󰝟"
readonly property string brightness: ""
readonly property string charging: ""
readonly property string battery100: ""
readonly property string battery75: ""
readonly property string battery50: ""
readonly property string battery25: ""
readonly property string battery00: ""
readonly property string cpu: "󰘚"
readonly property string memory: "󰍛"
readonly property string tempHigh: ""
readonly property string tempMedium: ""
readonly property string tempLow: ""
readonly property string ip: "󰇧"
readonly property string upload: ""
readonly property string download: ""
readonly property string speedSlower: "󰾆"
readonly property string speedFaster: "󰓅"
readonly property string speedReset: "󰾅"
readonly property string reset: "󰑙"
readonly property string lines: ""
readonly property string record: ""
readonly property string wifiOn: "󰖩"
readonly property string wifiOff: "󰖪"
readonly property string bluetoothOn: ""
readonly property string bluetoothOff: "󰂲"
// Tabler icons
// Expose the font family name for easy access
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
readonly property string defaultIcon: TablerIcons.defaultIcon
readonly property var icons: TablerIcons.icons
readonly property var aliases: TablerIcons.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
readonly property string defaultIcon: IconsTabler.defaultIcon
readonly property var icons: IconsTabler.icons
readonly property var aliases: IconsTabler.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/noctalia-tabler-icons.ttf"
// Current active font loader
property FontLoader currentFontLoader: null
property int fontVersion: 0
@@ -68,6 +34,7 @@ Singleton {
}
function loadFontWithCacheBusting() {
Logger.d("Icons", "Loading font with cache busting");
// Destroy old loader first
if (currentFontLoader) {
currentFontLoader.destroy();
@@ -82,24 +49,29 @@ Singleton {
`, root, "dynamicFontLoader_" + fontVersion);
// Connect to the new loader's status changes
currentFontLoader.statusChanged.connect(function() {
if (currentFontLoader.status === FontLoader.Ready)
if (currentFontLoader.status === FontLoader.Ready) {
Logger.d("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")");
fontReloaded();
else if (currentFontLoader.status === FontLoader.Error)
Logger.error("Icons", "Font failed to load (version " + fontVersion + ")");
} else if (currentFontLoader.status === FontLoader.Error) {
Logger.e("Icons", "Font failed to load (version " + fontVersion + ")");
}
});
}
function reloadFont() {
Logger.d("Icons", "Forcing font reload...");
fontVersion++;
loadFontWithCacheBusting();
}
Component.onCompleted: {
Logger.i("Icons", "Service started");
loadFontWithCacheBusting();
}
Connections {
function onReloadCompleted() {
Logger.d("Icons", "Quickshell reload completed - forcing font reload");
reloadFont();
}
@@ -33,6 +33,7 @@ Singleton {
"media-next": "player-skip-forward-filled",
"download-speed": "download",
"upload-speed": "upload",
"cpu-intensive": "alert-octagon",
"cpu-usage": "brand-speedtest",
"cpu-temperature": "flame",
"gpu-temperature": "device-desktop",
@@ -42,6 +43,7 @@ Singleton {
"powersaver": "leaf",
"storage": "database",
"ethernet": "sitemap",
"ethernet-off": "sitemap-off",
"keyboard": "keyboard",
"shutdown": "power",
"lock": "lock",
@@ -49,6 +51,7 @@ Singleton {
"logout": "logout",
"reboot": "refresh",
"suspend": "player-pause",
"hibernate": "zzz",
"nightlight-on": "moon",
"nightlight-off": "moon-off",
"nightlight-forced": "moon-stars",
@@ -71,15 +74,19 @@ Singleton {
"chevron-down": "chevron-down",
"caret-up": "caret-up-filled",
"caret-down": "caret-down-filled",
"caret-left": "caret-left-filled",
"caret-right": "caret-right-filled",
"star": "star",
"star-off": "star-off",
"battery-exclamation": "battery-exclamation",
"battery-charging": "battery-charging",
"battery-charging-2": "battery-charging-2",
"battery-4": "battery-4",
"battery-3": "battery-3",
"battery-2": "battery-2",
"battery-1": "battery-1",
"battery": "battery",
"battery-off": "battery-off",
"wifi-0": "wifi-0",
"wifi-1": "wifi-1",
"wifi-2": "wifi-2",
@@ -88,10 +95,14 @@ Singleton {
"microphone": "microphone",
"microphone-mute": "microphone-off",
"volume-mute": "volume-off",
"volume-x": "volume-3",
"volume-zero": "volume-3",
"volume-low": "volume-2",
"volume-high": "volume",
"weather-sun": "sun",
"weather-moon": "moon",
"weather-moon-stars": "moon-stars",
"weather-cloud-off": "cloud-off",
"weather-cloud": "cloud",
"weather-cloud-haze": "cloud-fog",
"weather-cloud-lightning": "cloud-bolt",
@@ -101,24 +112,33 @@ Singleton {
"brightness-low": "brightness-down-filled",
"brightness-high": "brightness-up-filled",
"settings-general": "adjustments-horizontal",
"settings-bar": "capsule-horizontal",
"settings-bar": "crop-16-9",
"settings-user-interface": "layout-board",
"settings-control-center": "adjustments-horizontal",
"settings-dock": "layout-bottombar",
"settings-launcher": "rocket",
"settings-audio": "device-speaker",
"settings-display": "device-desktop",
"settings-network": "sitemap",
"settings-network": "circles-relation",
"settings-brightness": "brightness-up",
"settings-location": "world-pin",
"settings-color-scheme": "palette",
"settings-wallpaper": "paint",
"settings-wallpaper-selector": "library-photo",
"settings-screen-recorder": "video",
"settings-hooks": "link",
"settings-notifications": "bell",
"settings-osd": "picture-in-picture",
"settings-about": "info-square-rounded",
"settings-idle": "moon",
"settings-lock-screen": "lock",
"settings-session-menu": "power",
"settings-system-monitor": "activity",
"bluetooth": "bluetooth",
"bt-device-generic": "bluetooth",
"bt-device-gamepad": "device-gamepad-2",
"bt-device-microphone": "microphone",
"bt-device-headset": "headset",
"bt-device-earbuds": "device-airpods",
"bt-device-headphones": "headphones",
"bt-device-mouse": "mouse-2",
"bt-device-keyboard": "bluetooth",
@@ -126,6 +146,12 @@ Singleton {
"bt-device-watch": "device-watch",
"bt-device-speaker": "device-speaker",
"bt-device-tv": "device-tv",
"antenna-bars-1": "antenna-bars-1",
"antenna-bars-2": "antenna-bars-2",
"antenna-bars-3": "antenna-bars-3",
"antenna-bars-4": "antenna-bars-4",
"antenna-bars-5": "antenna-bars-5",
"antenna-bars-off": "antenna-bars-off",
"noctalia": "noctalia",
"hyprland": "hyprland",
"filepicker-folder": "folder",
@@ -152,7 +178,10 @@ Singleton {
"filepicker-text": "file-text",
"filepicker-eye": "eye",
"filepicker-eye-off": "eye-off",
"filepicker-folder-current": "checks"
"filepicker-folder-current": "checks",
"plugin": "plug-connected",
"info": "file-description",
"official-plugin": "shield-filled"
}
// Fonts Codepoints - do not change!
@@ -295,8 +324,8 @@ Singleton {
"align-left": "\u{ea09}",
"align-left-2": "\u{ff00}",
"align-right": "\u{ea0a}",
"alpha"//"align-right-2": "\u{feff}",
: "\u{f543}",
//"align-right-2": "\u{feff}",
"alpha": "\u{f543}",
"alphabet-arabic": "\u{ff2f}",
"alphabet-bangla": "\u{ff2e}",
"alphabet-cyrillic": "\u{f1df}",
@@ -2084,7 +2113,7 @@ Singleton {
"cloud-snow": "\u{ea73}",
"cloud-star": "\u{f85b}",
"cloud-storm": "\u{ea74}",
"cloud-sun": "\u{ea7a}",
"cloud-sun": "\u{ec6d}",
"cloud-up": "\u{f85c}",
"cloud-upload": "\u{ea75}",
"cloud-x": "\u{f85d}",
@@ -3128,8 +3157,8 @@ Singleton {
"friends": "\u{eab0}",
"friends-off": "\u{f136}",
"frustum": "\u{fa9f}",
"frustum-plus"//"frustum-off": "\u{fa9d}",
: "\u{fa9e}",
//"frustum-off": "\u{fa9d}",
"frustum-plus": "\u{fa9e}",
"function": "\u{f225}",
"function-filled": "\u{fc2b}",
"function-off": "\u{f3f0}",
@@ -3388,13 +3417,13 @@ Singleton {
"hexagon-letter-x": "\u{f479}",
"hexagon-letter-x-filled": "\u{fe30}",
"hexagon-letter-y": "\u{f47a}",
"hexagon-letter-z"//"hexagon-letter-y-filled": "\u{fe2f}",
: "\u{f47b}",
"hexagon-minus"//"hexagon-letter-z-filled": "\u{fe2e}",
: "\u{fc8f}",
//"hexagon-letter-y-filled": "\u{fe2f}",
"hexagon-letter-z": "\u{f47b}",
//"hexagon-letter-z-filled": "\u{fe2e}",
"hexagon-minus": "\u{fc8f}",
"hexagon-minus-2": "\u{fc8e}",
"hexagon-number-0"//"hexagon-minus-filled": "\u{fe2d}",
: "\u{f459}",
//"hexagon-minus-filled": "\u{fe2d}",
"hexagon-number-0": "\u{f459}",
"hexagon-number-0-filled": "\u{f74c}",
"hexagon-number-1": "\u{f45a}",
"hexagon-number-1-filled": "\u{f74d}",
@@ -3417,8 +3446,8 @@ Singleton {
"hexagon-off": "\u{ee9c}",
"hexagon-plus": "\u{fc45}",
"hexagon-plus-2": "\u{fc90}",
"hexagonal-prism"//"hexagon-plus-filled": "\u{fe2c}",
: "\u{faa5}",
//"hexagon-plus-filled": "\u{fe2c}",
"hexagonal-prism": "\u{faa5}",
"hexagonal-prism-off": "\u{faa3}",
"hexagonal-prism-plus": "\u{faa4}",
"hexagonal-pyramid": "\u{faa8}",
@@ -3448,8 +3477,8 @@ Singleton {
"home-eco": "\u{f351}",
"home-edit": "\u{f352}",
"home-exclamation": "\u{f33c}",
"home-hand"//"home-filled": "\u{fe2b}",
: "\u{f504}",
//"home-filled": "\u{fe2b}",
"home-hand": "\u{f504}",
"home-heart": "\u{f353}",
"home-infinity": "\u{f505}",
"home-link": "\u{f354}",
@@ -3567,8 +3596,8 @@ Singleton {
"ironing-2-filled": "\u{1006e}",
"ironing-3": "\u{f2f6}",
"ironing-3-filled": "\u{1006d}",
"ironing-off"//"ironing-filled": "\u{fe2a}",
: "\u{f2f7}",
//"ironing-filled": "\u{fe2a}",
"ironing-off": "\u{f2f7}",
"ironing-steam": "\u{f2f9}",
"ironing-steam-filled": "\u{1006c}",
"ironing-steam-off": "\u{f2f8}",
@@ -3578,8 +3607,8 @@ Singleton {
"italic": "\u{eb93}",
"jacket": "\u{f661}",
"jetpack": "\u{f581}",
"jewish-star"//"jetpack-filled": "\u{fe29}",
: "\u{f3ff}",
//"jetpack-filled": "\u{fe29}",
"jewish-star": "\u{f3ff}",
"jewish-star-filled": "\u{f67e}",
"join-bevel": "\u{ff4c}",
"join-round": "\u{ff4b}",
@@ -3593,8 +3622,8 @@ Singleton {
"kering": "\u{efb8}",
"kerning": "\u{efb8}",
"key": "\u{eac7}",
"key-off"//"key-filled": "\u{fe28}",
: "\u{f14b}",
//"key-filled": "\u{fe28}",
"key-off": "\u{f14b}",
"keyboard": "\u{ebd6}",
"keyboard-filled": "\u{100a2}",
"keyboard-hide": "\u{ec7e}",
@@ -3650,20 +3679,20 @@ Singleton {
"layers-union": "\u{eacb}",
"layout": "\u{eadb}",
"layout-2": "\u{eacc}",
"layout-align-left"//"layout-2-filled": "\u{fe27}",
// "layout-align-bottom": "\u{eacd}",
//"layout-2-filled": "\u{fe27}",
//"layout-align-bottom": "\u{eacd}",
//"layout-align-bottom-filled": "\u{fe26}",
// "layout-align-center": "\u{eace}",
//"layout-align-center": "\u{eace}",
//"layout-align-center-filled": "\u{fe25}",
: "\u{eacf}",
"layout-align-middle"// "layout-align-left-filled": "\u{fe24}",
: "\u{ead0}",
"layout-align-right"//"layout-align-middle-filled": "\u{fe23}",
: "\u{ead1}",
"layout-align-top"//"layout-align-right-filled": "\u{fe22}",
: "\u{ead2}",
"layout-board"//"layout-align-top-filled": "\u{fe21}",
: "\u{ef95}",
"layout-align-left": "\u{eacf}",
//"layout-align-left-filled": "\u{fe24}",
"layout-align-middle": "\u{ead0}",
//"layout-align-middle-filled": "\u{fe23}",
"layout-align-right": "\u{ead1}",
//"layout-align-right-filled": "\u{fe22}",
"layout-align-top": "\u{ead2}",
//"layout-align-top-filled": "\u{fe21}",
"layout-board": "\u{ef95}",
"layout-board-filled": "\u{10182}",
"layout-board-split": "\u{ef94}",
"layout-board-split-filled": "\u{10183}",
@@ -3675,8 +3704,8 @@ Singleton {
"layout-bottombar-filled": "\u{fc37}",
"layout-bottombar-inactive": "\u{fd45}",
"layout-cards": "\u{ec13}",
"layout-collage"// "layout-cards-filled": "\u{fe20}",
: "\u{f389}",
//"layout-cards-filled": "\u{fe20}",
"layout-collage": "\u{f389}",
"layout-columns": "\u{ead4}",
"layout-dashboard": "\u{f02c}",
"layout-dashboard-filled": "\u{fe1f}",
@@ -4157,14 +4186,14 @@ Singleton {
"microphone": "\u{eaf0}",
"microphone-2": "\u{ef2c}",
"microphone-2-off": "\u{f40d}",
"microphone-off"//"microphone-filled": "\u{fe0f}",
: "\u{ed16}",
//"microphone-filled": "\u{fe0f}",
"microphone-off": "\u{ed16}",
"microscope": "\u{ef64}",
"microscope-filled": "\u{10166}",
"microscope-off": "\u{f40e}",
"microwave": "\u{f248}",
"microwave-off"//"microwave-filled": "\u{fe0e}",
: "\u{f264}",
//"microwave-filled": "\u{fe0e}",
"microwave-off": "\u{f264}",
"military-award": "\u{f079}",
"military-rank": "\u{efcf}",
"military-rank-filled": "\u{ff5e}",
@@ -4398,18 +4427,18 @@ Singleton {
"number-4-small": "\u{fcf9}",
"number-40-small": "\u{fffa}",
"number-41-small": "\u{fff9}",
"number-5"//"number-42-small": "\u{fff8}",
// "number-43-small": "\u{fff7}",
// "number-44-small": "\u{fff6}",
// "number-45-small": "\u{fff5}",
// "number-46-small": "\u{fff4}",
// "number-47-small": "\u{fff3}",
// "number-48-small": "\u{fff2}",
// "number-49-small": "\u{fff1}",
: "\u{edf5}",
//"number-42-small": "\u{fff8}",
//"number-43-small": "\u{fff7}",
//"number-44-small": "\u{fff6}",
//"number-45-small": "\u{fff5}",
//"number-46-small": "\u{fff4}",
//"number-47-small": "\u{fff3}",
//"number-48-small": "\u{fff2}",
//"number-49-small": "\u{fff1}",
"number-5": "\u{edf5}",
"number-5-small": "\u{fcfa}",
"number-51-small"// "number-50-small": "\u{fff0}",
: "\u{ffef}",
//"number-50-small": "\u{fff0}",
"number-51-small": "\u{ffef}",
"number-52-small": "\u{ffee}",
"number-53-small": "\u{ffed}",
"number-54-small": "\u{ffec}",
@@ -4761,6 +4790,7 @@ Singleton {
"playstation-triangle": "\u{f2af}",
"playstation-x": "\u{f2b0}",
"plug": "\u{ebd9}",
"plug-filled": "\u{f6b3}",
"plug-connected": "\u{f00a}",
"plug-connected-x": "\u{f0a0}",
"plug-off": "\u{f180}",
@@ -4849,11 +4879,11 @@ Singleton {
"quote": "\u{efbe}",
"quote-filled": "\u{1009c}",
"quote-off": "\u{f188}",
"radar"//"quotes": "\u{fb1e}",
: "\u{f017}",
//"quotes": "\u{fb1e}",
"radar": "\u{f017}",
"radar-2": "\u{f016}",
"radar-off"//"radar-filled": "\u{fe0d}",
: "\u{f41f}",
//"radar-filled": "\u{fe0d}",
"radar-off": "\u{f41f}",
"radio": "\u{ef2d}",
"radio-off": "\u{f420}",
"radioactive": "\u{ecc0}",
@@ -4913,12 +4943,12 @@ Singleton {
"regex-off": "\u{f421}",
"registered": "\u{eb14}",
"relation-many-to-many": "\u{ed7f}",
"relation-one-to-many"//"relation-many-to-many-filled": "\u{fe0c}",
: "\u{ed80}",
"relation-one-to-one"//"relation-one-to-many-filled": "\u{fe0b}",
: "\u{ed81}",
"reload"//"relation-one-to-one-filled": "\u{fe0a}",
: "\u{f3ae}",
//"relation-many-to-many-filled": "\u{fe0c}",
"relation-one-to-many": "\u{ed80}",
//"relation-one-to-many-filled": "\u{fe0b}",
"relation-one-to-one": "\u{ed81}",
//"relation-one-to-one-filled": "\u{fe0a}",
"reload": "\u{f3ae}",
"reorder": "\u{fc15}",
"repeat": "\u{eb72}",
"repeat-off": "\u{f18e}",
@@ -5070,8 +5100,8 @@ Singleton {
"search": "\u{eb1c}",
"search-off": "\u{f19c}",
"section": "\u{eed5}",
"section-sign"//"section-filled": "\u{fe09}",
: "\u{f019}",
//"section-filled": "\u{fe09}",
"section-sign": "\u{f019}",
"seeding": "\u{ed51}",
"seeding-filled": "\u{10006}",
"seeding-off": "\u{f19d}",
@@ -5279,8 +5309,8 @@ Singleton {
"sort-z-a": "\u{f550}",
"sos": "\u{f24a}",
"soup": "\u{ef2e}",
"soup-off"//"soup-filled": "\u{fe08}",
: "\u{f42d}",
//"soup-filled": "\u{fe08}",
"soup-off": "\u{f42d}",
"source-code": "\u{f4a2}",
"space": "\u{ec0c}",
"space-off": "\u{f1aa}",
@@ -5373,22 +5403,22 @@ Singleton {
"square-half": "\u{effb}",
"square-key": "\u{f638}",
"square-letter-a": "\u{f47c}",
"square-letter-b"//"square-letter-a-filled": "\u{fe07}",
: "\u{f47d}",
"square-letter-c"//"square-letter-b-filled": "\u{fe06}",
: "\u{f47e}",
"square-letter-d"//"square-letter-c-filled": "\u{fe05}",
: "\u{f47f}",
"square-letter-e"//"square-letter-d-filled": "\u{fe04}",
: "\u{f480}",
"square-letter-f"//"square-letter-e-filled": "\u{fe03}",
: "\u{f481}",
"square-letter-g"//"square-letter-f-filled": "\u{fe02}",
: "\u{f482}",
"square-letter-h"//"square-letter-g-filled": "\u{fe01}",
: "\u{f483}",
"square-letter-i"//"square-letter-h-filled": "\u{fe00}",
: "\u{f484}",
//"square-letter-a-filled": "\u{fe07}",
"square-letter-b": "\u{f47d}",
//"square-letter-b-filled": "\u{fe06}",
"square-letter-c": "\u{f47e}",
//"square-letter-c-filled": "\u{fe05}",
"square-letter-d": "\u{f47f}",
//"square-letter-d-filled": "\u{fe04}",
"square-letter-e": "\u{f480}",
//"square-letter-e-filled": "\u{fe03}",
"square-letter-f": "\u{f481}",
//"square-letter-f-filled": "\u{fe02}",
"square-letter-g": "\u{f482}",
//"square-letter-g-filled": "\u{fe01}",
"square-letter-h": "\u{f483}",
//"square-letter-h-filled": "\u{fe00}",
"square-letter-i": "\u{f484}",
"square-letter-i-filled": "\u{fdff}",
"square-letter-j": "\u{f485}",
"square-letter-j-filled": "\u{fdfe}",
@@ -0,0 +1,11 @@
import QtQuick
import Quickshell
pragma Singleton
Singleton {
id: root
readonly property string cacheDir: Quickshell.shellDir + "/Assets/Cache/"
readonly property string configDir: Quickshell.shellDir + "/Assets/Config/"
readonly property string recordingDir: Quickshell.env("HOME") + "/Videos/recordings/"
}
@@ -4,10 +4,6 @@ import Quickshell.Io
pragma Singleton
Singleton {
/*
Preset sizes for font, radii, ?
*/
id: root
// Font size
@@ -19,6 +15,7 @@ Singleton {
readonly property real fontSizeXL: 16
readonly property real fontSizeXXL: 18
readonly property real fontSizeXXXL: 24
readonly property real fontNerd: 16
// Font weight
readonly property int fontWeightRegular: 400
readonly property int fontWeightMedium: 500
@@ -50,19 +47,22 @@ Singleton {
readonly property real opacityHeavy: 0.75
readonly property real opacityAlmost: 0.95
readonly property real opacityFull: 1
// Shadows
readonly property real shadowOpacity: 0.85
readonly property real shadowBlur: 1
readonly property int shadowBlurMax: 22
readonly property real shadowHorizontalOffset: 2
readonly property real shadowVerticalOffset: 3
// Animation duration (ms)
readonly property int animationFast: 150
readonly property int animationNormal: 300
readonly property int animationSlow: 450
readonly property int animationSlowest: 1000
// Delays
readonly property int tooltipDelay: 300
readonly property int tooltipDelayLong: 1200
readonly property int pillDelay: 500
// Settings widgets base size
readonly property real baseWidgetSize: 33
readonly property real sliderWidth: 200
readonly property int baseWidgetSize: 33
readonly property int sliderWidth: 200
// Bar Dimensions
readonly property real barHeight: 45
readonly property real capsuleHeight: 35
readonly property int barHeight: 45
readonly property int sidebarWidth: 360
readonly property int capsuleHeight: 35
}
@@ -0,0 +1,167 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Constants
import qs.Services
Variants {
model: Quickshell.screens
Item {
property var modelData
PanelWindow {
screen: modelData
WlrLayershell.namespace: "quickshell-background"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors {
top: true
bottom: true
left: true
right: true
}
Rectangle {
anchors.fill: parent
color: Colors.mSurface
Item {
id: bgManager
property string activeSource: BackgroundService.previewPath || (BarService.focusMode ? BackgroundService.cachedBlurredPath : BackgroundService.cachedPath)
property bool showFirst: true
anchors.fill: parent
onActiveSourceChanged: {
showFirst = !showFirst;
if (showFirst)
img1.source = activeSource;
else
img2.source = activeSource;
}
Component.onCompleted: {
if (showFirst)
img1.source = activeSource;
else
img2.source = activeSource;
}
Image {
id: img1
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
opacity: (bgManager.showFirst && status === Image.Ready) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationSlow
}
}
}
Image {
id: img2
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
opacity: (!bgManager.showFirst && status === Image.Ready) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationSlow
}
}
}
}
}
}
PanelWindow {
screen: modelData
WlrLayershell.namespace: "quickshell-backdrop"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors {
top: true
bottom: true
left: true
right: true
}
Rectangle {
anchors.fill: parent
color: Colors.mSurface
Item {
id: backdropManager
property string activeSource: BackgroundService.cachedBlurredPath
property bool showFirst: true
anchors.fill: parent
onActiveSourceChanged: {
showFirst = !showFirst;
if (showFirst)
backImg1.source = activeSource;
else
backImg2.source = activeSource;
}
Component.onCompleted: {
if (showFirst)
backImg1.source = activeSource;
else
backImg2.source = activeSource;
}
Image {
id: backImg1
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
opacity: (backdropManager.showFirst && status === Image.Ready) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationSlow
}
}
}
Image {
id: backImg2
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
opacity: (!backdropManager.showFirst && status === Image.Ready) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationSlow
}
}
}
}
}
}
}
}
@@ -3,12 +3,11 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import Quickshell.Wayland
import qs.Components
import qs.Constants
import qs.Modules.Bar.Components
import qs.Modules.Bar.Misc
import qs.Modules.Misc
import qs.Modules.Bar.Modules
import qs.Services
Variants {
@@ -22,6 +21,7 @@ Variants {
screen: modelData
WlrLayershell.namespace: "quickshell-bar"
WlrLayershell.layer: WlrLayer.Top
color: Colors.transparent
implicitHeight: Style.barHeight
@@ -35,12 +35,11 @@ Variants {
id: barBackground
anchors.fill: parent
color: Niri.noFocus ? null : Colors.base
gradient: Gradient {
GradientStop {
position: 0
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0.8 : 1)
color: Qt.rgba(Colors.mSurface.r, Colors.mSurface.g, Colors.mSurface.b, BarService.focusMode ? 1 : 0.8)
Behavior on color {
ColorAnimation {
@@ -54,7 +53,7 @@ Variants {
GradientStop {
position: 1
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0 : 1)
color: Qt.rgba(Colors.mSurface.r, Colors.mSurface.g, Colors.mSurface.b, BarService.focusMode ? 1 : 0)
Behavior on color {
ColorAnimation {
@@ -81,42 +80,22 @@ Variants {
leftMargin: 5
}
SymbolButton {
symbol: Icons.distro
buttonColor: Colors.distroColor
onClicked: {
PanelService.getPanel("controlCenterPanel")?.toggle(this)
UIconButton {
textOverride: "󰣇"
fontFamily: Fonts.nerd
baseSize: parent.height - Style.marginXXS * 2
iconSize: Style.fontNerd
colorFg: Colors.distro
onClicked: () => {
BarService.toggleLeft();
}
onRightClicked: {
Quickshell.execDetached(["rofi", "-show", "drun"]);
onRightClicked: () => {
BarService.toggleRight();
}
}
SymbolButton {
symbol: SettingsService.wifiEnabled ? Icons.wifiOn : Icons.wifiOff
buttonColor: Colors.rosewater
onClicked: {
PanelService.getPanel("wifiPanel")?.toggle(this)
}
}
SymbolButton {
symbol: BluetoothService.enabled ? Icons.bluetoothOn : Icons.bluetoothOff
buttonColor: Colors.blue
onClicked: {
PanelService.getPanel("bluetoothPanel")?.toggle(this)
}
onRightClicked: {
Quickshell.execDetached(["blueman-manager"]);
}
}
Item {
width: 5
}
Separator {
implicitWidth: Style.marginXL
}
Workspace {
@@ -124,28 +103,17 @@ Variants {
}
Separator {
}
Item {
width: 10
implicitWidth: Style.marginXL
}
CavaBar {
}
Item {
width: 10
}
Separator {
}
Item {
width: 10
implicitWidth: Style.marginXL
}
FocusedWindow {
maxWidth: 400
}
}
@@ -176,21 +144,27 @@ Variants {
rightMargin: 5
}
Loader {
sourceComponent: LyricsService.showLyricsBar ? lyricsComponent : monitorsComponent
Component {
id: monitorsComponent
RowLayout {
id: monitorsLayout
visible: !SettingsService.showLyricsBar
height: parent.height
height: rightLayout.height
spacing: Style.marginM
Component.onCompleted: {
SystemStatService.registerComponent("BarMonitors");
}
NetworkSpeed {
}
Separator {
}
Item {
width: 10
}
RecordIndicator {
}
@@ -215,43 +189,49 @@ Variants {
Volume {
}
}
}
Component {
id: lyricsComponent
LyricsBar {
id: lyricsBar
visible: SettingsService.showLyricsBar
width: 600
}
Item {
width: 5
}
}
Separator {
}
Item {
width: 5
}
RowLayout {
height: rightLayout.height
spacing: Style.marginS
TrayExpander {
screen: modelData
baseSize: rightLayout.height - Style.marginXXS * 2
}
SymbolButton {
symbol: Caffeine.isInhibited ? Icons.idleInhibitorActivated : Icons.idleInhibitorDeactivated
buttonColor: Caffeine.isInhibited ? Colors.peach : Colors.yellow
onClicked: {
UIconButton {
iconName: Caffeine.isInhibited ? "mug-off" : "mug"
colorFg: Caffeine.isInhibited ? Colors.mOrange : Colors.mYellow
baseSize: rightLayout.height - Style.marginXXS * 2
alwaysHover: Caffeine.isInhibited
onClicked: () => {
Caffeine.manualToggle();
}
}
SymbolButton {
symbol: Icons.powerMenu
buttonColor: Colors.red
onClicked: {
Quickshell.execDetached(["wlogout"]);
UIconButton {
iconName: "power"
colorFg: Colors.mRed
baseSize: rightLayout.height - Style.marginXXS * 2
onClicked: () => {
BarService.toggleRight();
}
}
@@ -261,4 +241,6 @@ Variants {
}
}
}
@@ -1,28 +0,0 @@
import QtQuick
import Quickshell.Io
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
symbol: SystemStatService.cpuTemp > 80 ? Icons.tempHigh : SystemStatService.cpuTemp > 50 ? Icons.tempMedium : Icons.tempLow
fillColor: Colors.yellow
critical: SystemStatService.cpuTemp > 80
value: Math.round(SystemStatService.cpuTemp)
maxValue: 100
textSuffix: "°C"
onClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["wezterm", "start", "--", "btop"]);
}
Process {
id: action
running: false
}
}
@@ -1,28 +0,0 @@
import QtQuick
import Quickshell.Io
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
symbol: Icons.cpu
fillColor: Colors.teal
critical: SystemStatService.cpuUsage > 90
value: Math.round(SystemStatService.cpuUsage)
maxValue: 100
textSuffix: "%"
onClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["wezterm", "start", "--", "btop"]);
}
Process {
id: action
running: false
}
}
@@ -6,13 +6,14 @@ import qs.Constants
Item {
id: root
implicitHeight: parent.height
implicitHeight: Style.barHeight - Style.marginL * 2
implicitWidth: Style.marginM
Rectangle {
anchors.centerIn: parent
width: 1.5
height: parent.height * 0.32
color: Colors.text
height: parent.height
color: Colors.mOnSurface
}
}
@@ -5,7 +5,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import qs.Modules.Bar.Misc
import qs.Modules.Bar.Components
import qs.Constants
import qs.Services
import qs.Utils
@@ -107,7 +107,7 @@ Rectangle {
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set")
}
}
}
@@ -150,7 +150,7 @@ Rectangle {
Loader {
id: trayMenu
Component.onCompleted: {
setSource("../Misc/TrayMenu.qml", {
setSource("./TrayMenu.qml", {
"screen": root.screen
})
}
@@ -1,22 +0,0 @@
import QtQuick
import qs.Constants
import qs.Services
Text {
text: TimeService.time + " | " + TimeService.dateString
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: Colors.primary
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton)
PanelService.getPanel("calendarPanel")?.toggle(this)
else if (mouse.button === Qt.RightButton)
PanelService.getPanel("notificationHistoryPanel")?.toggle(this)
}
}
}
@@ -5,7 +5,7 @@ import Quickshell
import Quickshell.Widgets
import qs.Constants
import qs.Utils
import qs.Noctalia
import qs.Components
PopupWindow {
id: root
@@ -86,8 +86,8 @@ PopupWindow {
Rectangle {
anchors.fill: parent
color: Colors.base
border.color: Colors.primary
color: Colors.mSurface
border.color: Colors.mPrimary
border.width: 2
radius: Style.radiusM
}
@@ -126,7 +126,7 @@ PopupWindow {
color: Colors.transparent
property var subMenu: null
NDivider {
UDivider {
anchors.centerIn: parent
width: parent.width - (Style.marginM * 2)
visible: modelData?.isSeparator ?? false
@@ -134,7 +134,7 @@ PopupWindow {
Rectangle {
anchors.fill: parent
color: mouseArea.containsMouse ? Colors.primary : Colors.transparent
color: mouseArea.containsMouse ? Colors.mPrimary : Colors.transparent
radius: Style.radiusS
visible: !(modelData?.isSeparator ?? false)
@@ -144,10 +144,10 @@ PopupWindow {
anchors.rightMargin: Style.marginM
spacing: Style.marginS
NText {
UText {
id: text
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.mOnPrimary : Colors.mOnSurface) : Colors.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
pointSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
@@ -162,12 +162,12 @@ PopupWindow {
visible: (modelData?.icon ?? "") !== ""
}
NIcon {
icon: modelData?.hasChildren ? "menu" : ""
pointSize: Style.fontSizeS
UIcon {
iconName: modelData?.hasChildren ? "menu" : ""
iconSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
color: (mouseArea.containsMouse ? Colors.mOnPrimary : Colors.mOnSurface)
}
}
@@ -1,64 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
Item {
id: root
required property string symbol
property color buttonColor: Colors.distroColor
readonly property alias hovered: mouseArea.containsMouse
property real iconSize: Fonts.icon
property real radius: Style.radiusS
property bool disabledHover: false
signal clicked()
signal rightClicked()
implicitHeight: parent.height
implicitWidth: parent.height
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: !disabledHover
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton)
root.rightClicked();
else if (mouse.button === Qt.LeftButton)
root.clicked();
}
}
Text {
anchors.fill: parent
text: symbol
font.family: Fonts.nerd
font.pointSize: iconSize
font.bold: false
color: buttonColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
anchors.fill: parent
color: parent.hovered ? buttonColor : Colors.transparent
opacity: 0.3
radius: root.radius
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
}
}
}
@@ -1,20 +1,19 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Components
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
UProgressExpand {
readonly property var battery: UPower.displayDevice
readonly property bool isReady: (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
readonly property real percent: (isReady ? (battery.percentage * 100) : 0)
readonly property bool charging: (isReady ? battery.state === UPowerDeviceState.Charging : false)
property int lowBatteryThreshold: 20
symbol: {
return charging ? Icons.charging : percent >= 80 ? Icons.battery100 : percent >= 60 ? Icons.battery75 : percent >= 40 ? Icons.battery50 : percent >= 20 ? Icons.battery25 : Icons.battery00;
iconName: {
return charging ? "battery-charging" : percent >= 80 ? "battery-4" : percent >= 60 ? "battery-3" : percent >= 40 ? "battery-2" : percent >= 20 ? "battery-1" : "battery-0";
}
fillColor: Colors.sapphire
fillColor: Colors.mSky
value: percent
critical: isReady && !charging && percent <= lowBatteryThreshold
maxValue: 100
@@ -1,18 +1,18 @@
import QtQuick
import Quickshell
import qs.Components
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
UProgressExpand {
property ShellScreen screen: null
function getMonitor() {
return BrightnessService.getMonitorForScreen(screen) || null;
}
symbol: Icons.brightness
fillColor: Colors.blue
iconName: "sun-filled"
fillColor: Colors.mBlue
value: {
const monitor = getMonitor();
return monitor ? Math.round(monitor.brightness * 100) : "N/A";
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Modules.Bar.Services
import qs.Services
import qs.Utils
@@ -14,7 +14,7 @@ Item {
property int mode: 0
implicitWidth: root.barWidth * CavaBarService.count + root.barSpacing * (CavaBarService.count - 1)
implicitHeight: parent.height - 10
implicitHeight: Style.barHeight - Style.marginS * 2
RowLayout {
anchors.fill: parent
@@ -53,9 +53,7 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
MusicManager.playPause();
} else if (mouse.button === Qt.RightButton) {
SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
MediaService.playPause();
} else if (mouse.button === Qt.MiddleButton) {
mode = (mode + 1) % 3;
if (mode === 0) {
@@ -71,13 +69,15 @@ Item {
CavaBarService.forceEnable = false;
CavaBarService.forceDisable = true;
}
} else if (mouse.button === Qt.RightButton) {
LyricsService.toggleLyricsBar();
}
}
onWheel: function(wheel) {
if (wheel.angleDelta.y > 0)
MusicManager.previous();
MediaService.previous();
else if (wheel.angleDelta.y < 0)
MusicManager.next();
MediaService.next();
}
}
@@ -0,0 +1,17 @@
import QtQuick
import qs.Components
import qs.Constants
import qs.Modules.Bar.Services
import qs.Services
UProgressExpand {
iconName: "temperature"
fillColor: Colors.mYellow
critical: SystemStatService.cpuTemp > 80
value: Math.round(SystemStatService.cpuTemp)
maxValue: 100
textSuffix: "°C"
onClicked: {
MonitorProcess.toggle();
}
}
@@ -0,0 +1,20 @@
import QtQuick
import Quickshell.Io
import qs.Components
import qs.Constants
import qs.Modules.Bar.Services
import qs.Services
UProgressExpand {
// Quickshell.execDetached(["wezterm", "start", "--", "btop"]);
iconName: "cpu"
fillColor: Colors.mCyan
critical: SystemStatService.cpuUsage > 90
value: Math.round(SystemStatService.cpuUsage)
maxValue: 100
textSuffix: "%"
onClicked: {
MonitorProcess.toggle();
}
}
@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.Components
import qs.Constants
import qs.Services
import qs.Utils
@@ -10,7 +11,7 @@ import qs.Utils
Item {
id: root
property real maxWidth: 250
property real maxWidth: 320
property string fallbackIcon: "application-x-executable"
function getAppIcon(appId) {
@@ -23,24 +24,25 @@ Item {
return iconResult;
} catch (iconError) {
Logger.warn("FocusedWindow", "Error getting icon from CompositorService: " + iconError);
Logger.w("FocusedWindow", "Error getting icon from CompositorService: " + iconError);
}
}
return ThemeIcons.iconFromName(root.fallbackIcon);
} catch (e) {
Logger.warn("FocusedWindow", "Error in getAppIcon:", e);
Logger.w("FocusedWindow", "Error in getAppIcon:", e);
return ThemeIcons.iconFromName(root.fallbackIcon);
}
}
implicitHeight: parent.height
implicitWidth: layout.implicitWidth
implicitHeight: Math.max(windowIcon.implicitHeight, windowTitle.implicitHeight)
RowLayout {
id: layout
anchors.fill: parent
spacing: 10
visible: Niri.focusedWindowId !== -1
visible: Niri.hasFocusedWindow
Item {
// Layout.alignment: Qt.AlignVCenter
@@ -79,24 +81,20 @@ Item {
height: parent.height
anchors.verticalCenter: parent.verticalCenter
Text {
UText {
id: windowTitle
text: Niri.focusedWindowTitle
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: Colors.primary
color: Colors.mPrimary
}
Text {
UText {
text: Niri.focusedWindowTitle
anchors.verticalCenter: parent.verticalCenter
anchors.left: windowTitle.right
anchors.leftMargin: titleContainer.scrollSpacing
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: Colors.primary
color: Colors.mPrimary
visible: titleContainer.shouldScroll
}
@@ -3,15 +3,14 @@ import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Services
import qs.Modules.Bar.Misc
import qs.Components
MonitorItem {
symbol: Icons.ip
fillColor: Colors.peach
UProgressExpand {
iconName: "world"
fillColor: Colors.mOrange
value: 100
maxValue: 100
textValue: displayText
symbolSize: 18
property int displayIndex: 0
readonly property list<string> displayTexts: [IpService.countryCode, IpService.ip, IpService.alias]
@@ -1,84 +1,76 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Noctalia
import qs.Services
Rectangle {
implicitHeight: parent.height
radius: Style.radiusS
color: Colors.base
border.color: Colors.primary
color: Colors.mSurface
border.color: Colors.mPrimary
border.width: Style.borderS
Connections {
function onShowLyricsBarChanged() {
visible = SettingsService.showLyricsBar;
if (visible)
Component.onCompleted: {
LyricsService.startSyncing();
else
}
Component.onDestruction: {
LyricsService.stopSyncing();
}
target: SettingsService
}
implicitHeight: Style.barHeight - Style.marginXS * 2
implicitWidth: 600
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
spacing: Style.marginS
spacing: Style.marginXS
Item {
implicitWidth: parent.width - slowerButton.implicitWidth * 3 - parent.spacing * 3 - parent.anchors.leftMargin - parent.anchors.rightMargin
Layout.fillHeight: true
clip: true
NText {
UText {
text: LyricsService.lyrics[LyricsService.currentIndex] || ""
family: Fonts.sans
pointSize: Style.fontSizeS
pointSize: Style.fontSizeM
maximumLineCount: 1
anchors.verticalCenter: parent.verticalCenter
}
}
NIconButton {
UIconButton {
id: slowerButton
baseSize: 24
colorBg: Color.transparent
colorBgHover: Colors.blue
colorFg: Colors.blue
icon: "rotate-2"
colorFg: Colors.mBlue
iconName: "rotate-2"
baseSize: parent.height - Style.marginXS * 2
iconSize: Style.fontSizeM
onClicked: {
LyricsService.increaseOffset();
}
}
NIconButton {
UIconButton {
id: playPauseButton
baseSize: 24
colorBg: Color.transparent
colorBgHover: Colors.yellow
colorFg: Colors.yellow
icon: "rotate-clockwise-2"
colorFg: Colors.mYellow
iconName: "rotate-clockwise-2"
baseSize: parent.height - Style.marginXS * 2
iconSize: Style.fontSizeM
onClicked: {
LyricsService.decreaseOffset();
}
}
NIconButton {
UIconButton {
id: nextButton
baseSize: 24
colorBg: Color.transparent
colorBgHover: Colors.green
colorFg: Colors.green
icon: "rotate-clockwise"
colorFg: Colors.mGreen
iconName: "rotate-clockwise"
baseSize: parent.height - Style.marginXS * 2
iconSize: Style.fontSizeM
onClicked: {
LyricsService.resetOffset();
}
@@ -1,34 +1,23 @@
import QtQuick
import Quickshell.Io
import qs.Components
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Modules.Bar.Services
import qs.Services
MonitorItem {
UProgressExpand {
property bool _showPercent: false
symbol: Icons.memory
fillColor: Colors.green
iconName: "database"
fillColor: Colors.mGreen
critical: SystemStatService.memPercent > 90
value: Math.round(SystemStatService.memPercent)
maxValue: 100
textValue: _showPercent ? SystemStatService.memPercent : SystemStatService.memGb
textSuffix: _showPercent ? "%" : "GB"
onClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["wezterm", "start", "--", "btop"]);
MonitorProcess.toggle();
}
onRightClicked: {
_showPercent = !_showPercent;
}
Process {
id: action
running: false
}
}
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Services
@@ -16,35 +17,30 @@ Item {
anchors.bottom: parent.bottom
spacing: 5
Text {
text: Icons.download
font.pointSize: Fonts.icon - 3
color: Colors.primary
Layout.leftMargin: 10
UIcon {
iconName: "arrow-big-down-line-filled"
}
Text {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.pointSize: Fonts.medium
font.pointSize: Style.fontSizeM
font.family: Fonts.primary
color: Colors.primary
color: Colors.mPrimary
}
Item {
width: 5
}
Text {
text: Icons.upload
font.pointSize: Fonts.icon - 3
color: Colors.primary
UIcon {
iconName: "arrow-big-up-line-filled"
}
Text {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.pointSize: Fonts.medium
font.pointSize: Style.fontSizeM
font.family: Fonts.primary
color: Colors.primary
color: Colors.mPrimary
}
}
@@ -1,18 +1,20 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Services
Item {
id: root
property color fillColor: Colors.red
property color _actualColor: Colors.red
property color fillColor: Colors.mRed
property color _actualColor: Colors.mRed
property bool _expand: mouseArea.containsMouse
visible: RecordService.isRecording
implicitHeight: parent.height
implicitWidth: layout.width + 10
implicitHeight: Math.max(symbolIcon.implicitHeight, textLabel.implicitHeight)
implicitWidth: height + expander.implicitWidth
SequentialAnimation {
id: blinkAnimation
@@ -45,34 +47,36 @@ Item {
anchors.bottom: parent.bottom
spacing: 0
Text {
text: Icons.record
font.pointSize: 18
color: _actualColor
UIcon {
id: symbolIcon
iconName: "capture-filled"
iconSize: Style.fontSizeM + 12
color: root._actualColor
Layout.preferredWidth: parent.height
Layout.preferredHeight: parent.height
}
Item {
id: expander
implicitWidth: mouseArea.containsMouse ? ipText.implicitWidth + 10 : 0
implicitHeight: parent.height
implicitWidth: root._expand ? textLabel.implicitWidth + 10 : 0
clip: true
Text {
id: ipText
UText {
id: textLabel
text: RecordService.recordingDisplay
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: fillColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
text: RecordService.recordingDisplay || "Recording"
color: root.fillColor
}
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationFast
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
@@ -0,0 +1,10 @@
import QtQuick
import qs.Constants
import qs.Services
Text {
text: TimeService.time + " | " + TimeService.dateString
font.pointSize: Style.fontSizeM
font.family: Fonts.primary
color: Colors.mPrimary
}
@@ -2,16 +2,18 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Modules.Bar.Components
Item {
id: root
property ShellScreen screen
property int baseSize: Style.baseWidgetSize
implicitHeight: parent.height
implicitWidth: layout.implicitWidth
implicitWidth: baseSize + trayContainer.implicitWidth
implicitHeight: layout.implicitHeight
RowLayout {
id: layout
@@ -20,9 +22,10 @@ Item {
anchors.bottom: parent.bottom
spacing: 0
SymbolButton {
symbol: Icons.tray
buttonColor: Colors.green
UIconButton {
iconName: "layout-sidebar-right-expand-filled"
colorFg: Colors.mGreen
baseSize: root.baseSize
disabledHover: true
}
@@ -41,7 +44,7 @@ Item {
Behavior on implicitWidth {
NumberAnimation {
duration: 200
duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
@@ -1,12 +1,12 @@
import QtQuick
import Quickshell
import qs.Components
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
symbol: AudioService.muted ? Icons.volumeMuted : (AudioService.volume >= 0.5 ? Icons.volumeHigh : (AudioService.volume >= 0.2 ? Icons.volumeMedium : Icons.volumeLow))
fillColor: Colors.lavender
UProgressExpand {
iconName: AudioService.muted ? "volume-3" : (AudioService.volume >= 0.5 ? "volume" : (AudioService.volume >= 0.2 ? "volume-2" : "volume-2"))
fillColor: Colors.mLavender
value: Math.round(AudioService.volume * 100)
maxValue: 100
textSuffix: "%"
@@ -18,7 +18,7 @@ MonitorItem {
AudioService.decreaseVolume();
}
onClicked: {
AudioService.toggleMute();
AudioService.setOutputMuted(!AudioService.muted);
}
onRightClicked: {
Quickshell.execDetached(["sh", "-c", "pkill -x -n pwvucontrol || pwvucontrol"]);
@@ -1,4 +1,3 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -12,78 +11,59 @@ Item {
id: root
required property ShellScreen screen
property bool hovered: false
property ListModel localWorkspaces
property real masterProgress: 0
property bool effectsActive: false
property color effectColor: Colors.primary
property color effectColor: Colors.mPrimary
property int horizontalPadding: 16
property int spacingBetweenPills: 8
property bool isDestroying: false
signal workspaceChanged(int workspaceId, color primaryColor)
function triggerUnifiedWave() {
effectColor = Colors.primary;
effectColor = Colors.mPrimary;
masterAnimation.restart();
}
function updateWorkspaceFocus() {
for (let i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i);
if (ws.isFocused === true) {
root.triggerUnifiedWave();
root.workspaceChanged(ws.id, Colors.primary);
break;
}
}
}
function syncWorkspaces() {
let j = 0;
let focusChanged = false;
for (let i = 0; i < Niri.workspaces.count; i++) {
const ws = Niri.workspaces.get(i);
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
if (j < localWorkspaces.count) {
const existing = localWorkspaces.get(j);
if (ws.isFocused && !existing.isFocused)
focusChanged = true;
implicitWidth: {
let total = 0;
for (let i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i);
if (ws.isFocused)
total += 44;
else if (ws.isActive)
total += 28;
else
total += 16;
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills;
total += horizontalPadding * 2;
return total;
}
height: parent.height
Component.onCompleted: {
localWorkspaces.clear();
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
const ws = WorkspaceManager.workspaces.get(i);
if (ws.output.toLowerCase() === screen.name.toLowerCase())
localWorkspaces.setProperty(j, "id", ws.id);
localWorkspaces.setProperty(j, "idx", ws.idx);
localWorkspaces.setProperty(j, "isFocused", ws.isFocused);
localWorkspaces.setProperty(j, "isActive", ws.isActive);
localWorkspaces.setProperty(j, "isUrgent", ws.isUrgent);
localWorkspaces.setProperty(j, "isOccupied", ws.isOccupied);
} else {
localWorkspaces.append(ws);
if (ws.isFocused)
focusChanged = true;
}
workspaceRepeater.model = localWorkspaces;
updateWorkspaceFocus();
j++;
}
Component.onDestruction: {
root.isDestroying = true;
}
while (localWorkspaces.count > j)localWorkspaces.remove(localWorkspaces.count - 1)
if (focusChanged)
triggerUnifiedWave();
}
implicitWidth: pillRow.implicitWidth + horizontalPadding * 2
Component.onCompleted: syncWorkspaces()
Connections {
function onWorkspacesChanged() {
localWorkspaces.clear();
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
const ws = WorkspaceManager.workspaces.get(i);
if (ws.output.toLowerCase() === screen.name.toLowerCase())
localWorkspaces.append(ws);
}
workspaceRepeater.model = localWorkspaces;
updateWorkspaceFocus();
function onWorkspaceChanged() {
syncWorkspaces();
}
target: WorkspaceManager
target: Niri
}
SequentialAnimation {
@@ -118,35 +98,11 @@ Item {
}
Rectangle {
id: workspaceBackground
width: parent.width - 15
height: 26
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
radius: 12
color: Colors.transparent
layer.enabled: true
layer.effect: DropShadow {
color: "black"
radius: 12
samples: 24
verticalOffset: 0
horizontalOffset: 0
opacity: 0.1
}
}
Row {
id: pillRow
spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2
x: horizontalPadding
anchors.centerIn: parent
Repeater {
id: workspaceRepeater
@@ -172,23 +128,18 @@ Item {
id: workspacePill
anchors.fill: parent
radius: {
if (model.isFocused)
return 12;
else
return 6;
}
radius: height / 2
color: {
if (model.isFocused)
return Colors.primary;
return Colors.mPrimary;
if (model.isActive)
return Colors.overlay2;
return Colors.mOnSurfaceVariant;
if (model.isUrgent)
return Theme.error;
return Colors.surface2;
return Colors.mSurfaceVariant;
}
scale: model.isFocused ? 1 : 0.9
z: 0
@@ -199,28 +150,11 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceManager.switchToWorkspace(model.idx);
Niri.switchToWorkspace(model);
}
z: 20
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
Behavior on scale {
NumberAnimation {
@@ -246,14 +180,6 @@ Item {
}
Behavior on radius {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
}
Rectangle {
@@ -262,8 +188,8 @@ Item {
anchors.centerIn: workspacePillContainer
width: workspacePillContainer.width + 18 * root.masterProgress
height: workspacePillContainer.height + 18 * root.masterProgress
radius: width / 2
color: "transparent"
radius: height / 2
color: root.effectColor
border.color: root.effectColor
border.width: 2 + 6 * (1 - root.masterProgress)
opacity: root.effectsActive && model.isFocused ? (1 - root.masterProgress) * 0.7 : 0
@@ -6,7 +6,7 @@ pragma Singleton
Singleton {
id: root
property int count: 6
property int count: 7
property bool forceEnable: false
property bool forceDisable: false
property alias values: cavaProcess.values
@@ -0,0 +1,24 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
Singleton {
id: root
function toggle() {
if (process.running) {
process.signal(15);
return ;
}
process.running = true;
}
Process {
id: process
running: false
command: ["wezterm", "start", "--", "btop"]
}
}
@@ -11,7 +11,7 @@ Shape {
property int concaveHeight: 60 * size
property int offsetX: -20
property int offsetY: -20
property color fillColor: Colors.base
property color fillColor: Colors.mSurface
property int arcRadius: 20 * size
property var modelData: null
// Position flags derived from position string
@@ -10,12 +10,11 @@ import qs.Services
Scope {
id: rootScope
property var shell
property string namespace: "quickshell-corners"
property int topMargin: 45
property int cornerHeight: 20
property real cornerSize: 1
property real opacity: Niri.noFocus ? 0 : 1
property real opacity: BarService.focusMode ? 1 : 0
Item {
id: cornersRootItem
@@ -26,7 +25,15 @@ Scope {
model: Quickshell.screens
Item {
id: screenItem
property var modelData
// property int leftOffset: BarService.leftOffset(modelData)
// property int rightOffset: BarService.rightOffset(modelData)
readonly property var leftBar: BarService.getLeftSidebar(modelData.name)
readonly property var rightBar: BarService.getRightSidebar(modelData.name)
property int leftOffset: leftBar?.isOpen ? leftBar.barWidth : 0
property int rightOffset: rightBar?.isOpen ? rightBar.barWidth : 0
PanelWindow {
id: fakeBar
@@ -45,7 +52,7 @@ Scope {
Rectangle {
anchors.fill: parent
color: Colors.base
color: Colors.mSurface
opacity: rootScope.opacity
}
@@ -59,9 +66,10 @@ Scope {
color: "transparent"
screen: modelData
margins.top: topMargin
margins.left: screenItem.leftOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.layer: WlrLayer.Top
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
@@ -87,9 +95,10 @@ Scope {
color: "transparent"
screen: modelData
margins.top: topMargin
margins.right: screenItem.rightOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.layer: WlrLayer.Top
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
@@ -114,9 +123,10 @@ Scope {
anchors.left: true
color: "transparent"
screen: modelData
margins.left: screenItem.leftOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.layer: WlrLayer.Top
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
@@ -141,9 +151,10 @@ Scope {
anchors.right: true
color: "transparent"
screen: modelData
margins.right: screenItem.rightOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.layer: WlrLayer.Top
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
@@ -161,6 +172,22 @@ Scope {
}
Behavior on leftOffset {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
}
}
Behavior on rightOffset {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
}
}
}
}
@@ -169,7 +196,7 @@ Scope {
Behavior on opacity {
NumberAnimation {
duration: 1000
duration: Style.animationSlowest
easing.type: Easing.InOutCubic
}
File diff suppressed because it is too large Load Diff
@@ -1,254 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Constants
import qs.Modules.Panel.Misc
import qs.Noctalia
import qs.Services
NPanel {
id: root
preferredWidth: 380
preferredHeight: 500
panelContent: Rectangle {
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// HEADER
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NIcon {
icon: "bluetooth"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: "Bluetooth"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
id: bluetoothSwitch
checked: BluetoothService.enabled
onToggled: (checked) => {
return BluetoothService.setBluetoothEnabled(checked);
}
baseSize: Style.baseWidgetSize * 0.65
}
NIconButton {
enabled: BluetoothService.enabled
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
}
colorFg: Colors.green
colorBg: Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.green
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close();
}
colorFg: Colors.red
colorBg: Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.red
}
}
NDivider {
Layout.fillWidth: true
}
Rectangle {
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.transparent
// Center the content within this rectangle
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM
NIcon {
icon: "bluetooth-off"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Bluetooth is turned off"
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Bluetooth"
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
NScrollView {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
ColumnLayout {
width: parent.width
spacing: Style.marginM
// Connected devices
BluetoothDevicesList {
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.blocked && dev.connected;
});
return BluetoothService.sortDevices(filtered);
}
label: "Connected Devices"
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Known devices
BluetoothDevicesList {
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted);
});
return BluetoothService.sortDevices(filtered);
}
label: "Known Devices"
tooltipText: "Connect/Disconnect Devices"
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Available devices
BluetoothDevicesList {
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.blocked && !dev.paired && !dev.trusted;
});
return BluetoothService.sortDevices(filtered);
}
label: "Available Devices"
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Fallback - No devices, scanning
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return false;
var availableCount = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
}).length;
return (availableCount === 0);
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginXS
NIcon {
icon: "refresh"
pointSize: Style.fontSizeXXL * 1.5
color: Color.mPrimary
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: Style.animationSlow * 4
}
}
NText {
text: "Scanning..."
pointSize: Style.fontSizeL
color: Color.mOnSurface
}
}
NText {
text: "Pairing Mode"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}
@@ -1,526 +0,0 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Constants
import qs.Noctalia
import qs.Services
import qs.Utils
NPanel {
id: root
preferredWidth: 400
preferredHeight: 520
panelContent: ColumnLayout {
id: content
readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
property bool isCurrentMonth: checkIsCurrentMonth()
readonly property bool weatherReady: (LocationService.data.weather !== null)
function checkIsCurrentMonth() {
return (Time.date.getMonth() === grid.month) && (Time.date.getFullYear() === grid.year);
}
function getISOWeekNumber(date) {
const target = new Date(date.getTime());
target.setHours(0, 0, 0, 0);
const dayOfWeek = target.getDay() || 7;
target.setDate(target.getDate() + 4 - dayOfWeek);
const yearStart = new Date(target.getFullYear(), 0, 1);
const weekNumber = Math.ceil(((target - yearStart) / 8.64e+07 + 1) / 7);
return weekNumber;
}
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
Connections {
function onDateChanged() {
isCurrentMonth = checkIsCurrentMonth();
}
target: Time
}
// Combined blue banner with date/time and weather summary
NBox {
Layout.fillWidth: true
Layout.preferredHeight: blueColumn.implicitHeight + Style.marginM * 2
ColumnLayout {
id: blueColumn
anchors.fill: parent
anchors.margins: Style.marginM
spacing: 0
// Combined layout for weather icon, date, and weather text
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 60
spacing: Style.marginS
// Weather icon and temperature
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXXS
NIcon {
Layout.alignment: Qt.AlignHCenter
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "cloud"
pointSize: Style.fontSizeXXL
color: Colors.text
}
NText {
Layout.alignment: Qt.AlignHCenter
text: {
if (!weatherReady)
return "";
var temp = LocationService.data.weather.current_weather.temperature;
var suffix = "C";
temp = Math.round(temp);
return `${temp}°${suffix}`;
}
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Colors.text
}
}
// Today day number
NText {
visible: content.isCurrentMonth
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: Time.date.getDate()
pointSize: Style.fontSizeXXXL * 1.5
font.weight: Style.fontWeightBold
color: Colors.text
}
Item {
visible: !content.isCurrentMonth
}
// Month, year, location
ColumnLayout {
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
spacing: -Style.marginXS
RowLayout {
spacing: 0
NText {
text: Qt.locale().monthName(grid.month, Locale.LongFormat).toUpperCase()
pointSize: Style.fontSizeXL * 1.2
font.weight: Style.fontWeightBold
color: Colors.text
Layout.alignment: Qt.AlignBaseline
Layout.maximumWidth: 150
elide: Text.ElideRight
}
NText {
text: ` ${grid.year}`
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Qt.alpha(Colors.text, 0.7)
Layout.alignment: Qt.AlignBaseline
}
}
RowLayout {
spacing: 0
NText {
text: {
if (!weatherReady)
return "Weather unavailable";
const chunks = LocationService.data.name.split(",");
return chunks[0];
}
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
color: Colors.text
Layout.maximumWidth: 150
elide: Text.ElideRight
}
NText {
text: weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : ""
pointSize: Style.fontSizeXS
font.weight: Style.fontWeightMedium
color: Qt.alpha(Colors.text, 0.7)
}
}
}
// Spacer between date and clock
Item {
Layout.fillWidth: true
}
// Digital clock with circular progress
Item {
width: Style.fontSizeXXXL * 1.9
height: Style.fontSizeXXXL * 1.9
Layout.alignment: Qt.AlignVCenter
// Seconds circular progress
Canvas {
id: secondsProgress
property real progress: Time.date.getSeconds() / 60
anchors.fill: parent
onProgressChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - 3;
ctx.reset();
// Background circle
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.lineWidth = 2.5;
ctx.strokeStyle = Qt.alpha(Colors.text, 0.15);
ctx.stroke();
// Progress arc
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
ctx.lineWidth = 2.5;
ctx.strokeStyle = Colors.text;
ctx.lineCap = "round";
ctx.stroke();
}
Connections {
function onDateChanged() {
secondsProgress.progress = Time.date.getSeconds() / 60;
}
target: Time
}
}
// Digital clock
ColumnLayout {
anchors.centerIn: parent
spacing: -Style.marginXXS
NText {
text: {
var t = Qt.locale().toString(new Date(), "HH");
return t.split(" ")[0];
}
pointSize: Style.fontSizeXS
font.weight: Style.fontWeightBold
color: Colors.text
family: Fonts.sans
Layout.alignment: Qt.AlignHCenter
}
NText {
text: Qt.formatTime(Time.date, "mm")
pointSize: Style.fontSizeXXS
font.weight: Style.fontWeightBold
color: Colors.text
family: Fonts.sans
Layout.alignment: Qt.AlignHCenter
}
}
}
}
}
}
// 6-day forecast (outside blue banner)
RowLayout {
visible: weatherReady
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginL
Repeater {
model: weatherReady ? Math.min(6, LocationService.data.weather.daily.time.length) : 0
delegate: ColumnLayout {
Layout.preferredWidth: 0
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginS
NText {
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
return Qt.locale().toString(weatherDate, "ddd");
}
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignHCenter
}
NIcon {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
pointSize: Style.fontSizeXXL * 1.5
color: LocationService.weatherColorFromCode(LocationService.data.weather.daily.weathercode[index])
}
NText {
Layout.alignment: Qt.AlignHCenter
text: {
var max = LocationService.data.weather.daily.temperature_2m_max[index];
var min = LocationService.data.weather.daily.temperature_2m_min[index];
max = Math.round(max);
min = Math.round(min);
return `${max}°/${min}°`;
}
pointSize: Style.fontSizeXS
color: Colors.text
font.weight: Style.fontWeightMedium
}
}
}
}
// Loading indicator for weather
RowLayout {
visible: !weatherReady
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
NBusyIndicator {
}
}
// Spacer
Item {
}
// Navigation and divider
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NDivider {
Layout.fillWidth: true
}
NIconButton {
icon: "chevron-left"
colorBg: Color.transparent
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1);
grid.year = newDate.getFullYear();
grid.month = newDate.getMonth();
content.isCurrentMonth = content.checkIsCurrentMonth();
}
}
NIconButton {
icon: "calendar"
colorBg: Color.transparent
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: {
grid.month = Time.date.getMonth();
grid.year = Time.date.getFullYear();
content.isCurrentMonth = true;
}
}
NIconButton {
icon: "chevron-right"
colorBg: Color.transparent
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1);
grid.year = newDate.getFullYear();
grid.month = newDate.getMonth();
content.isCurrentMonth = content.checkIsCurrentMonth();
}
}
}
// Names of days of the week
RowLayout {
Layout.fillWidth: true
spacing: 0
Item {
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
}
GridLayout {
Layout.fillWidth: true
columns: 7
rows: 1
columnSpacing: 0
rowSpacing: 0
Repeater {
model: 7
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 0.6
NText {
anchors.centerIn: parent
text: {
let dayIndex = (content.firstDayOfWeek + index) % 7;
const dayNames = ["S", "M", "T", "W", "T", "F", "S"];
return dayNames[dayIndex];
}
color: Color.mPrimary
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
// Grid with weeks and days
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
// Column of week numbers
ColumnLayout {
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
Layout.fillHeight: true
spacing: 0
Repeater {
model: 6
Item {
Layout.fillWidth: true
Layout.fillHeight: true
NText {
anchors.centerIn: parent
color: Color.mOutline
pointSize: Style.fontSizeXXS
font.weight: Style.fontWeightMedium
text: {
let firstOfMonth = new Date(grid.year, grid.month, 1);
let firstDayOfWeek = content.firstDayOfWeek;
let firstOfMonthDayOfWeek = firstOfMonth.getDay();
let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7;
if (daysBeforeFirst === 0)
daysBeforeFirst = 7;
let gridStartDate = new Date(grid.year, grid.month, 1 - daysBeforeFirst);
let rowStartDate = new Date(gridStartDate);
rowStartDate.setDate(gridStartDate.getDate() + (index * 7));
let thursday = new Date(rowStartDate);
if (firstDayOfWeek === 0) {
thursday.setDate(rowStartDate.getDate() + 4);
} else if (firstDayOfWeek === 1) {
thursday.setDate(rowStartDate.getDate() + 3);
} else {
let daysToThursday = (4 - firstDayOfWeek + 7) % 7;
thursday.setDate(rowStartDate.getDate() + daysToThursday);
}
return `${getISOWeekNumber(thursday)}`;
}
}
}
}
}
// Days Grid
MonthGrid {
id: grid
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Style.marginXXS
month: Time.date.getMonth()
year: Time.date.getFullYear()
locale: Qt.locale()
delegate: Item {
Rectangle {
width: Style.baseWidgetSize * 0.9
height: Style.baseWidgetSize * 0.9
anchors.centerIn: parent
radius: Style.radiusM
color: model.today ? Color.mSecondary : Color.transparent
NText {
anchors.centerIn: parent
text: model.day
color: {
if (model.today)
return Color.mOnSecondary;
if (model.month === grid.month)
return Color.mOnSurface;
return Color.mOnSurfaceVariant;
}
opacity: model.month === grid.month ? 1 : 0.4
pointSize: Style.fontSizeM
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
}
@@ -1,96 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Noctalia
import qs.Services
GridLayout {
id: buttonsGrid
columns: 2
columnSpacing: 10
rowSpacing: 10
Layout.margins: 10
NIconButton {
id: slowerButton
baseSize: 32
colorBg: Color.transparent
colorBgHover: Colors.blue
colorFg: Colors.blue
icon: "arrow-bar-up"
onClicked: {
LyricsService.increaseOffset();
}
}
NIconButton {
id: playPauseButton
baseSize: 32
colorBg: Color.transparent
colorBgHover: Colors.yellow
colorFg: Colors.yellow
icon: "arrow-bar-down"
onClicked: {
LyricsService.decreaseOffset();
}
}
NIconButton {
id: nextButton
baseSize: 32
colorBg: Color.transparent
colorBgHover: Colors.green
colorFg: Colors.green
icon: "rotate-clockwise"
onClicked: {
LyricsService.resetOffset();
}
}
NIconButton {
id: fasterButton
baseSize: 32
colorBg: Color.transparent
colorBgHover: Colors.red
colorFg: Colors.red
icon: "trash"
onClicked: {
LyricsService.clearCache();
}
}
NIconButton {
id: barLyricsButton
baseSize: 32
colorBg: SettingsService.showLyricsBar ? Colors.peach : Color.transparent
colorBgHover: Colors.peach
colorFg: SettingsService.showLyricsBar ? Colors.base : Colors.peach
icon: "app-window"
onClicked: {
SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
}
}
NIconButton {
id: textButton
baseSize: 32
colorBg: Color.transparent
colorBgHover: Colors.subtext1
colorFg: Colors.subtext1
icon: "align-box-left-bottom"
onClicked: {
LyricsService.showLyricsText();
controlCenterPanel.close();
}
}
}
@@ -1,458 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Noctalia
import qs.Services
import qs.Utils
NBox {
id: root
// Background artwork that covers everything
Item {
anchors.fill: parent
clip: true
NImageRounded {
id: bgArtImage
anchors.fill: parent
imagePath: MusicManager.trackArtUrl
imageRadius: Style.radiusM
visible: MusicManager.trackArtUrl !== ""
}
// Dark overlay for readability
Rectangle {
anchors.fill: parent
color: Color.mSurfaceVariant
opacity: 0.85
radius: Style.radiusM
}
// Border
Rectangle {
anchors.fill: parent
color: Color.transparent
radius: Style.radiusM
}
}
// Background visualizer on top of the artwork
Item {
id: visualizerContainer
anchors.fill: parent
layer.enabled: true
Item {
anchors.fill: parent
Cava {
id: cava
count: 32
}
Repeater {
model: cava.values
Rectangle {
anchors.bottom: parent.bottom
width: (parent.width - (cava.count - 1) * Style.marginXS) / cava.count
height: modelData * parent.height
x: index * (width + Style.marginXS)
color: Color.mPrimary
radius: width / 2
opacity: 0.25
}
}
}
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: root.width
height: root.height
radius: Style.radiusM
color: "white"
}
}
}
}
// Player selector - positioned at the very top
Rectangle {
id: playerSelectorButton
property var currentPlayer: MusicManager.getAvailablePlayers()[MusicManager.selectedPlayerIndex]
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Style.marginXS
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
height: Style.barHeight
visible: MusicManager.getAvailablePlayers().length > 1
radius: Style.radiusM
color: Color.transparent
Component.onCompleted: {
MusicManager.selectedPlayerIndex = -1;
}
Component.onDestruction: {
MusicManager.selectedPlayerIndex = -1;
}
RowLayout {
anchors.fill: parent
spacing: Style.marginS
NIcon {
icon: "caret-down"
pointSize: Style.fontSizeXXL
color: Color.mOnSurfaceVariant
}
NText {
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
MouseArea {
id: playerSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
var menuItems = [];
var players = MusicManager.getAvailablePlayers();
for (var i = 0; i < players.length; i++) {
menuItems.push({
"label": players[i].identity,
"action": i.toString(),
"icon": "disc",
"enabled": true,
"visible": true
});
}
playerContextMenu.model = menuItems;
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height);
}
}
NContextMenu {
id: playerContextMenu
parent: root
width: 200
onTriggered: function(action) {
var index = parseInt(action);
if (!isNaN(index)) {
MusicManager.selectedPlayerIndex = index;
MusicManager.updateCurrentPlayer();
}
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM
// No media player detected
ColumnLayout {
id: fallback
visible: !main.visible
spacing: Style.marginS
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginL
Item {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: Style.fontSizeXXXL * 4
Layout.preferredHeight: Style.fontSizeXXXL * 4
// Pulsating audio circles (background)
Repeater {
model: 3
Rectangle {
anchors.centerIn: parent
width: parent.width * (1 + index * 0.2)
height: width
radius: width / 2
color: "transparent"
border.color: Color.mOnSurfaceVariant
border.width: 2
opacity: 0
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
PauseAnimation {
duration: index * 600
}
NumberAnimation {
from: 1
to: 0
duration: 2000
easing.type: Easing.OutQuad
}
}
SequentialAnimation on scale {
running: true
loops: Animation.Infinite
PauseAnimation {
duration: index * 600
}
NumberAnimation {
from: 0.5
to: 1.2
duration: 2000
easing.type: Easing.OutQuad
}
}
}
}
// Spinning disc
NIcon {
anchors.centerIn: parent
icon: "disc"
pointSize: Style.fontSizeXXXL * 3
color: Color.mOnSurfaceVariant
RotationAnimator on rotation {
from: 0
to: 360
duration: 8000
loops: Animation.Infinite
running: true
}
}
}
// Descriptive text
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginXS
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
// MediaPlayer Main Content
ColumnLayout {
id: main
visible: MusicManager.currentPlayer && MusicManager.canPlay
spacing: Style.marginS
// Spacer to push content down
Item {
Layout.preferredHeight: Style.marginM
}
// Metadata at the bottom left
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
spacing: Style.marginXS
NText {
visible: MusicManager.trackTitle !== ""
text: MusicManager.trackTitle
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
elide: Text.ElideRight
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: 1
}
NText {
visible: MusicManager.trackArtist !== ""
text: MusicManager.trackArtist
color: Color.mPrimary
pointSize: Style.fontSizeS
elide: Text.ElideRight
Layout.fillWidth: true
maximumLineCount: 1
}
NText {
visible: MusicManager.trackAlbum !== ""
text: MusicManager.trackAlbum
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
elide: Text.ElideRight
Layout.fillWidth: true
maximumLineCount: 1
}
}
// Progress slider
Item {
id: progressWrapper
property real localSeekRatio: -1
property real lastSentSeekRatio: -1
property real seekEpsilon: 0.01
property real progressRatio: {
if (!MusicManager.currentPlayer || MusicManager.trackLength <= 0)
return 0;
const r = MusicManager.currentPosition / MusicManager.trackLength;
if (isNaN(r) || !isFinite(r))
return 0;
return Math.max(0, Math.min(1, r));
}
property real effectiveRatio: (MusicManager.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
visible: (MusicManager.currentPlayer && MusicManager.trackLength > 0)
Layout.fillWidth: true
height: Style.baseWidgetSize * 0.5
Timer {
id: seekDebounce
interval: 75
repeat: false
onTriggered: {
if (MusicManager.isSeeking && progressWrapper.localSeekRatio >= 0) {
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio));
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MusicManager.seekByRatio(next);
progressWrapper.lastSentSeekRatio = next;
}
}
}
}
NSlider {
id: progressSlider
anchors.fill: parent
from: 0
to: 1
stepSize: 0
snapAlways: false
enabled: MusicManager.trackLength > 0 && MusicManager.canSeek
heightRatio: 0.65
onMoved: {
progressWrapper.localSeekRatio = value;
seekDebounce.restart();
}
onPressedChanged: {
if (pressed) {
MusicManager.isSeeking = true;
progressWrapper.localSeekRatio = value;
MusicManager.seekByRatio(value);
progressWrapper.lastSentSeekRatio = value;
} else {
seekDebounce.stop();
MusicManager.seekByRatio(value);
MusicManager.isSeeking = false;
progressWrapper.localSeekRatio = -1;
progressWrapper.lastSentSeekRatio = -1;
}
}
}
Binding {
target: progressSlider
property: "value"
value: progressWrapper.progressRatio
when: !MusicManager.isSeeking
}
}
// Media controls
RowLayout {
spacing: Style.marginS
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
NIconButton {
icon: "media-prev"
visible: MusicManager.canGoPrevious
onClicked: MusicManager.canGoPrevious ? MusicManager.previous() : {
}
}
NIconButton {
icon: MusicManager.isPlaying ? "media-pause" : "media-play"
visible: (MusicManager.canPlay || MusicManager.canPause)
onClicked: (MusicManager.canPlay || MusicManager.canPause) ? MusicManager.playPause() : {
}
}
NIconButton {
icon: "media-next"
visible: MusicManager.canGoNext
onClicked: MusicManager.canGoNext ? MusicManager.next() : {
}
}
}
}
}
}
@@ -1,95 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Modules.Panel.Misc
import qs.Noctalia
import qs.Services
import qs.Utils
ColumnLayout {
id: root
spacing: 0
RowLayout {
id: sunsetControlRow
Layout.fillWidth: true
NIconButton {
id: barLyricsButton
implicitHeight: 32
implicitWidth: 32
colorBg: SunsetService.isRunning ? Colors.flamingo : Color.transparent
colorBgHover: Colors.flamingo
colorFg: SunsetService.isRunning ? Colors.base : Colors.flamingo
icon: "sunset-2"
onClicked: SunsetService.toggleSunset()
}
NText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: SunsetService.isRunning ? "Temp: " + SunsetService.temperature + " K" : "Sunset Off"
}
}
NBox {
id: monitors
compact: true
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginS
MonitorSlider {
icon: "cpu-usage"
value: SystemStatService.cpuUsage
from: 0
to: 100
colorFill: Colors.teal
Layout.fillWidth: true
}
MonitorSlider {
icon: "memory"
value: SystemStatService.memPercent
from: 0
to: 100
colorFill: Colors.green
Layout.fillWidth: true
}
MonitorSlider {
icon: "cpu-temperature"
value: SystemStatService.cpuTemp
from: 0
to: 100
colorFill: Colors.yellow
Layout.fillWidth: true
}
MonitorSlider {
icon: "storage"
value: SystemStatService.diskPercent
from: 0
to: 100
colorFill: Colors.peach
Layout.fillWidth: true
}
}
}
}
@@ -1,175 +0,0 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import Quickshell.Widgets
import qs.Constants
import qs.Noctalia
import qs.Services
import qs.Utils
ColumnLayout {
id: root
readonly property bool hasPP: PowerProfileService.available
spacing: Style.marginM
NBox {
id: whoamiBox
property string uptimeText: "--"
property string hostname: "--"
function updateSystemInfo() {
uptimeProcess.running = true;
hostnameProcess.running = true;
}
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
id: content
spacing: root.spacing
anchors.fill: parent
anchors.margins: root.spacing
NImageCircled {
width: Style.baseWidgetSize * 1.5
height: Style.baseWidgetSize * 1.5
imagePath: Quickshell.shellDir + "/Assets/Images/Avatar.jpg"
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM)
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: Style.marginXS
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
NText {
text: `${Quickshell.env("USER") || "user"} @ ${whoamiBox.hostname}`
font.weight: Style.fontWeightBold
font.pointSize: Style.fontSizeL
font.capitalization: Font.Capitalize
}
NText {
text: "Uptime: " + whoamiBox.uptimeText
font.pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
}
Item {
Layout.fillWidth: true
}
}
// ----------------------------------
// Uptime
Timer {
interval: 60000
repeat: true
running: true
onTriggered: uptimeProcess.running = true
}
Process {
id: uptimeProcess
command: ["cat", "/proc/uptime"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]);
whoamiBox.uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds);
uptimeProcess.running = false;
}
}
}
Process {
id: hostnameProcess
command: ["cat", "/etc/hostname"]
running: true
stdout: StdioCollector {
onStreamFinished: {
whoamiBox.hostname = this.text.trim();
hostnameProcess.running = false;
}
}
}
}
RowLayout {
id: utilitiesRow
Layout.fillWidth: true
// Performance
NIconButton {
implicitHeight: 32
implicitWidth: 32
icon: PowerProfileService.getIcon(PowerProfile.Performance)
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBgHover: Colors.red
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Colors.red : Color.transparent
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Colors.red
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
}
// Balanced
NIconButton {
implicitHeight: 32
implicitWidth: 32
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBgHover: Colors.blue
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Colors.blue : Color.transparent
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Colors.blue
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
}
// Eco
NIconButton {
implicitHeight: 32
implicitWidth: 32
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBgHover: Colors.green
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Colors.green : Color.transparent
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Colors.green
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
}
Item {
Layout.fillWidth: true
}
// Lyrics Offset
NText {
text: `Lyrics Offset: ${LyricsService.offset >= 0 ? '+' : ''}${LyricsService.offset} ms`
Layout.alignment: Qt.AlignVCenter
}
}
}
@@ -1,87 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs.Constants
import qs.Modules.Panel.Cards
import qs.Noctalia
import qs.Services
import qs.Utils
NPanel {
id: root
// Positioning
readonly property string controlCenterPosition: "top_left"
property real topCardHeight: 120
property real middleCardHeight: 100
property real bottomCardHeight: 200
preferredWidth: 480
preferredHeight: topCardHeight + middleCardHeight + bottomCardHeight + Style.marginL * 4
panelKeyboardFocus: false
panelAnchorHorizontalCenter: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_center")
panelAnchorVerticalCenter: false
panelAnchorLeft: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
panelAnchorRight: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
panelAnchorBottom: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
panelAnchorTop: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_")
panelContent: Item {
id: content
property real cardSpacing: Style.marginL
// Layout content
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: content.cardSpacing
spacing: content.cardSpacing
// Top Card: profile + utilities
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: topCardHeight
TopLeftCard {
Layout.fillWidth: true
Layout.maximumHeight: topCardHeight
}
LyricsControl {
Layout.preferredHeight: topCardHeight
}
}
LyricsCard {
Layout.fillWidth: true
Layout.preferredHeight: middleCardHeight
}
// Media + stats column
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: bottomCardHeight
spacing: content.cardSpacing
SystemMonitorCard {
Layout.fillWidth: true
Layout.preferredHeight: bottomCardHeight
}
MediaCard {
Layout.preferredWidth: 270
Layout.preferredHeight: bottomCardHeight
}
}
}
}
}
@@ -1,54 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Noctalia
Item {
id: root
property string icon: "volume-high"
property real value: 50
property real from: 0
property real to: 100
property color colorFill: Colors.primary
property color colorRest: Colors.surface0
implicitHeight: layout.implicitHeight
RowLayout {
id: layout
anchors.fill: parent
spacing: Style.marginS
NIcon {
id: iconItem
icon: root.icon
color: root.colorFill
}
Rectangle {
id: whole
Layout.fillWidth: true
color: root.colorRest
radius: height / 2
height: Style.baseWidgetSize * 0.3
Rectangle {
id: fill
width: Math.max(0, Math.min(whole.width, (root.value - root.from) / (root.to - root.from) * whole.width))
height: parent.height
color: root.colorFill
radius: height / 2
anchors.left: parent.left
}
}
}
}
@@ -1,341 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import Quickshell.Wayland
import qs.Constants
import qs.Noctalia
import qs.Services
import qs.Utils
// Notification History panel
NPanel {
id: root
preferredWidth: 380
preferredHeight: 480
panelContent: Rectangle {
id: notificationRect
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// Header section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NIcon {
icon: "bell"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: "Notifications"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: SettingsService.notifications.doNotDisturb ? "bell-off" : "bell"
baseSize: Style.baseWidgetSize * 0.8
onClicked: SettingsService.notifications.doNotDisturb = !SettingsService.notifications.doNotDisturb
colorFg: SettingsService.notifications.doNotDisturb ? Colors.base : Colors.green
colorBg: SettingsService.notifications.doNotDisturb ? Colors.green : Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.green
}
NIconButton {
icon: "trash"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
NotificationService.clearHistory();
// Close panel as there is nothing more to see.
root.close();
}
colorFg: Colors.red
colorBg: Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.red
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
colorFg: Colors.blue
colorBg: Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.blue
}
}
NDivider {
Layout.fillWidth: true
}
// Empty state when no notifications
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.historyList.count === 0
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
NIcon {
icon: "bell-off"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No Notifications"
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Notification list
NListView {
id: notificationList
// Track which notification is expanded
property string expandedId: ""
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
model: NotificationService.historyList
spacing: Style.marginM
clip: true
boundsBehavior: Flickable.StopAtBounds
visible: NotificationService.historyList.count > 0
delegate: NBox {
property string notificationId: model.id
property bool isExpanded: notificationList.expandedId === notificationId
width: notificationList.width
height: notificationLayout.implicitHeight + (Style.marginM * 2)
// Click to expand/collapse
MouseArea {
anchors.fill: parent
// Don't capture clicks on the delete button
anchors.rightMargin: 48
enabled: (summaryText.truncated || bodyText.truncated)
onClicked: {
if (notificationList.expandedId === notificationId)
notificationList.expandedId = "";
else
notificationList.expandedId = notificationId;
}
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
}
RowLayout {
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
ColumnLayout {
NImageCircled {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.alignment: Qt.AlignTop
Layout.topMargin: 20
imagePath: model.cachedImage || model.originalImage || ""
borderColor: Color.transparent
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24
}
Item {
Layout.fillHeight: true
}
}
// Notification content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: Style.marginXS
Layout.rightMargin: -(Style.marginM + Style.baseWidgetSize * 0.6)
// Header row with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
// Urgency indicator
Rectangle {
Layout.preferredWidth: 6
Layout.preferredHeight: 6
Layout.alignment: Qt.AlignVCenter
radius: 3
visible: model.urgency !== 1
color: {
if (model.urgency === 2)
return Color.mError;
else if (model.urgency === 0)
return Color.mOnSurfaceVariant;
else
return Color.transparent;
}
}
NText {
text: model.appName || "Unknown App"
pointSize: Style.fontSizeXS
color: Color.mSecondary
family: Fonts.sans
}
NText {
text: Time.formatRelativeTime(model.timestamp)
pointSize: Style.fontSizeXS
color: Color.mSecondary
family: Fonts.sans
}
Item {
Layout.fillWidth: true
}
}
// Summary
NText {
id: summaryText
text: model.summary || "No Summary"
pointSize: Style.fontSizeM
font.weight: Font.Medium
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: isExpanded ? 999 : 2
family: Fonts.sans
elide: Text.ElideRight
}
// Body
NText {
id: bodyText
text: model.body || ""
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: isExpanded ? 999 : 3
elide: Text.ElideRight
family: Fonts.sans
visible: text.length > 0
}
// Spacer for expand indicator
Item {
Layout.fillWidth: true
Layout.preferredHeight: (!isExpanded && (summaryText.truncated || bodyText.truncated)) ? (Style.marginS) : 0
}
// Expand indicator
RowLayout {
Layout.fillWidth: true
visible: !isExpanded && (summaryText.truncated || bodyText.truncated)
spacing: Style.marginXS
Item {
Layout.fillWidth: true
}
NText {
text: "Click to expand"
pointSize: Style.fontSizeXS
color: Color.mPrimary
family: Fonts.sans
font.weight: Font.Medium
}
NIcon {
icon: "chevron-down"
pointSize: Style.fontSizeS
color: Color.mPrimary
}
}
}
// Delete button
NIconButton {
icon: "trash"
baseSize: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignTop
onClicked: {
// Remove from history using the service API
NotificationService.removeFromHistory(notificationId);
}
colorFg: Colors.red
colorBg: Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.red
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
// Smooth color transition on hover
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
@@ -1,633 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Constants
import qs.Noctalia
import qs.Services
import qs.Utils
NPanel {
id: root
property string passwordSsid: ""
property string passwordInput: ""
property string expandedSsid: ""
preferredWidth: 400
preferredHeight: 500
onOpened: NetworkService.scan()
panelContent: Rectangle {
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// Header
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NIcon {
icon: SettingsService.wifiEnabled ? "wifi" : "wifi-off"
pointSize: Style.fontSizeXXL
color: SettingsService.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: "WiFi"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
id: wifiSwitch
checked: SettingsService.wifiEnabled
onToggled: (checked) => {
return NetworkService.setWifiEnabled(checked);
}
baseSize: Style.baseWidgetSize * 0.65
}
NIconButton {
icon: "refresh"
baseSize: Style.baseWidgetSize * 0.8
enabled: SettingsService.wifiEnabled && !NetworkService.scanning
onClicked: NetworkService.scan()
colorFg: Colors.green
colorBg: Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.green
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
colorFg: Colors.red
colorBg: Color.transparent
colorFgHover: Colors.base
colorBgHover: Colors.red
}
}
NDivider {
Layout.fillWidth: true
}
// Error message
Rectangle {
visible: NetworkService.lastError.length > 0
Layout.fillWidth: true
Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * 2)
color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1)
radius: Style.radiusS
border.width: Math.max(1, Style.borderS)
border.color: Color.mError
RowLayout {
id: errorRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
NIcon {
icon: "warning"
pointSize: Style.fontSizeL
color: Color.mError
}
NText {
text: NetworkService.lastError
color: Color.mError
pointSize: Style.fontSizeS
wrapMode: Text.Wrap
Layout.fillWidth: true
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.6
onClicked: NetworkService.lastError = ""
}
}
}
// Main content area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.transparent
// WiFi disabled state
ColumnLayout {
visible: !SettingsService.wifiEnabled
anchors.fill: parent
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "wifi-off"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Wi-Fi Disabled"
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Please enable Wi-Fi to connect to a network."
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Scanning state
ColumnLayout {
visible: SettingsService.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
NBusyIndicator {
running: true
color: Color.mPrimary
size: Style.baseWidgetSize
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Searching for networks..."
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Networks list container
NScrollView {
visible: SettingsService.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0)
anchors.fill: parent
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
ColumnLayout {
width: parent.width
spacing: Style.marginM
// Network list
Repeater {
model: {
if (!SettingsService.wifiEnabled)
return [];
const nets = Object.values(NetworkService.networks);
return nets.sort((a, b) => {
if (a.connected !== b.connected)
return b.connected - a.connected;
return b.signal - a.signal;
});
}
NBox {
Layout.fillWidth: true
implicitHeight: netColumn.implicitHeight + (Style.marginM * 2)
// Add opacity for operations in progress
opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1
ColumnLayout {
id: netColumn
width: parent.width - (Style.marginM * 2)
x: Style.marginM
y: Style.marginM
spacing: Style.marginS
// Main row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: NetworkService.signalIcon(modelData.signal)
pointSize: Style.fontSizeXXL
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
NText {
text: modelData.ssid
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginXS
NText {
text: `${modelData.signal}%`
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.preferredWidth: Style.marginXXS
}
// Update the status badges area (around line 237)
Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.mPrimary
radius: height * 0.5
width: connectedText.implicitWidth + (Style.marginS * 2)
height: connectedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: connectedText
anchors.centerIn: parent
text: "Connected"
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Color.mError
radius: height * 0.5
width: disconnectingText.implicitWidth + (Style.marginS * 2)
height: disconnectingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: disconnectingText
anchors.centerIn: parent
text: "disconnecting"
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Color.mError
radius: height * 0.5
width: forgettingText.implicitWidth + (Style.marginS * 2)
height: forgettingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: forgettingText
anchors.centerIn: parent
text: "forgetting"
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.transparent
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS)
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * 2)
height: savedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: savedText
anchors.centerIn: parent
text: "saved"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
}
}
}
// Action area
RowLayout {
spacing: Style.marginS
NBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary
size: Style.baseWidgetSize * 0.5
}
NIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
icon: "trash"
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
}
NButton {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
text: {
if (modelData.existing || modelData.cached)
return "Connect";
if (!NetworkService.isSecured(modelData.security))
return "Connect";
return "Enter Password";
}
outlined: !hovered
fontSize: Style.fontSizeXS
enabled: !NetworkService.connecting
onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid);
} else {
passwordSsid = modelData.ssid;
passwordInput = "";
expandedSsid = "";
}
}
}
NButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
text: "Disconnect"
outlined: !hovered
fontSize: Style.fontSizeXS
backgroundColor: Color.mError
onClicked: NetworkService.disconnect(modelData.ssid)
}
}
}
// Password input
Rectangle {
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: passwordRow.implicitHeight + Style.marginS * 2
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS)
radius: Style.radiusS
RowLayout {
id: passwordRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusXS
color: Color.mSurface
border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS)
TextInput {
id: pwdInput
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginS
text: passwordInput
font.family: Fonts.sans
font.pointSize: Style.fontSizeS
color: Color.mOnSurface
echoMode: TextInput.Password
selectByMouse: true
focus: visible
passwordCharacter: "●"
onTextChanged: passwordInput = text
onVisibleChanged: {
if (visible)
forceActiveFocus();
}
onAccepted: {
if (text && !NetworkService.connecting) {
NetworkService.connect(passwordSsid, text);
passwordSsid = "";
passwordInput = "";
}
}
NText {
visible: parent.text.length === 0
anchors.verticalCenter: parent.verticalCenter
text: "Enter Password"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
}
}
NButton {
text: "Connect"
fontSize: Style.fontSizeXXS
enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: true
onClicked: {
NetworkService.connect(passwordSsid, passwordInput);
passwordSsid = "";
passwordInput = "";
}
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
passwordSsid = "";
passwordInput = "";
}
}
}
}
// Forget network
Rectangle {
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.marginS * 2
color: Color.mSurfaceVariant
radius: Style.radiusS
border.width: Math.max(1, Style.borderS)
border.color: Color.mOutline
RowLayout {
id: forgetRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
RowLayout {
NIcon {
icon: "trash"
pointSize: Style.fontSizeL
color: Color.mError
}
NText {
text: "Forget this network?"
pointSize: Style.fontSizeS
color: Color.mError
Layout.fillWidth: true
}
}
NButton {
id: forgetButton
text: "Forget"
fontSize: Style.fontSizeXXS
backgroundColor: Color.mError
outlined: forgetButton.hovered ? false : true
onClicked: {
NetworkService.forget(modelData.ssid);
expandedSsid = "";
}
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = ""
}
}
}
}
// Smooth opacity animation
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
}
}
}
}
// Empty state when no networks
ColumnLayout {
visible: SettingsService.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
NIcon {
icon: "search"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No networks found"
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: "Scan Again"
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan()
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}
@@ -0,0 +1,79 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Constants
import qs.Modules.Misc
import qs.Modules.Sidebar
import qs.Services
PanelWindow {
id: root
property int barWidth: Style.sidebarWidth
property int realWidth: 0
property bool isOpen: false
property Component contentComponent: null
property bool isLeft: true
function open() {
realWidth = barWidth;
isOpen = true;
}
function close() {
realWidth = 0;
isOpen = false;
}
Component.onCompleted: {
if (root.isLeft)
BarService.registerLeft(modelData.name, root);
else
BarService.registerRight(modelData.name, root);
}
screen: modelData
WlrLayershell.namespace: root.isLeft ? "quickshell-sidebar-left" : "quickshell-sidebar-right"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
implicitWidth: realWidth > 0 ? barWidth : 0 // jump change for better performance (maybe)
visible: realWidth > 0
margins.top: Style.barHeight
color: Colors.transparent
anchors {
left: isLeft
top: true
bottom: true
right: !isLeft
}
Rectangle {
id: sidebarContent
width: root.barWidth
height: parent.height
x: isLeft ? root.realWidth - root.barWidth : root.barWidth - root.realWidth
color: Colors.mSurface
Loader {
anchors.fill: parent
active: root.realWidth > 0
asynchronous: true
sourceComponent: root.contentComponent
}
}
mask: Region {
item: sidebarContent
}
Behavior on realWidth {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
}
}
}
@@ -5,7 +5,7 @@ import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Constants
import qs.Noctalia
import qs.Components
import qs.Services
ColumnLayout {
@@ -17,12 +17,12 @@ ColumnLayout {
}
Layout.fillWidth: true
spacing: Style.marginM
spacing: Style.marginS
NText {
UText {
text: root.label
pointSize: Style.fontSizeL
color: Color.mSecondary
color: Colors.mPrimary
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
visible: root.model.length > 0
@@ -35,39 +35,41 @@ ColumnLayout {
model: root.model
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
NBox {
UBox {
id: device
readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
function getContentColor(defaultColor = Color.mOnSurface) {
compact: true
function getContentColor(defaultColor = Colors.mOnSurface) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary;
return Colors.mPrimary;
if (modelData.blocked)
return Color.mError;
return Colors.mError;
return defaultColor;
}
Layout.fillWidth: true
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * 2)
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginS * 2)
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
anchors.margins: Style.marginS
spacing: Style.marginS
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
icon: BluetoothService.getDeviceIcon(modelData)
pointSize: Style.fontSizeXXL
color: getContentColor(Color.mOnSurface)
UIcon {
iconName: BluetoothService.getDeviceIcon(modelData)
iconSize: Style.fontSizeXXL
color: getContentColor(Colors.mOnSurface)
Layout.alignment: Qt.AlignVCenter
}
@@ -76,21 +78,21 @@ ColumnLayout {
spacing: Style.marginXXS
// Device name
NText {
UText {
text: modelData.name || modelData.deviceName
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
color: getContentColor(Color.mOnSurface)
color: getContentColor(Colors.mOnSurface)
Layout.fillWidth: true
}
// Status
NText {
UText {
text: BluetoothService.getStatusString(modelData)
visible: text !== ""
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
color: getContentColor(Colors.mOnSurfaceVariant)
}
// Signal Strength
@@ -100,34 +102,34 @@ ColumnLayout {
spacing: Style.marginXS
// Device signal strength - "Unknown" when not connected
NText {
UText {
text: BluetoothService.getSignalStrength(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
color: getContentColor(Colors.mOnSurfaceVariant)
}
NIcon {
UIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
iconName: BluetoothService.getSignalIcon(modelData)
iconSize: Style.fontSizeXS
color: getContentColor(Colors.mOnSurface)
}
NText {
UText {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
color: getContentColor(Colors.mOnSurface)
}
}
// Battery
NText {
UText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
color: getContentColor(Colors.mOnSurfaceVariant)
}
}
@@ -138,19 +140,18 @@ ColumnLayout {
}
// Call to action
NButton {
UButton {
id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting)
enabled: (canConnect || canDisconnect) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeXS
fontWeight: Style.fontWeightMedium
backgroundColor: {
if (device.canDisconnect && !isBusy)
return Color.mError;
return Colors.mError;
return Color.mPrimary;
return Colors.mPrimary;
}
text: {
if (modelData.pairing)
@@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Constants
import qs.Services
import qs.Components
import qs.Modules.Sidebar.Misc
ColumnLayout {
spacing: Style.marginM
Rectangle {
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true
Layout.fillHeight: true
color: Colors.transparent
// Center the content within this rectangle
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM
UIcon {
iconName: "bluetooth-off"
iconSize: 64
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UText {
text: "Bluetooth is turned off"
pointSize: Style.fontSizeL
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UText {
text: "Enable Bluetooth"
pointSize: Style.fontSizeS
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
UScrollView {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
ColumnLayout {
width: parent.width
spacing: Style.marginM
// Connected devices
BluetoothDevicesList {
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.blocked && dev.connected;
});
return BluetoothService.sortDevices(filtered);
}
label: "Connected Devices"
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Known devices
BluetoothDevicesList {
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted);
});
return BluetoothService.sortDevices(filtered);
}
label: "Known Devices"
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Available devices
BluetoothDevicesList {
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.blocked && !dev.paired && !dev.trusted;
});
return BluetoothService.sortDevices(filtered);
}
label: "Available Devices"
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Fallback - No devices, scanning
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginL
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return false;
var availableCount = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
}).length;
return (availableCount === 0);
}
UBusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: true
width: Style.fontSizeL
height: Style.fontSizeL
}
UText {
text: "Pairing Mode"
pointSize: Style.fontSizeM
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
Item {
Layout.fillHeight: true
}
}
}
}
@@ -0,0 +1,133 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Services
import qs.Utils
UBox {
id: root
// Internal state
readonly property var now: Time.now
readonly property bool weatherReady: LocationService.data.weather !== null
// Expose current month/year for potential synchronization with CalendarMonthCard
readonly property int currentMonth: now.getMonth()
readonly property int currentYear: now.getFullYear()
implicitHeight: (60) + Style.marginM * 2
color: Colors.mPrimary
ColumnLayout {
id: capsuleColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.topMargin: Style.marginM
anchors.bottomMargin: Style.marginM
anchors.rightMargin: clockLoader.width + Style.marginXL * 2
anchors.leftMargin: Style.marginXL
spacing: 0
// Combined layout for date, month year, location and time-zone
RowLayout {
Layout.fillWidth: true
height: 60
clip: true
spacing: Style.marginS
// Today day number
UText {
Layout.preferredWidth: implicitWidth
elide: Text.ElideNone
clip: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.now.getDate()
pointSize: Style.fontSizeXXXL * 1.5
font.weight: Style.fontWeightBold
color: Colors.mOnPrimary
}
// Month, year, location
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.bottomMargin: Style.marginXXS
Layout.topMargin: -Style.marginXXS
spacing: -Style.marginXS
RowLayout {
spacing: Style.marginS
UText {
text: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][root.currentMonth]
pointSize: Style.fontSizeXL * 1.1
font.weight: Style.fontWeightBold
color: Colors.mOnPrimary
Layout.alignment: Qt.AlignBaseline
elide: Text.ElideRight
}
UText {
text: `${root.currentYear}`
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Qt.alpha(Colors.mOnPrimary, 0.7)
Layout.alignment: Qt.AlignBaseline
}
}
RowLayout {
spacing: 0
UText {
text: {
if (!root.weatherReady)
return "Loading weather...";
const chunks = SettingsService.location.split(",");
return chunks[0];
}
pointSize: Style.fontSizeM
color: Colors.mOnPrimary
Layout.maximumWidth: 150
elide: Text.ElideRight
}
UText {
text: root.weatherReady && ` (${LocationService.data.weather.timezone_abbreviation})`
pointSize: Style.fontSizeXS
color: Qt.alpha(Colors.mOnPrimary, 0.7)
}
}
}
// Spacer
Item {
Layout.fillWidth: true
}
}
}
// Analog/Digital clock
UClock {
id: clockLoader
anchors.right: parent.right
anchors.rightMargin: Style.marginXL
anchors.verticalCenter: parent.verticalCenter
clockStyle: "analog"
progressColor: Colors.mOnPrimary
Layout.alignment: Qt.AlignVCenter
now: root.now
}
}
@@ -0,0 +1,344 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Utils
// Calendar month grid with navigation
UBox {
id: root
// Internal state - independent from header
readonly property var now: Time.now
property int calendarMonth: now.getMonth()
property int calendarYear: now.getFullYear()
readonly property var locale: Qt.locale("en")
readonly property int firstDayOfWeek: locale.firstDayOfWeek
// Helper function to calculate ISO week number
function getISOWeekNumber(date) {
const target = new Date(date.valueOf());
const dayNr = (date.getDay() + 6) % 7;
target.setDate(target.getDate() - dayNr + 3);
const firstThursday = new Date(target.getFullYear(), 0, 4);
const diff = target - firstThursday;
const oneWeek = 1000 * 60 * 60 * 24 * 7;
const weekNumber = 1 + Math.round(diff / oneWeek);
return weekNumber;
}
// Helper function to check if an event is all-day
function isAllDayEvent(event) {
const duration = event.end - event.start;
const startDate = new Date(event.start * 1000);
const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0;
return duration === 86400 && isAtMidnight;
}
// Navigation functions
function navigateToPreviousMonth() {
let newDate = new Date(root.calendarYear, root.calendarMonth - 1, 1);
root.calendarYear = newDate.getFullYear();
root.calendarMonth = newDate.getMonth();
const now = new Date();
const monthStart = new Date(root.calendarYear, root.calendarMonth, 1);
const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0);
const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000)));
const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000)));
}
function navigateToNextMonth() {
let newDate = new Date(root.calendarYear, root.calendarMonth + 1, 1);
root.calendarYear = newDate.getFullYear();
root.calendarMonth = newDate.getMonth();
const now = new Date();
const monthStart = new Date(root.calendarYear, root.calendarMonth, 1);
const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0);
const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000)));
const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000)));
}
Layout.fillWidth: true
implicitHeight: calendarContent.implicitHeight + Style.marginM * 2
compact: true
// Wheel handler for month navigation
WheelHandler {
id: wheelHandler
target: root
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function(event) {
if (event.angleDelta.y > 0) {
// Scroll up - go to previous month
root.navigateToPreviousMonth();
event.accepted = true;
} else if (event.angleDelta.y < 0) {
// Scroll down - go to next month
root.navigateToNextMonth();
event.accepted = true;
}
}
}
ColumnLayout {
id: calendarContent
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
// Navigation row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
Item {
Layout.preferredWidth: Style.marginS
}
UText {
text: locale.monthName(root.calendarMonth, Locale.LongFormat).toUpperCase() + " " + root.calendarYear
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
UDivider {
Layout.fillWidth: true
}
UIconButton {
iconName: "chevron-left"
onClicked: root.navigateToPreviousMonth()
}
UIconButton {
iconName: "calendar"
onClicked: {
root.calendarMonth = root.now.getMonth();
root.calendarYear = root.now.getFullYear();
}
}
UIconButton {
iconName: "chevron-right"
onClicked: root.navigateToNextMonth()
}
}
// Day names header
RowLayout {
Layout.fillWidth: true
spacing: 0
Item {
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
}
GridLayout {
Layout.fillWidth: true
columns: 7
rows: 1
columnSpacing: 0
rowSpacing: 0
Repeater {
model: 7
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.fontSizeS * 2
UText {
anchors.centerIn: parent
text: {
let dayIndex = (root.firstDayOfWeek + index) % 7;
const dayName = locale.dayName(dayIndex, Locale.ShortFormat);
return dayName.substring(0, 2).toUpperCase();
}
color: Colors.mPrimary
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
// Calendar grid with week numbers
RowLayout {
Layout.fillWidth: true
spacing: 0
// Week numbers column
ColumnLayout {
property var weekNumbers: {
if (!grid.daysModel || grid.daysModel.length === 0)
return [];
const weeks = [];
const numWeeks = Math.ceil(grid.daysModel.length / 7);
for (var i = 0; i < numWeeks; i++) {
const dayIndex = i * 7;
if (dayIndex < grid.daysModel.length) {
const weekDay = grid.daysModel[dayIndex];
const date = new Date(weekDay.year, weekDay.month, weekDay.day);
let thursday = new Date(date);
if (root.firstDayOfWeek === 0) {
thursday.setDate(date.getDate() + 4);
} else if (root.firstDayOfWeek === 1) {
thursday.setDate(date.getDate() + 3);
} else {
let daysToThursday = (4 - root.firstDayOfWeek + 7) % 7;
thursday.setDate(date.getDate() + daysToThursday);
}
weeks.push(root.getISOWeekNumber(thursday));
}
}
return weeks;
}
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignTop
spacing: Style.marginXXS
Repeater {
model: parent.weekNumbers
Item {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.9
UText {
anchors.centerIn: parent
color: Qt.alpha(Colors.mPrimary, 0.7)
pointSize: Style.fontSizeXXS
text: modelData
}
}
}
}
// Calendar grid
GridLayout {
id: grid
property int month: root.calendarMonth
property int year: root.calendarYear
property var daysModel: {
const firstOfMonth = new Date(year, month, 1);
const lastOfMonth = new Date(year, month + 1, 0);
const daysInMonth = lastOfMonth.getDate();
const firstDayOfWeek = root.firstDayOfWeek;
const firstOfMonthDayOfWeek = firstOfMonth.getDay();
let daysBefore = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7;
const lastOfMonthDayOfWeek = lastOfMonth.getDay();
const daysAfter = (firstDayOfWeek - lastOfMonthDayOfWeek - 1 + 7) % 7;
const days = [];
const today = new Date();
// Previous month days
const prevMonth = new Date(year, month, 0);
const prevMonthDays = prevMonth.getDate();
for (var i = daysBefore - 1; i >= 0; i--) {
const day = prevMonthDays - i;
days.push({
"day": day,
"month": month - 1,
"year": month === 0 ? year - 1 : year,
"today": false,
"currentMonth": false
});
}
// Current month days
for (var day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate();
days.push({
"day": day,
"month": month,
"year": year,
"today": isToday,
"currentMonth": true
});
}
// Next month days
for (var i = 1; i <= daysAfter; i++) {
days.push({
"day": i,
"month": month + 1,
"year": month === 11 ? year + 1 : year,
"today": false,
"currentMonth": false
});
}
return days;
}
Layout.fillWidth: true
columns: 7
columnSpacing: Style.marginXXS
rowSpacing: Style.marginXXS
Repeater {
model: grid.daysModel
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 0.9
Rectangle {
width: Style.baseWidgetSize * 0.9
height: Style.baseWidgetSize * 0.9
anchors.centerIn: parent
radius: Style.radiusM
color: modelData.today ? Colors.mPrimary : "transparent"
UText {
anchors.centerIn: parent
text: modelData.day
color: {
if (modelData.today)
return Colors.mOnPrimary;
if (modelData.currentMonth)
return Colors.mOnSurface;
return Colors.mOnSurfaceVariant;
}
opacity: modelData.currentMonth ? 1 : 0.4
pointSize: Style.fontSizeM
font.weight: modelData.today ? Style.fontWeightBold : Style.fontWeightMedium
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,211 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Services
UBox {
id: root
property string currentPanel: "bluetooth" // "bluetooth", "wifi"
implicitHeight: contentLoader.implicitHeight + toggleGroup.implicitHeight + Style.marginXS * 2 + Style.marginS * 2
ColumnLayout {
spacing: Style.marginXS
anchors.fill: parent
anchors.margins: Style.marginS
RowLayout {
Layout.fillWidth: true
Rectangle {
id: toggleGroup
Layout.preferredWidth: Style.baseWidgetSize * 2.8
Layout.preferredHeight: Style.baseWidgetSize
radius: Math.min(Style.radiusS, height / 2)
color: Colors.mSurface
// border.color: Colors.mOutline
Row {
anchors.fill: parent
spacing: Style.marginS / 2
Rectangle {
id: btnBluetooth
width: root.currentPanel === "bluetooth" ? (parent.width - parent.spacing) * 0.65 : (parent.width - parent.spacing) * 0.35
height: parent.height
radius: Math.min(Style.radiusS, height / 2)
color: root.currentPanel === "bluetooth" ? Colors.mPrimary : "transparent"
Behavior on width {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
UIcon {
anchors.centerIn: parent
iconName: "bluetooth"
iconSize: Style.fontSizeL
color: root.currentPanel === "bluetooth" ? Colors.mOnPrimary : Colors.mOnSurface
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
MouseArea {
anchors.fill: parent
onClicked: root.currentPanel = "bluetooth"
cursorShape: Qt.PointingHandCursor
}
}
Rectangle {
id: btnWifi
width: root.currentPanel === "wifi" ? (parent.width - parent.spacing) * 0.65 : (parent.width - parent.spacing) * 0.35
height: parent.height
radius: Math.min(Style.radiusS, height / 2)
color: root.currentPanel === "wifi" ? Colors.mPrimary : "transparent"
Behavior on width {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
UIcon {
anchors.centerIn: parent
iconName: "wifi"
iconSize: Style.fontSizeL
color: root.currentPanel === "wifi" ? Colors.mOnPrimary : Colors.mOnSurface
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
MouseArea {
anchors.fill: parent
onClicked: root.currentPanel = "wifi"
cursorShape: Qt.PointingHandCursor
}
}
}
}
Item {
Layout.fillWidth: true
}
Loader {
sourceComponent: currentPanel === "bluetooth" ? bluetoothHeaderComponent : wifiHeaderComponent
Component {
id: bluetoothHeaderComponent
RowLayout {
UToggle {
id: bluetoothSwitch
checked: BluetoothService.enabled
onToggled: (checked) => {
return BluetoothService.setBluetoothEnabled(checked);
}
baseSize: Style.baseWidgetSize * 0.65
}
UIconButton {
enabled: BluetoothService.enabled
iconName: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
}
colorFg: Colors.mGreen
}
}
}
Component {
id: wifiHeaderComponent
RowLayout {
UToggle {
id: wifiSwitch
checked: SettingsService.wifiEnabled
onToggled: (checked) => {
return NetworkService.setWifiEnabled(checked);
}
baseSize: Style.baseWidgetSize * 0.65
}
UIconButton {
iconName: "refresh"
baseSize: Style.baseWidgetSize * 0.8
enabled: SettingsService.wifiEnabled && !NetworkService.scanning
onClicked: NetworkService.scan()
colorFg: Colors.mGreen
}
}
}
}
}
UDivider {
Layout.fillWidth: true
}
Loader {
id: contentLoader
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: currentPanel === "bluetooth" ? bluetoothComponent : wifiComponent
Component {
id: bluetoothComponent
BluetoothCard {
anchors.fill: parent
anchors.margins: Style.marginS
}
}
Component {
id: wifiComponent
WifiCard {
anchors.fill: parent
anchors.margins: Style.marginS
}
}
}
}
}
@@ -1,12 +1,12 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Noctalia
import qs.Services
import qs.Utils
NBox {
UBox {
id: lyricsBox
Component.onCompleted: {
@@ -25,13 +25,13 @@ NBox {
Repeater {
model: LyricsService.lyrics
NText {
UText {
Layout.fillWidth: true
text: modelData
font.pointSize: index === LyricsService.currentIndex ? Style.fontSizeM : Style.fontSizeS
font.pointSize: index === LyricsService.currentIndex ? Style.fontSizeS : Style.fontSizeXS
font.weight: index === LyricsService.currentIndex ? Style.fontWeightBold : Style.fontWeightRegular
font.family: Fonts.sans
color: index === LyricsService.currentIndex ? Color.mOnSurface : Color.mOnSurfaceVariant
color: index === LyricsService.currentIndex ? Colors.mOnSurface : Colors.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
@@ -0,0 +1,111 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Services
ColumnLayout {
UText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: buttonsGrid.width
Layout.bottomMargin: Style.marginS
horizontalAlignment: Text.AlignHCenter
text: (LyricsService.offset > 0 ? "+" + LyricsService.offset : LyricsService.offset) + " ms"
}
GridLayout {
id: buttonsGrid
columns: 2
columnSpacing: Style.marginS
rowSpacing: Style.marginS
UIconButton {
id: slowerButton
baseSize: 32
colorFg: Colors.mCyan
iconName: "arrow-bar-up"
onClicked: {
LyricsService.increaseOffset();
}
}
UIconButton {
id: playPauseButton
baseSize: 32
colorFg: Colors.mPurple
iconName: "arrow-bar-down"
onClicked: {
LyricsService.decreaseOffset();
}
}
UIconButton {
id: nextButton
baseSize: 32
colorFg: Colors.mGreen
iconName: "rotate-clockwise"
onClicked: {
LyricsService.resetOffset();
}
}
UIconButton {
id: fasterButton
baseSize: 32
colorFg: Colors.mRed
iconName: "trash"
onClicked: {
LyricsService.clearCache();
}
}
UIconButton {
id: barLyricsButton
baseSize: 32
colorFg: Colors.mSky
alwaysHover: LyricsService.showLyricsBar
iconName: "app-window"
onClicked: {
LyricsService.toggleLyricsBar();
}
}
UIconButton {
id: textButton
baseSize: 32
colorFg: Colors.mYellow
iconName: "align-box-left-bottom"
onClicked: {
LyricsService.showLyricsText();
controlCenterPanel.close();
}
}
UIconButton {
baseSize: 32
colorFg: Colors.mOrange
alwaysHover: SunsetService.isEnabled
iconName: "sunset-2"
onClicked: SunsetService.toggleSunset()
}
UIconButton {
baseSize: 32
colorFg: Colors.mBlue
alwaysHover: MediaService.autoSwitchingPaused
iconName: "lock-square"
onClicked: MediaService.toggleAutoSwitchingPaused()
}
}
}
@@ -0,0 +1,483 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Services
import qs.Utils
UBox {
id: root
implicitHeight: 200
// Track whether we have an active media player
readonly property bool hasActivePlayer: MediaService.currentPlayer && MediaService.canPlay
// Wrapper - rounded rect clipper
Item {
anchors.fill: parent
layer.enabled: true
layer.smooth: true
// Solid color background (always present as base layer)
Rectangle {
anchors.fill: parent
color: Colors.mSurface
}
// Background image that covers everything
Image {
id: bgImage
readonly property int dim: 256
anchors.fill: parent
visible: source.toString() !== ""
source: MediaService.trackArtUrl
sourceSize: Qt.size(dim, dim)
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.smooth: true
layer.effect: MultiEffect {
blurEnabled: true
blurMax: 8
blur: 0.33
}
}
// Dark overlay for readability
Rectangle {
anchors.fill: parent
color: Colors.mSurface
opacity: 0.65
radius: Style.radiusM
}
// Background visualizer on top of the artwork
Item {
id: visualizerContainer
anchors.fill: parent
layer.enabled: true
Item {
anchors.fill: parent
Cava {
id: cava
count: 32
}
Repeater {
model: cava.values
Rectangle {
anchors.bottom: parent.bottom
width: (parent.width - (cava.count - 1) * Style.marginXS) / cava.count
height: modelData * parent.height
x: index * (width + Style.marginXS)
color: Colors.mPrimary
radius: width / 2
opacity: 0.25
}
}
}
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: root.width
height: root.height
radius: Style.radiusM
color: "white"
}
}
}
}
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.95
maskSpreadAtMin: 0.15
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: root.width
height: root.height
radius: Style.radiusM
color: "white"
}
}
}
}
// Player selector
Rectangle {
id: playerSelectorButton
property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex]
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Style.marginXS
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
height: Style.baseWidgetSize
visible: MediaService.getAvailablePlayers().length > 1
radius: Style.radiusM
color: "transparent"
RowLayout {
anchors.fill: parent
spacing: Style.marginS
UIcon {
iconName: "caret-down"
iconSize: Style.fontSizeXXL
color: Colors.mOnSurfaceVariant
}
UText {
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
pointSize: Style.fontSizeXS
color: Colors.mOnSurfaceVariant
Layout.fillWidth: true
}
}
MouseArea {
id: playerSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
var menuItems = [];
var players = MediaService.getAvailablePlayers();
for (var i = 0; i < players.length; i++) {
menuItems.push({
"label": players[i].identity,
"action": i.toString(),
"icon": "disc",
"enabled": true,
"visible": true
});
}
playerContextMenu.model = menuItems;
playerContextMenu.openAtItem(playerSelectorButton, 0, playerSelectorButton.height);
}
}
UContextMenu {
id: playerContextMenu
parent: root
width: 200
verticalPolicy: ScrollBar.AlwaysOff
onTriggered: function(action) {
var index = parseInt(action);
if (!isNaN(index))
MediaService.switchToPlayer(index);
}
}
}
// Content container that adjusts for player selector
Item {
anchors.fill: parent
anchors.topMargin: playerSelectorButton.visible ? (playerSelectorButton.height + Style.marginXS + Style.marginM) : Style.marginM
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM
anchors.bottomMargin: Style.marginM
Item {
id: fallback
visible: !root.hasActivePlayer
anchors.fill: parent
Item {
anchors.centerIn: parent
implicitWidth: Style.fontSizeXXXL * 4
implicitHeight: Style.fontSizeXXXL * 4
// Pulsating audio circles (background)
Repeater {
model: 3
Rectangle {
anchors.centerIn: parent
width: parent.width * (1 + index * 0.2)
height: width
radius: width / 2
color: "transparent"
border.color: Colors.mOnSurfaceVariant
border.width: 2
opacity: 0
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
PauseAnimation {
duration: index * 600
}
NumberAnimation {
from: 1
to: 0
duration: 2000
easing.type: Easing.OutQuad
}
}
SequentialAnimation on scale {
running: true
loops: Animation.Infinite
PauseAnimation {
duration: index * 600
}
NumberAnimation {
from: 0.5
to: 1.2
duration: 2000
easing.type: Easing.OutQuad
}
}
}
}
// Spinning disc
UIcon {
anchors.centerIn: parent
iconName: "disc"
iconSize: Style.fontSizeXXXL * 3
color: Colors.mOnSurfaceVariant
RotationAnimator on rotation {
from: 0
to: 360
duration: 8000
loops: Animation.Infinite
running: true
}
}
}
}
// MediaPlayer Main Content - use Loader for performance
Loader {
id: mainLoader
anchors.fill: parent
active: root.hasActivePlayer
sourceComponent: Item {
Layout.fillWidth: true
Layout.fillHeight: true
// Exceptionaly we put shadow on text and controls to ease readability
UDropShadow {
anchors.fill: main
source: main
autoPaddingEnabled: true
shadowBlur: 1
shadowOpacity: 0.9
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
shadowColor: "black"
}
ColumnLayout {
id: main
anchors.fill: parent
spacing: Style.marginS
// Metadata
ColumnLayout {
id: metadata
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
spacing: Style.marginXS
UText {
visible: MediaService.trackTitle !== ""
text: MediaService.trackTitle
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
Layout.fillWidth: true
}
UText {
visible: MediaService.trackArtist !== ""
text: MediaService.trackArtist
color: Colors.mPrimary
pointSize: Style.fontSizeXS
elide: Text.ElideRight
Layout.fillWidth: true
}
UText {
visible: MediaService.trackAlbum !== ""
text: MediaService.trackAlbum
color: Colors.mOnSurfaceVariant
pointSize: Style.fontSizeS
elide: Text.ElideRight
Layout.fillWidth: true
}
}
// Progress slider
Item {
id: progressWrapper
property real localSeekRatio: -1
property real lastSentSeekRatio: -1
property real seekEpsilon: 0.01
property real progressRatio: {
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
return 0;
const r = MediaService.currentPosition / MediaService.trackLength;
if (isNaN(r) || !isFinite(r))
return 0;
return Math.max(0, Math.min(1, r));
}
property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
Layout.fillWidth: true
height: Style.baseWidgetSize * 0.5
Timer {
id: seekDebounce
interval: 75
repeat: false
onTriggered: {
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio));
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MediaService.seekByRatio(next);
progressWrapper.lastSentSeekRatio = next;
}
}
}
}
USlider {
id: progressSlider
anchors.fill: parent
from: 0
to: 1
stepSize: 0
snapAlways: false
enabled: MediaService.trackLength > 0 && MediaService.canSeek
heightRatio: 0.6
onMoved: {
progressWrapper.localSeekRatio = value;
seekDebounce.restart();
}
onPressedChanged: {
if (pressed) {
MediaService.isSeeking = true;
progressWrapper.localSeekRatio = value;
MediaService.seekByRatio(value);
progressWrapper.lastSentSeekRatio = value;
} else {
seekDebounce.stop();
MediaService.seekByRatio(value);
MediaService.isSeeking = false;
progressWrapper.localSeekRatio = -1;
progressWrapper.lastSentSeekRatio = -1;
}
}
}
Binding {
target: progressSlider
property: "value"
value: progressWrapper.progressRatio
when: !MediaService.isSeeking
}
}
// Media controls
RowLayout {
spacing: Style.marginS
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
UIconButton {
iconName: "media-prev"
visible: MediaService.canGoPrevious
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {
}
}
UIconButton {
iconName: MediaService.isPlaying ? "media-pause" : "media-play"
visible: (MediaService.canPlay || MediaService.canPause)
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {
}
}
UIconButton {
iconName: "media-next"
visible: MediaService.canGoNext
onClicked: MediaService.canGoNext ? MediaService.next() : {
}
}
}
}
}
}
}
}
@@ -0,0 +1,939 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import Quickshell.Wayland
import qs.Components
import qs.Constants
import qs.Services
import qs.Utils
// Notification History panel
Rectangle {
// Check if below visible area
// Stop at edges
id: root
// Calculate content height based on header + tabs (if visible) + content
property real calculatedHeight: {
if (NotificationService.historyList.count === 0)
return headerBox.implicitHeight + scrollView.implicitHeight + Style.marginL * 2 + Style.marginM;
return headerBox.implicitHeight + scrollView.implicitHeight + Style.marginL * 2 + Style.marginM;
}
property real contentPreferredHeight: Math.min(root.height, Math.ceil(calculatedHeight))
property real layoutWidth: Math.max(1, root.width - Style.marginL * 2)
// State (lazy-loaded with root)
property var rangeCounts: [0, 0, 0, 0]
property var lastKnownDate: null // Track the current date to detect day changes
// UI state (lazy-loaded with root)
// 0 = All, 1 = Today, 2 = Yesterday, 3 = Earlier
property int currentRange: 1
// start on Today by default
property bool groupByDate: true
// Keyboard navigation state
property int focusIndex: -1
property int actionIndex: -1 // For actions within a notification
function parseActions(actions) {
try {
return JSON.parse(actions || "[]");
} catch (e) {
return [];
}
}
function moveSelection(dir) {
var m = NotificationService.historyList;
if (!m || m.count === 0)
return ;
var newIndex = focusIndex;
var found = false;
var count = m.count;
// If no selection yet, start from beginning (or end if up)
if (focusIndex === -1) {
if (dir > 0)
newIndex = -1;
else
newIndex = count;
}
// Loop to find next visible item
var loopCount = 0;
while (loopCount < count) {
newIndex += dir;
// Bounds check
if (newIndex < 0 || newIndex >= count)
break;
var item = m.get(newIndex);
if (item && isInCurrentRange(item.timestamp)) {
found = true;
break;
}
loopCount++;
}
if (found) {
focusIndex = newIndex;
actionIndex = -1; // Reset action selection
scrollToItem(focusIndex);
}
}
function moveAction(dir) {
if (focusIndex === -1)
return ;
var item = NotificationService.historyList.get(focusIndex);
if (!item)
return ;
var actions = parseActions(item.actionsJson);
if (actions.length === 0)
return ;
var newActionIndex = actionIndex + dir;
// Clamp between -1 (body) and actions.length - 1
if (newActionIndex < -1)
newActionIndex = -1;
if (newActionIndex >= actions.length)
newActionIndex = actions.length - 1;
actionIndex = newActionIndex;
}
function activateSelection() {
if (focusIndex === -1)
return ;
var item = NotificationService.historyList.get(focusIndex);
if (!item)
return ;
if (actionIndex >= 0) {
var actions = parseActions(item.actionsJson);
if (actionIndex < actions.length)
NotificationService.invokeAction(item.id, actions[actionIndex].identifier);
} else {
var delegate = notificationColumn.children[focusIndex];
if (!delegate)
return ;
if (!(delegate.canExpand || delegate.isExpanded))
return ;
if (scrollView.expandedId === item.id)
scrollView.expandedId = "";
else
scrollView.expandedId = item.id;
}
}
function removeSelection() {
if (focusIndex === -1)
return ;
var item = NotificationService.historyList.get(focusIndex);
if (!item)
return ;
NotificationService.removeFromHistory(item.id);
}
function scrollToItem(index) {
// Find the delegate item
if (index < 0 || index >= notificationColumn.children.length)
return ;
var item = notificationColumn.children[index];
if (item && item.visible) {
// Use the internal flickable from NScrollView for accurate scrolling
var flickable = scrollView._internalFlickable;
if (!flickable || !flickable.contentItem)
return ;
var pos = flickable.contentItem.mapFromItem(item, 0, 0);
var itemY = pos.y;
var itemHeight = item.height;
var currentContentY = flickable.contentY;
var viewHeight = flickable.height;
// Check if above visible area
if (itemY < currentContentY)
flickable.contentY = Math.max(0, itemY - Style.marginM);
else if (itemY + itemHeight > currentContentY + viewHeight)
flickable.contentY = (itemY + itemHeight) - viewHeight + Style.marginM;
}
}
function resetFocus() {
focusIndex = -1;
actionIndex = -1;
}
// Helper functions (lazy-loaded with root)
function dateOnly(d) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function getDateKey(d) {
// Returns a string key for the date (YYYY-MM-DD) for comparison
var date = dateOnly(d);
return date.getFullYear() + "-" + date.getMonth() + "-" + date.getDate();
}
function rangeForTimestamp(ts) {
var dt = new Date(ts);
var today = dateOnly(new Date());
var thatDay = dateOnly(dt);
var diffMs = today - thatDay;
var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0)
return 0;
if (diffDays === 1)
return 1;
return 2;
}
function recalcRangeCounts() {
var m = NotificationService.historyList;
if (!m || typeof m.count === "undefined" || m.count <= 0) {
root.rangeCounts = [0, 0, 0, 0];
return ;
}
var counts = [0, 0, 0, 0];
counts[0] = m.count;
for (var i = 0; i < m.count; ++i) {
var item = m.get(i);
if (!item || typeof item.timestamp === "undefined")
continue;
var r = rangeForTimestamp(item.timestamp);
counts[r + 1] = counts[r + 1] + 1;
}
root.rangeCounts = counts;
}
function isInCurrentRange(ts) {
if (currentRange === 0)
return true;
return rangeForTimestamp(ts) === (currentRange - 1);
}
function countForRange(range) {
return rangeCounts[range] || 0;
}
function hasNotificationsInCurrentRange() {
var m = NotificationService.historyList;
if (!m || m.count === 0)
return false;
for (var i = 0; i < m.count; ++i) {
var item = m.get(i);
if (item && isInCurrentRange(item.timestamp))
return true;
}
return false;
}
color: "transparent"
onCurrentRangeChanged: resetFocus()
Component.onCompleted: {
NotificationService.updateLastSeenTs();
recalcRangeCounts();
// Initialize lastKnownDate
lastKnownDate = getDateKey(new Date());
}
Connections {
function onCountChanged() {
root.recalcRangeCounts();
}
target: NotificationService.historyList
}
// Timer to check for day changes at midnight
Timer {
// Day has changed, recalculate counts
id: dayChangeTimer
interval: 60000 // Check every minute
repeat: true
running: true // Always runs when root exists (panel is open)
onTriggered: {
var currentDateKey = root.getDateKey(new Date());
if (root.lastKnownDate !== null && root.lastKnownDate !== currentDateKey)
root.recalcRangeCounts();
root.lastKnownDate = currentDateKey;
}
}
ColumnLayout {
id: mainColumn
anchors.fill: parent
spacing: Style.marginM
// Header section
UBox {
id: headerBox
Layout.fillWidth: true
implicitHeight: header.implicitHeight + Style.marginM * 2
ColumnLayout {
id: header
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
RowLayout {
id: headerRow
Layout.fillWidth: true
UIcon {
iconName: "bell"
iconSize: Style.fontSizeXXL
color: Colors.mPrimary
}
UText {
text: "Notifications" + " (" + root.countForRange(tabsBox.currentIndex) + ")"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
Layout.fillWidth: true
}
UIconButton {
iconName: NotificationService.doNotDisturb ? "bell-off" : "bell"
baseSize: Style.baseWidgetSize * 0.8
colorFg: Colors.mGreen
alwaysHover: NotificationService.doNotDisturb
onClicked: NotificationService.toggleDoNotDisturb()
}
UIconButton {
// Close panel as there is nothing more to see.
iconName: "trash"
baseSize: Style.baseWidgetSize * 0.8
colorFg: Colors.mError
onClicked: {
NotificationService.clearHistory();
}
}
}
// Time range tabs ([All] / [Today] / [Yesterday] / [Earlier])
UTabBar {
id: tabsBox
Layout.fillWidth: true
visible: NotificationService.historyList.count > 0 && root.groupByDate
currentIndex: root.currentRange
tabHeight: Math.round(Style.baseWidgetSize * 0.8)
spacing: Style.marginXS
distributeEvenly: true
UTabButton {
tabIndex: 0
text: "All"
checked: tabsBox.currentIndex === 0
onClicked: root.currentRange = 0
pointSize: Style.fontSizeXS
}
UTabButton {
tabIndex: 1
text: "Today"
checked: tabsBox.currentIndex === 1
onClicked: root.currentRange = 1
pointSize: Style.fontSizeXS
}
UTabButton {
tabIndex: 2
text: "Yesterday"
checked: tabsBox.currentIndex === 2
onClicked: root.currentRange = 2
pointSize: Style.fontSizeXS
}
UTabButton {
tabIndex: 3
text: "Earlier"
checked: tabsBox.currentIndex === 3
onClicked: root.currentRange = 3
pointSize: Style.fontSizeXS
}
}
}
}
// Notification list container with gradient overlay
Item {
Layout.fillWidth: true
Layout.fillHeight: true
NScrollView {
id: scrollView
// Track which notification is expanded
property string expandedId: ""
anchors.fill: parent
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
reserveScrollbarSpace: false
gradientColor: Colors.mSurface
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM
// Empty state when no notifications
UBox {
visible: !root.hasNotificationsInCurrentRange()
Layout.fillWidth: true
Layout.preferredHeight: emptyState.implicitHeight + Style.marginM * 2
ColumnLayout {
id: emptyState
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
UIcon {
iconName: "bell-off"
iconSize: (NotificationService.historyList.count === 0) ? 48 : Style.baseWidgetSize
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UText {
text: "No notifications"
pointSize: (NotificationService.historyList.count === 0) ? Style.fontSizeL : Style.fontSizeM
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UText {
visible: NotificationService.historyList.count === 0
text: "No notifications in this range"
pointSize: Style.fontSizeS
color: Colors.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
Item {
Layout.fillHeight: true
}
}
}
// Notification list container
Item {
visible: root.hasNotificationsInCurrentRange()
Layout.fillWidth: true
Layout.preferredHeight: notificationColumn.implicitHeight
Column {
id: notificationColumn
anchors.fill: parent
spacing: Style.marginS
Repeater {
model: NotificationService.historyList
delegate: Item {
id: notificationDelegate
property int listIndex: index
property string notificationId: model.id
property string appName: model.appName || ""
property bool isExpanded: scrollView.expandedId === notificationId
property bool canExpand: summaryText.truncated || bodyText.truncated
property real swipeOffset: 0
property real pressGlobalX: 0
property real pressGlobalY: 0
property bool isSwiping: false
property bool isRemoving: false
property string pendingLink: ""
readonly property real swipeStartThreshold: 16
readonly property real swipeDismissThreshold: Math.max(110, width * 0.3)
readonly property int removeAnimationDuration: Style.animationNormal
readonly property int notificationTextFormat: notificationDelegate.isExpanded ? Text.MarkdownText : Text.StyledText
readonly property real actionButtonSize: Style.baseWidgetSize * 0.7
readonly property real buttonClusterWidth: notificationDelegate.actionButtonSize * 2 + Style.marginXS
readonly property real iconSize: 40
// Parse actions safely
property var actionsList: parseActions(model.actionsJson)
property bool isFocused: index === root.focusIndex
function isSafeLink(link) {
if (!link)
return false;
const lower = link.toLowerCase();
const schemes = ["http://", "https://", "mailto:"];
return schemes.some((scheme) => {
return lower.startsWith(scheme);
});
}
function linkAtPoint(x, y) {
if (!notificationDelegate.isExpanded)
return "";
if (summaryText) {
const summaryPoint = summaryText.mapFromItem(historyInteractionArea, x, y);
if (summaryPoint.x >= 0 && summaryPoint.y >= 0 && summaryPoint.x <= summaryText.width && summaryPoint.y <= summaryText.height) {
const summaryLink = summaryText.linkAt ? summaryText.linkAt(summaryPoint.x, summaryPoint.y) : "";
if (isSafeLink(summaryLink))
return summaryLink;
}
}
if (bodyText) {
const bodyPoint = bodyText.mapFromItem(historyInteractionArea, x, y);
if (bodyPoint.x >= 0 && bodyPoint.y >= 0 && bodyPoint.x <= bodyText.width && bodyPoint.y <= bodyText.height) {
const bodyLink = bodyText.linkAt ? bodyText.linkAt(bodyPoint.x, bodyPoint.y) : "";
if (isSafeLink(bodyLink))
return bodyLink;
}
}
return "";
}
function updateCursorAt(x, y) {
if (notificationDelegate.isExpanded && notificationDelegate.linkAtPoint(x, y))
historyInteractionArea.cursorShape = Qt.PointingHandCursor;
else
historyInteractionArea.cursorShape = Qt.ArrowCursor;
}
function dismissBySwipe() {
if (isRemoving)
return ;
isRemoving = true;
isSwiping = false;
swipeOffset = swipeOffset >= 0 ? width + Style.marginL : -width - Style.marginL;
opacity = 0;
removeTimer.restart();
}
width: parent.width
visible: root.isInCurrentRange(model.timestamp)
height: visible && !isRemoving ? contentColumn.height + Style.marginM * 2 : 0
onVisibleChanged: {
if (!visible) {
notificationDelegate.isSwiping = false;
notificationDelegate.swipeOffset = 0;
notificationDelegate.opacity = 1;
notificationDelegate.isRemoving = false;
removeTimer.stop();
}
}
Component.onDestruction: removeTimer.stop()
Timer {
id: removeTimer
interval: notificationDelegate.removeAnimationDuration
repeat: false
onTriggered: NotificationService.removeFromHistory(notificationId)
}
Rectangle {
// return "transparent";
anchors.fill: parent
radius: Style.radiusM
color: Colors.mSurfaceVariant
border.color: {
if (notificationDelegate.isFocused)
return Colors.mPrimary;
return Qt.alpha(Colors.mOutline, Style.opacityHeavy);
}
border.width: notificationDelegate.isFocused ? Style.borderM : Style.borderS
Behavior on color {
enabled: true
ColorAnimation {
duration: Style.animationFast
}
}
}
// Click to expand/collapse
MouseArea {
id: historyInteractionArea
anchors.fill: parent
anchors.rightMargin: notificationDelegate.buttonClusterWidth + Style.marginM
enabled: !notificationDelegate.isRemoving
hoverEnabled: true
cursorShape: Qt.ArrowCursor
onPressed: (mouse) => {
root.focusIndex = index;
root.actionIndex = -1;
if (notificationDelegate.isExpanded) {
const link = notificationDelegate.linkAtPoint(mouse.x, mouse.y);
if (link)
notificationDelegate.pendingLink = link;
else
notificationDelegate.pendingLink = "";
}
if (mouse.button !== Qt.LeftButton)
return ;
const globalPoint = historyInteractionArea.mapToGlobal(mouse.x, mouse.y);
notificationDelegate.pressGlobalX = globalPoint.x;
notificationDelegate.pressGlobalY = globalPoint.y;
notificationDelegate.isSwiping = false;
}
onPositionChanged: (mouse) => {
if (!(mouse.buttons & Qt.LeftButton) || notificationDelegate.isRemoving)
return ;
const globalPoint = historyInteractionArea.mapToGlobal(mouse.x, mouse.y);
const deltaX = globalPoint.x - notificationDelegate.pressGlobalX;
const deltaY = globalPoint.y - notificationDelegate.pressGlobalY;
if (!notificationDelegate.isSwiping) {
if (Math.abs(deltaX) < notificationDelegate.swipeStartThreshold)
return ;
// Only start a swipe-dismiss when horizontal movement is dominant.
if (Math.abs(deltaX) <= Math.abs(deltaY) * 1.15)
return ;
notificationDelegate.isSwiping = true;
}
if (notificationDelegate.pendingLink && Math.abs(deltaX) >= notificationDelegate.swipeStartThreshold)
notificationDelegate.pendingLink = "";
notificationDelegate.swipeOffset = deltaX;
}
onReleased: (mouse) => {
if (mouse.button !== Qt.LeftButton)
return ;
if (notificationDelegate.isSwiping) {
if (Math.abs(notificationDelegate.swipeOffset) >= notificationDelegate.swipeDismissThreshold)
notificationDelegate.dismissBySwipe();
else
notificationDelegate.swipeOffset = 0;
notificationDelegate.isSwiping = false;
notificationDelegate.pendingLink = "";
return ;
}
if (notificationDelegate.pendingLink) {
Qt.openUrlExternally(notificationDelegate.pendingLink);
notificationDelegate.pendingLink = "";
return ;
}
// Focus sender window (and invoke default action if available)
var actions = notificationDelegate.actionsList;
var hasDefault = actions.some(function(a) {
return a.identifier === "default";
});
if (hasDefault) {
NotificationService.focusSenderWindow(notificationDelegate.appName);
NotificationService.invokeAction(notificationDelegate.notificationId, "default");
} else {
NotificationService.focusSenderWindow(notificationDelegate.appName);
}
}
onCanceled: {
notificationDelegate.isSwiping = false;
notificationDelegate.swipeOffset = 0;
notificationDelegate.pendingLink = "";
historyInteractionArea.cursorShape = Qt.ArrowCursor;
}
}
HoverHandler {
target: historyInteractionArea
onPointChanged: notificationDelegate.updateCursorAt(point.position.x, point.position.y)
onActiveChanged: {
if (!active)
historyInteractionArea.cursorShape = Qt.ArrowCursor;
}
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginM
spacing: Style.marginM
Row {
width: parent.width
spacing: Style.marginM
// Icon
UImageRounded {
anchors.verticalCenter: notificationDelegate.isExpanded ? undefined : parent.verticalCenter
width: notificationDelegate.iconSize
height: notificationDelegate.iconSize
radius: Math.min(Style.radiusL, width / 2)
imagePath: model.cachedImage || model.originalImage || ""
borderColor: "transparent"
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24
}
// Content
Column {
width: parent.width - notificationDelegate.iconSize - notificationDelegate.buttonClusterWidth - Style.marginM * 2
spacing: Style.marginXS
// Header row with app name and timestamp
Row {
width: parent.width
spacing: Style.marginS
// Urgency indicator
Rectangle {
width: 6
height: 6
anchors.verticalCenter: parent.verticalCenter
radius: 3
visible: model.urgency !== 1
color: {
if (model.urgency === 2)
return Colors.mError;
else if (model.urgency === 0)
return Colors.mOnSurfaceVariant;
else
return "transparent";
}
}
UText {
text: model.appName || "Unknown App"
pointSize: Style.fontSizeXS
font.weight: Style.fontWeightBold
color: Colors.mPrimary
}
UText {
textFormat: Text.PlainText
text: " " + Time.formatRelativeTime(model.timestamp)
pointSize: Style.fontSizeXXS
color: Colors.mOnSurfaceVariant
anchors.bottom: parent.bottom
}
}
// Summary
UText {
id: summaryText
width: parent.width
text: notificationDelegate.isExpanded ? (model.summaryMarkdown || I18n.tr("common.no-summary")) : (model.summary || I18n.tr("common.no-summary"))
pointSize: Style.fontSizeM
color: Colors.mOnSurface
textFormat: notificationDelegate.notificationTextFormat
wrapMode: Text.Wrap
maximumLineCount: notificationDelegate.isExpanded ? 999 : 2
elide: Text.ElideRight
}
// Body
UText {
id: bodyText
width: parent.width
text: notificationDelegate.isExpanded ? (model.bodyMarkdown || "") : (model.body || "")
pointSize: Style.fontSizeS
color: Colors.mOnSurfaceVariant
textFormat: notificationDelegate.notificationTextFormat
wrapMode: Text.Wrap
maximumLineCount: notificationDelegate.isExpanded ? 999 : 3
elide: Text.ElideRight
visible: text.length > 0
}
// Actions Flow
Flow {
width: parent.width
spacing: Style.marginS
visible: notificationDelegate.actionsList.length > 0
Repeater {
model: notificationDelegate.actionsList
delegate: UButton {
readonly property bool actionNavActive: notificationDelegate.isFocused && root.actionIndex !== -1
readonly property bool isSelected: actionNavActive && root.actionIndex === index
// Capture modelData in a property to avoid reference errors
property var actionData: modelData
text: modelData.text
fontSize: Style.fontSizeS
backgroundColor: isSelected ? Colors.mSecondary : Colors.mPrimary
textColor: isSelected ? Colors.mOnSecondary : Colors.mOnPrimary
outlined: false
implicitHeight: 24
onHoveredChanged: {
if (hovered)
root.focusIndex = notificationDelegate.listIndex;
}
onClicked: {
NotificationService.focusSenderWindow(notificationDelegate.appName);
NotificationService.invokeAction(notificationDelegate.notificationId, actionData.identifier);
}
}
}
}
}
Item {
width: notificationDelegate.buttonClusterWidth
height: notificationDelegate.actionButtonSize
Row {
anchors.right: parent.right
spacing: Style.marginXS
UIconButton {
id: expandButton
iconName: notificationDelegate.isExpanded ? "chevron-up" : "chevron-down"
baseSize: notificationDelegate.actionButtonSize
opacity: (notificationDelegate.canExpand || notificationDelegate.isExpanded) ? 1 : 0
enabled: notificationDelegate.canExpand || notificationDelegate.isExpanded
onClicked: {
notificationDelegate.pendingLink = "";
historyInteractionArea.cursorShape = Qt.ArrowCursor;
if (scrollView.expandedId === notificationId)
scrollView.expandedId = "";
else
scrollView.expandedId = notificationId;
}
}
// Delete button
UIconButton {
iconName: "trash"
baseSize: notificationDelegate.actionButtonSize
colorFg: Colors.mError
onClicked: {
NotificationService.removeFromHistory(notificationId);
}
}
}
}
}
}
transform: Translate {
x: notificationDelegate.swipeOffset
}
Behavior on swipeOffset {
enabled: !notificationDelegate.isSwiping
NumberAnimation {
duration: notificationDelegate.removeAnimationDuration
easing.type: Easing.OutCubic
}
}
Behavior on opacity {
enabled: notificationDelegate.isRemoving
NumberAnimation {
duration: notificationDelegate.removeAnimationDuration
easing.type: Easing.OutCubic
}
}
Behavior on height {
enabled: notificationDelegate.isRemoving
NumberAnimation {
duration: notificationDelegate.removeAnimationDuration
easing.type: Easing.OutCubic
}
}
Behavior on y {
enabled: notificationDelegate.isRemoving
NumberAnimation {
duration: notificationDelegate.removeAnimationDuration
easing.type: Easing.OutCubic
}
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,240 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Components
import qs.Constants
import qs.Services
UBox {
id: root
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + Style.marginL * 2
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginL
// Card Header
RowLayout {
Layout.fillWidth: true
ColumnLayout {
RowLayout {
id: content
spacing: Style.marginM
UImageRounded {
width: Style.baseWidgetSize * 2.4
height: Style.baseWidgetSize * 2.4
imagePath: Quickshell.shellDir + "/Assets/Avatar.jpg"
fallbackIcon: "person"
radius: Style.radiusL
}
ColumnLayout {
spacing: Style.marginXS
UText {
text: `${HostService.username} @ ${HostService.hostname}`
font.weight: Style.fontWeightBold
font.pointSize: Style.fontSizeL
font.capitalization: Font.Capitalize
}
UText {
text: "Uptime: " + HostService.uptimeText
font.pointSize: Style.fontSizeM
color: Colors.mOnSurfaceVariant
}
RowLayout {
id: profileLayout
spacing: Style.marginXS
// Performance
UIconButton {
iconName: PowerService.getIcon(PowerProfile.Performance)
enabled: PowerService.available
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorFg: Colors.mRed
radius: height / 2
alwaysHover: enabled && PowerService.profile === PowerProfile.Performance
onClicked: PowerService.setProfile(PowerProfile.Performance)
}
// Balanced
UIconButton {
iconName: PowerService.getIcon(PowerProfile.Balanced)
enabled: PowerService.available
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorFg: Colors.mBlue
radius: height / 2
alwaysHover: enabled && PowerService.profile === PowerProfile.Balanced
onClicked: PowerService.setProfile(PowerProfile.Balanced)
}
// Eco
UIconButton {
iconName: PowerService.getIcon(PowerProfile.PowerSaver)
enabled: PowerService.available
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorFg: Colors.mGreen
radius: height / 2
alwaysHover: enabled && PowerService.profile === PowerProfile.PowerSaver
onClicked: PowerService.setProfile(PowerProfile.PowerSaver)
}
}
}
Item {
Layout.fillWidth: true
}
}
}
}
UDivider {
Layout.fillWidth: true
}
// Action Tiles
GridLayout {
Layout.fillWidth: true
columns: 3
rowSpacing: Style.marginM
columnSpacing: Style.marginM
Repeater {
model: [{
"name": "Lock",
"icon": "lock",
"isError": false,
"clicked": function() {
PowerService.lock();
}
}, {
"name": "Logout",
"icon": "logout",
"isError": false,
"clicked": function() {
PowerService.logout();
}
}, {
"name": "Suspend",
"icon": "moon",
"isError": false,
"clicked": function() {
PowerService.suspend();
}
}, {
"name": "Hibernate",
"icon": "bed",
"isError": false,
"clicked": function() {
PowerService.hibernate();
}
}, {
"name": "Reboot",
"icon": "refresh",
"isError": false,
"clicked": function() {
PowerService.reboot();
}
}, {
"name": "Shutdown",
"icon": "power",
"isError": true,
"clicked": function() {
PowerService.shutdown();
}
}]
delegate: Rectangle {
id: tile
readonly property bool hovered: mouseArea.containsMouse
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 2.2
radius: Style.radiusL
color: hovered ? (modelData.isError ? Colors.mError : Colors.mPrimary) : Colors.mSurface
border.color: hovered ? "transparent" : Colors.mOutline
border.width: 1
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginXS
UIcon {
Layout.alignment: Qt.AlignHCenter
iconName: modelData.icon
iconSize: Style.fontSizeXL * 1.1
color: tile.hovered ? (modelData.isError ? Colors.mOnError : Colors.mOnPrimary) : (modelData.isError ? Colors.mError : Colors.mOnSurface)
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
UText {
Layout.alignment: Qt.AlignHCenter
text: modelData.name
pointSize: Style.fontSizeS
font.weight: Style.fontWeightMedium
color: tile.hovered ? (modelData.isError ? Colors.mOnError : Colors.mOnPrimary) : Colors.mOnSurfaceVariant
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: {
modelData.clicked();
}
}
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
}
}
}
}
@@ -0,0 +1,227 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Components
import qs.Constants
import qs.Services
import qs.Utils
// Weather overview card (placeholder data)
UBox {
id: root
property int forecastDays: 6
property bool showLocation: true
property bool showEffects: true
readonly property bool weatherReady: LocationService.data.weather !== null
// Test mode: set to "clear_day", "clear_night", "rain", "snow", "cloud" or "fog"
property string testEffects: ""
// Weather condition detection
readonly property int currentWeatherCode: weatherReady ? LocationService.data.weather.current_weather.weathercode : 0
readonly property bool isDayTime: weatherReady ? LocationService.data.weather.current_weather.is_day : true
readonly property bool isRaining: testEffects === "rain" || (testEffects === "" && ((currentWeatherCode >= 51 && currentWeatherCode <= 67) || (currentWeatherCode >= 80 && currentWeatherCode <= 82)))
readonly property bool isSnowing: testEffects === "snow" || (testEffects === "" && ((currentWeatherCode >= 71 && currentWeatherCode <= 77) || (currentWeatherCode >= 85 && currentWeatherCode <= 86)))
readonly property bool isCloudy: testEffects === "cloud" || (testEffects === "" && (currentWeatherCode === 3))
readonly property bool isFoggy: testEffects === "fog" || (testEffects === "" && (currentWeatherCode >= 40 && currentWeatherCode <= 49))
readonly property bool isClearDay: testEffects === "clear_day" || (testEffects === "" && (currentWeatherCode === 0 && isDayTime))
readonly property bool isClearNight: testEffects === "clear_night" || (testEffects === "" && (currentWeatherCode === 0 && !isDayTime))
implicitHeight: Math.max(100, content.implicitHeight + Style.marginXL * 2)
// Weather effect layer (rain/snow)
Loader {
id: weatherEffectLoader
anchors.fill: parent
active: root.showEffects && (root.isRaining || root.isSnowing || root.isCloudy || root.isFoggy || root.isClearDay || root.isClearNight)
sourceComponent: Item {
// Animated time for shaders
property real shaderTime: 0
anchors.fill: parent
ShaderEffect {
id: weatherEffect
property var source
property real time: parent.shaderTime
property real itemWidth: weatherEffect.width
property real itemHeight: weatherEffect.height
property color bgColor: root.color
property real cornerRadius: root.isRaining ? 0 : (root.radius - root.border.width)
property real alternative: root.isFoggy
anchors.fill: parent
// Rain matches content margins, everything else fills the box
anchors.margins: root.isRaining ? Style.marginXL : root.border.width
fragmentShader: {
let shaderName;
if (root.isSnowing)
shaderName = "weather_snow";
else if (root.isRaining)
shaderName = "weather_rain";
else if (root.isCloudy || root.isFoggy)
shaderName = "weather_cloud";
else if (root.isClearDay)
shaderName = "weather_sun";
else if (root.isClearNight)
shaderName = "weather_stars";
else
shaderName = "";
return Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/" + shaderName + ".frag.qsb");
}
source: ShaderEffectSource {
sourceItem: content
hideSource: root.isRaining // Only hide for rain (distortion), show for snow
}
}
NumberAnimation on shaderTime {
loops: Animation.Infinite
from: 0
to: 1000
duration: 100000
}
}
}
ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: Style.marginXL
spacing: Style.marginM
clip: true
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
Item {
Layout.preferredWidth: Style.marginXXS
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
UIcon {
Layout.alignment: Qt.AlignVCenter
iconName: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode, LocationService.data.weather.current_weather.is_day) : "weather-cloud-off"
iconSize: Style.fontSizeXXXL * 1.75
color: Colors.mPrimary
}
ColumnLayout {
spacing: Style.marginXXS
UText {
text: {
// Ensure the name is not too long if one had to specify the country
const loc = SettingsService.location || "Unknown Location";
const chunks = loc.split(",");
return chunks[0];
}
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
visible: showLocation
}
RowLayout {
UText {
visible: weatherReady
text: {
if (!weatherReady)
return "";
var temp = LocationService.data.weather.current_weather.temperature;
var suffix = "C";
temp = Math.round(temp);
return `${temp}°${suffix}`;
}
pointSize: showLocation ? Style.fontSizeXL : Style.fontSizeXL * 1.6
font.weight: Style.fontWeightBold
}
UText {
text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : ""
pointSize: Style.fontSizeXS
color: Colors.mOnSurfaceVariant
visible: LocationService.data.weather && showLocation
}
}
}
}
}
UDivider {
visible: weatherReady
Layout.fillWidth: true
}
RowLayout {
visible: weatherReady
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginM
Repeater {
model: weatherReady ? Math.min(root.forecastDays, LocationService.data.weather.daily.time.length) : 0
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXS
Item {
Layout.fillWidth: true
}
UText {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
// return I18n.locale.toString(weatherDate, "ddd");
return Qt.formatDate(weatherDate, "ddd");
}
color: Colors.mOnSurface
}
UIcon {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
iconName: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
iconSize: Style.fontSizeXXL * 1.6
color: Colors.mPrimary
}
UText {
Layout.alignment: Qt.AlignHCenter
text: {
var max = LocationService.data.weather.daily.temperature_2m_max[index];
var min = LocationService.data.weather.daily.temperature_2m_min[index];
max = Math.round(max);
min = Math.round(min);
return `${max}°/${min}°`;
}
pointSize: Style.fontSizeXS
color: Colors.mOnSurfaceVariant
}
}
}
}
}
}
@@ -0,0 +1,559 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Constants
import qs.Components
import qs.Services
import qs.Utils
ColumnLayout {
property string passwordSsid: ""
property string passwordInput: ""
property string expandedSsid: ""
// Error message
Rectangle {
visible: NetworkService.lastError.length > 0
Layout.fillWidth: true
Layout.preferredHeight: errorRow.implicitHeight + (Style.marginS * 2)
color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, 0.1)
radius: Style.radiusS
border.width: Math.max(1, Style.borderS)
border.color: Colors.mError
RowLayout {
id: errorRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginS
UIcon {
iconName: "warning"
iconSize: Style.fontSizeL
color: Colors.mError
}
UText {
text: NetworkService.lastError
color: Colors.mError
pointSize: Style.fontSizeS
wrapMode: Text.Wrap
Layout.fillWidth: true
}
UIconButton {
iconName: "close"
baseSize: Style.baseWidgetSize * 0.6
onClicked: NetworkService.lastError = ""
}
}
}
// Main content area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Colors.transparent
// WiFi disabled state
ColumnLayout {
visible: !SettingsService.wifiEnabled
anchors.fill: parent
spacing: Style.marginS
Item {
Layout.fillHeight: true
}
UIcon {
iconName: "wifi-off"
iconSize: 64
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UText {
text: "Wi-Fi Disabled"
pointSize: Style.fontSizeL
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UText {
text: "Please enable Wi-Fi to connect to a network."
pointSize: Style.fontSizeS
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Scanning state
ColumnLayout {
visible: SettingsService.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
UBusyIndicator {
running: true
color: Colors.mPrimary
size: Style.baseWidgetSize
Layout.alignment: Qt.AlignHCenter
}
UText {
text: "Searching for networks..."
pointSize: Style.fontSizeM
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Networks list container
UScrollView {
visible: SettingsService.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0)
anchors.fill: parent
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
ColumnLayout {
width: parent.width
spacing: Style.marginS
// Network list
Repeater {
model: {
if (!SettingsService.wifiEnabled)
return [];
const nets = Object.values(NetworkService.networks);
return nets.sort((a, b) => {
if (a.connected !== b.connected)
return a.connected ? -1 : 1;
return b.signal - a.signal;
});
}
UBox {
Layout.fillWidth: true
implicitHeight: netColumn.implicitHeight + (Style.marginS * 2)
compact: true
ColumnLayout {
id: netColumn
width: parent.width - (Style.marginS * 2)
x: Style.marginS
y: Style.marginS
spacing: Style.marginS
// Main row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
UIcon {
iconName: NetworkService.signalIcon(modelData.signal)
iconSize: Style.fontSizeXXL
color: modelData.connected ? Colors.mPrimary : Colors.mOnSurface
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
UText {
text: modelData.ssid
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
color: Colors.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginXS
UText {
text: `${modelData.signal}%`
pointSize: Style.fontSizeXXS
color: Colors.mOnSurfaceVariant
}
UText {
text: "•"
pointSize: Style.fontSizeXXS
color: Colors.mOnSurfaceVariant
}
UText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
pointSize: Style.fontSizeXXS
color: Colors.mOnSurfaceVariant
}
Item {
Layout.preferredWidth: Style.marginXXS
}
// Update the status badges area (around line 237)
Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
color: Colors.mPrimary
radius: height * 0.5
width: connectedText.implicitWidth + (Style.marginS * 2)
height: connectedText.implicitHeight + (Style.marginXXS * 2)
UText {
id: connectedText
anchors.centerIn: parent
text: "Connected"
pointSize: Style.fontSizeXXS
color: Colors.mOnPrimary
}
}
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Colors.mError
radius: height * 0.5
width: disconnectingText.implicitWidth + (Style.marginS * 2)
height: disconnectingText.implicitHeight + (Style.marginXXS * 2)
UText {
id: disconnectingText
anchors.centerIn: parent
text: "disconnecting"
pointSize: Style.fontSizeXXS
color: Colors.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Colors.mError
radius: height * 0.5
width: forgettingText.implicitWidth + (Style.marginS * 2)
height: forgettingText.implicitHeight + (Style.marginXXS * 2)
UText {
id: forgettingText
anchors.centerIn: parent
text: "forgetting"
pointSize: Style.fontSizeXXS
color: Colors.mOnPrimary
}
}
Rectangle {
visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
color: Colors.transparent
border.color: Colors.mOutline
border.width: Math.max(1, Style.borderS)
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * 2)
height: savedText.implicitHeight + (Style.marginXXS * 2)
UText {
id: savedText
anchors.centerIn: parent
text: "saved"
pointSize: Style.fontSizeXXS
color: Colors.mOnSurfaceVariant
}
}
}
}
// Action area
RowLayout {
spacing: Style.marginS
UBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Colors.mPrimary
size: Style.baseWidgetSize * 0.5
}
UIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
iconName: "trash"
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
}
UButton {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
text: {
if (modelData.existing || modelData.cached)
return "Connect";
if (!NetworkService.isSecured(modelData.security))
return "Connect";
return "Enter Password";
}
outlined: false
fontSize: Style.fontSizeXS
enabled: !NetworkService.connecting
onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid);
} else {
passwordSsid = modelData.ssid;
passwordInput = "";
expandedSsid = "";
}
}
}
UButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
text: "Disconnect"
outlined: false
fontSize: Style.fontSizeXS
backgroundColor: Colors.mError
onClicked: NetworkService.disconnect(modelData.ssid)
}
}
}
// Password input
Rectangle {
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: passwordRow.implicitHeight + Style.marginS * 2
color: Colors.mSurfaceVariant
border.color: Colors.mOutline
border.width: Math.max(1, Style.borderS)
radius: Style.radiusS
RowLayout {
id: passwordRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginS
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusXS
color: Colors.mSurface
border.color: pwdInput.activeFocus ? Colors.mSecondary : Colors.mOutline
border.width: Math.max(1, Style.borderS)
TextInput {
id: pwdInput
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginS
text: passwordInput
font.family: Fonts.sans
font.pointSize: Style.fontSizeS
color: Colors.mOnSurface
echoMode: TextInput.Password
selectByMouse: true
focus: visible
passwordCharacter: "●"
onTextChanged: passwordInput = text
onVisibleChanged: {
if (visible)
forceActiveFocus();
}
onAccepted: {
if (text && !NetworkService.connecting) {
NetworkService.connect(passwordSsid, text);
passwordSsid = "";
passwordInput = "";
}
}
UText {
visible: parent.text.length === 0
anchors.verticalCenter: parent.verticalCenter
text: "Enter Password"
color: Colors.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
}
}
UButton {
text: "Connect"
fontSize: Style.fontSizeXXS
enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: false
onClicked: {
NetworkService.connect(passwordSsid, passwordInput);
passwordSsid = "";
passwordInput = "";
}
}
UIconButton {
iconName: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
passwordSsid = "";
passwordInput = "";
}
}
}
}
// Forget network
Rectangle {
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.marginS * 2
color: Colors.mSurfaceVariant
radius: Style.radiusS
border.width: Math.max(1, Style.borderS)
border.color: Colors.mOutline
RowLayout {
id: forgetRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginS
RowLayout {
UIcon {
iconName: "trash"
iconSize: Style.fontSizeL
color: Colors.mError
}
UText {
text: "Forget this network?"
pointSize: Style.fontSizeS
color: Colors.mError
Layout.fillWidth: true
}
}
UButton {
id: forgetButton
text: "Forget"
fontSize: Style.fontSizeXXS
backgroundColor: Colors.mError
outlined: false
onClicked: {
NetworkService.forget(modelData.ssid);
expandedSsid = "";
}
}
UIconButton {
iconName: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = ""
}
}
}
}
// Smooth opacity animation
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
}
}
}
}
// Empty state when no networks
ColumnLayout {
visible: SettingsService.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
UIcon {
iconName: "search"
iconSize: 64
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UText {
text: "No networks found"
pointSize: Style.fontSizeL
color: Colors.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
UButton {
text: "Scan Again"
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan()
}
Item {
Layout.fillHeight: true
}
}
}
}
@@ -0,0 +1,83 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Modules.Sidebar.Components
import qs.Modules.Sidebar.Modules
import qs.Services
Variants {
model: Quickshell.screens
Item {
property var modelData
SidebarWrap {
screen: modelData
isLeft: true
contentComponent: ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
WeatherCard {
Layout.fillWidth: true
}
CalendarMonthCard {
Layout.fillWidth: true
}
LyricsCard {
Layout.fillWidth: true
Layout.preferredHeight: 100
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
LyricsControl {
}
MediaCard {
Layout.fillWidth: true
}
}
ConnectionCard {
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}
SidebarWrap {
screen: modelData
isLeft: false
contentComponent: ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
PowerMenuCard {
Layout.fillWidth: true
}
NotificationHistoryCard {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
}
}
@@ -1,28 +0,0 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import qs.Constants
import qs.Noctalia
// Rounded group container using the variant surface color.
// To be used in side panels and settings panes to group fields or buttons.
Rectangle {
id: root
property bool compact: false
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
color: compact ? Color.transparent : Color.mSurfaceVariant
radius: Style.radiusM
layer.enabled: !compact
layer.effect: DropShadow {
horizontalOffset: 6
verticalOffset: 6
radius: 8
samples: 12
color: Qt.rgba(0, 0, 0, 0.3)
}
}
@@ -1,183 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
import qs.Services
Rectangle {
id: root
// Public properties
property string text: ""
property string icon: ""
property string tooltipText
property color backgroundColor: Color.mPrimary
property color textColor: Color.mOnPrimary
property color hoverColor: Color.mTertiary
property bool enabled: true
property real fontSize: Style.fontSizeM
property int fontWeight: Style.fontWeightBold
property string fontFamily: Fonts.primary
property real iconSize: Style.fontSizeL
property bool outlined: false
// Internal properties
property bool hovered: false
property bool pressed: false
// Signals
signal clicked()
signal rightClicked()
signal middleClicked()
// Dimensions
implicitWidth: contentRow.implicitWidth + (Style.marginL * 2)
implicitHeight: Math.max(Style.baseWidgetSize, contentRow.implicitHeight + (Style.marginM))
// Appearance
radius: Style.radiusS
color: {
if (!enabled)
return outlined ? Color.transparent : Qt.lighter(Color.mSurfaceVariant, 1.2);
if (hovered)
return hoverColor;
return outlined ? Color.transparent : backgroundColor;
}
border.width: outlined ? Math.max(1, Style.borderS) : 0
border.color: {
if (!enabled)
return Color.mOutline;
if (pressed || hovered)
return backgroundColor;
return outlined ? backgroundColor : Color.transparent;
}
opacity: enabled ? 1 : 0.6
// Content
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: Style.marginXS
// Icon (optional)
NIcon {
Layout.alignment: Qt.AlignVCenter
visible: root.icon !== ""
icon: root.icon
pointSize: root.iconSize
color: {
if (!root.enabled)
return Color.mOnSurfaceVariant;
if (root.outlined) {
if (root.pressed || root.hovered)
return root.backgroundColor;
return root.backgroundColor;
}
return root.textColor;
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
// Text
NText {
Layout.alignment: Qt.AlignVCenter
visible: root.text !== ""
text: root.text
pointSize: root.fontSize
font.weight: root.fontWeight
family: root.fontFamily
color: {
if (!root.enabled)
return Color.mOnSurfaceVariant;
if (root.outlined) {
if (root.hovered)
return root.textColor;
return root.backgroundColor;
}
return root.textColor;
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
}
// Mouse interaction
MouseArea {
id: mouseArea
anchors.fill: parent
enabled: root.enabled
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onEntered: {
root.hovered = true;
if (tooltipText)
TooltipService.show(Screen, root, root.tooltipText);
}
onExited: {
root.hovered = false;
if (tooltipText)
TooltipService.hide();
}
onPressed: (mouse) => {
if (tooltipText)
TooltipService.hide();
if (mouse.button === Qt.LeftButton)
root.clicked();
else if (mouse.button == Qt.RightButton)
root.rightClicked();
else if (mouse.button == Qt.MiddleButton)
root.middleClicked();
}
onCanceled: {
root.hovered = false;
if (tooltipText)
TooltipService.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
@@ -1,122 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Noctalia
import qs.Services
import qs.Utils
// Compact circular statistic display using Layout management
Rectangle {
id: root
property real value: 0 // 0..100 (or any range visually mapped)
property string icon: ""
property string suffix: "%"
// When nested inside a parent group (NBox), you can make it flat
property bool flat: false
// Scales the internal content (labels, gauge, icon) without changing the
// outer width/height footprint of the component
property real contentScale: 1
width: 68
height: 92
color: flat ? Color.transparent : Color.mSurface
radius: Style.radiusS
border.color: flat ? Color.transparent : Color.mSurfaceVariant
border.width: flat ? 0 : Math.max(1, Style.borderS)
// Repaint gauge when the bound value changes
onValueChanged: gauge.requestPaint()
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Style.marginS * contentScale
spacing: 0
// Main gauge container
Item {
id: gaugeContainer
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: 68 * contentScale
Layout.preferredHeight: 68 * contentScale
Canvas {
// 390° (equivalent to 30°)
id: gauge
anchors.fill: parent
renderStrategy: Canvas.Cooperative
onPaint: {
const ctx = getContext("2d");
const w = width, h = height;
const cx = w / 2, cy = h / 2;
const r = Math.min(w, h) / 2 - 5 * contentScale;
// Rotated 90° to the right: gap at the bottom
// Start at 150° and end at 390° (30°) → bottom opening
const start = Math.PI * 5 / 6;
// 150°
const endBg = Math.PI * 13 / 6;
ctx.reset();
ctx.lineWidth = 6 * contentScale;
// Track uses surfaceVariant for stronger contrast
ctx.strokeStyle = Color.mSurface;
ctx.beginPath();
ctx.arc(cx, cy, r, start, endBg);
ctx.stroke();
// Value arc
const ratio = Math.max(0, Math.min(1, root.value / 100));
const end = start + (endBg - start) * ratio;
ctx.strokeStyle = Color.mPrimary;
ctx.beginPath();
ctx.arc(cx, cy, r, start, end);
ctx.stroke();
}
}
// Percent centered in the circle
NText {
id: valueLabel
anchors.centerIn: parent
anchors.verticalCenterOffset: -4 * contentScale
text: `${root.value}${root.suffix}`
pointSize: Style.fontSizeM * contentScale
font.weight: Style.fontWeightBold
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
}
// Tiny circular badge for the icon, positioned inside below the percentage
Rectangle {
id: iconBadge
width: iconText.implicitWidth + Style.marginXXS
height: width
radius: width / 2
color: Color.mPrimary
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: valueLabel.bottom
anchors.topMargin: 8 * contentScale
NIcon {
id: iconText
anchors.centerIn: parent
icon: root.icon
color: Color.mOnPrimary
pointSize: Style.fontSizeS
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
@@ -1,28 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Constants
import qs.Noctalia
Text {
id: root
property string icon: Icons.defaultIcon
property real pointSize: Style.fontSizeL
visible: (icon !== undefined) && (icon !== "")
text: {
if ((icon === undefined) || (icon === ""))
return "";
if (Icons.get(icon) === undefined) {
Logger.warn("Icon", `"${icon}"`, "doesn't exist in the icons font");
Logger.callStack();
return Icons.get(Icons.defaultIcon);
}
return Icons.get(icon);
}
font.family: Icons.fontFamily
font.pointSize: root.pointSize
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
}
@@ -1,92 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Constants
import qs.Noctalia
Rectangle {
id: root
property real baseSize: Style.baseWidgetSize
property string icon
property bool enabled: true
property bool allowClickWhenDisabled: false
property bool hovering: false
property bool compact: false
property color colorBg: Color.mSurfaceVariant
property color colorFg: Color.mPrimary
property color colorBgHover: Color.mTertiary
property color colorFgHover: Color.mOnTertiary
property color colorBorder: Color.transparent
property color colorBorderHover: Color.transparent
signal entered()
signal exited()
signal clicked()
signal rightClicked()
signal middleClicked()
implicitWidth: Math.round(baseSize)
implicitHeight: Math.round(baseSize)
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
color: root.enabled && root.hovering ? colorBgHover : colorBg
radius: width * 0.5
border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder
border.width: Math.max(1, Style.borderS)
NIcon {
icon: root.icon
pointSize: Math.max(1, root.compact ? root.width * 0.65 : root.width * 0.48)
color: root.enabled && root.hovering ? colorFgHover : colorFg
// Center horizontally
x: (root.width - width) / 2
// Center vertically accounting for font metrics
y: (root.height - height) / 2 + (height - contentHeight) / 2
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
MouseArea {
// Always enabled to allow hover/tooltip even when the button is disabled
enabled: true
anchors.fill: parent
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onEntered: {
hovering = root.enabled ? true : false;
root.entered();
}
onExited: {
hovering = false;
root.exited();
}
onClicked: function(mouse) {
if (!root.enabled && !allowClickWhenDisabled)
return ;
if (mouse.button === Qt.LeftButton)
root.clicked();
else if (mouse.button === Qt.RightButton)
root.rightClicked();
else if (mouse.button === Qt.MiddleButton)
root.middleClicked();
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
@@ -1,85 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Constants
import qs.Noctalia
import qs.Services
Rectangle {
id: root
property string imagePath: ""
property color borderColor: Color.transparent
property real borderWidth: 0
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL
color: Color.transparent
radius: parent.width * 0.5
anchors.margins: Style.marginXXS
Rectangle {
color: Color.transparent
anchors.fill: parent
Image {
id: img
anchors.fill: parent
source: imagePath
visible: false // Hide since we're using it as shader source
mipmap: true
smooth: true
asynchronous: true
antialiasing: true
fillMode: Image.PreserveAspectCrop
}
ShaderEffect {
property var source
property real imageOpacity: root.opacity
anchors.fill: parent
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/circled_image.frag.qsb")
supportsAtlasTextures: false
blending: true
source: ShaderEffectSource {
sourceItem: img
hideSource: true
live: true
recursive: false
format: ShaderEffectSource.RGBA
}
}
// Fallback icon
Loader {
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
anchors.centerIn: parent
sourceComponent: NIcon {
anchors.centerIn: parent
icon: fallbackIcon
pointSize: fallbackIconSize
z: 0
}
}
}
// Border
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: parent.borderColor
border.width: parent.borderWidth
antialiasing: true
z: 10
}
}
@@ -1,103 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Constants
import qs.Noctalia
Rectangle {
id: root
property string imagePath: ""
property color borderColor: Color.transparent
property real borderWidth: 0
property real imageRadius: width * 0.5
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL
property real scaledRadius: imageRadius
signal statusChanged(int status)
color: Color.transparent
radius: scaledRadius
anchors.margins: Style.marginXXS
Rectangle {
color: Color.transparent
anchors.fill: parent
Image {
id: img
anchors.fill: parent
source: imagePath
visible: false // Hide since we're using it as shader source
mipmap: true
smooth: true
asynchronous: true
antialiasing: true
fillMode: Image.PreserveAspectCrop
onStatusChanged: root.statusChanged(status)
}
ShaderEffect {
property var source
// Use custom property names to avoid conflicts with final properties
property real itemWidth: root.width
property real itemHeight: root.height
property real cornerRadius: root.radius
property real imageOpacity: root.opacity
anchors.fill: parent
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
// Qt6 specific properties - ensure proper blending
supportsAtlasTextures: false
blending: true
// Make sure the background is transparent
Rectangle {
id: background
anchors.fill: parent
color: Color.transparent
z: -1
}
source: ShaderEffectSource {
sourceItem: img
hideSource: true
live: true
recursive: false
format: ShaderEffectSource.RGBA
}
}
// Fallback icon
Loader {
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
anchors.centerIn: parent
sourceComponent: NIcon {
anchors.centerIn: parent
icon: fallbackIcon
pointSize: fallbackIconSize
z: 0
}
}
}
// Border
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: parent.borderColor
border.width: parent.borderWidth
antialiasing: true
z: 10
}
}
@@ -1,34 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Constants
ColumnLayout {
id: root
property string label: ""
property string description: ""
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
spacing: Style.marginXXS
Layout.fillWidth: true
NText {
text: label
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: labelColor
visible: label !== ""
Layout.fillWidth: true
}
NText {
text: description
pointSize: Style.fontSizeS
color: descriptionColor
wrapMode: Text.WordWrap
visible: description !== ""
Layout.fillWidth: true
}
}

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