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> <summary>Niri & Quickshell</summary>
https://github.com/user-attachments/assets/7e2db305-58bc-4b3d-9c65-7dc0461aead7
<figure> <figure>
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop-alt.jpg?raw=true"/> <img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop-alt.jpg?raw=true"/>
</figure> </figure>
@@ -48,11 +46,11 @@ https://github.com/user-attachments/assets/7e2db305-58bc-4b3d-9c65-7dc0461aead7
- Shell: **Fish** - Shell: **Fish**
- Prompt: **Oh My Posh** - Prompt: **Oh My Posh**
- Terminal: **Kitty** & (**WezTerm** | Ghostty) - Terminal: **Kitty** & (**WezTerm** | Ghostty)
- Power Menu: **Wlogout** - Power Menu: **Wlogout** & Quickshell
- Colorscheme: **Catppuccin Mocha** - Colorscheme: **Catppuccin Mocha**
- App Launcher: **Rofi** | Fuzzel - App Launcher: **Rofi** | Fuzzel
- Desktop Widgets: Eww | **Quickshell** - Desktop Widgets: Eww | **Quickshell**
- Wallpaper Daemon: **Awww** (previously Swww) - Wallpaper Daemon: Awww | **Quickshell**
- Notification Daemon: Mako | **Quickshell** - Notification Daemon: Mako | **Quickshell**
(**bold**: currently preferred) (**bold**: currently preferred)
@@ -67,7 +65,7 @@ Ported from Hyprland, and shares some of the desktop components such as hyprlock
## Quickshell ## 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. This setup is currently only adapted for Niri.
@@ -80,8 +78,6 @@ This setup is currently only adapted for Niri.
## Wallpaper & Colortheme ## Wallpaper & Colortheme
- [WallReel](https://github.com/Uyanide/WallReel): an Image Carousel implemented with QtQuick to browse and set wallpapers from. - [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. - [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). - [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+Return { spawn "kitty"; }
Mod+Shift+W { spawn "wallreel"; } Mod+Shift+W { spawn "wallreel"; }
Mod+O { spawn-sh "pkill -x -n pwvucontrol || pwvucontrol"; } Mod+O { spawn-sh "pkill -x -n pwvucontrol || pwvucontrol"; }
Ctrl+Alt+Delete { spawn-sh "pkill -x wlogout || wlogout"; }
// Quickshell // Quickshell
Mod+Space { spawn "qs" "ipc" "call" "panels" "toggleControlCenter"; } Mod+Space { spawn "qs" "ipc" "call" "bars" "toggleLeft"; }
Mod+Shift+D { spawn "qs" "ipc" "call" "panels" "toggleCalendar"; } Mod+N { spawn "qs" "ipc" "call" "bars" "toggleRight"; }
Mod+Shift+L { spawn "qs" "ipc" "call" "lyrics" "toggleBarLyrics"; } Mod+Shift+L { spawn "qs" "ipc" "call" "bars" "toggleLyrics"; }
Mod+Shift+K { spawn-sh "quickshell-kill || quickshell"; } Mod+Shift+K { spawn-sh "quickshell-kill || quickshell"; }
Mod+I { spawn "qs" "ipc" "call" "idleInhibitor" "toggleInhibitor"; } Mod+I { spawn "qs" "ipc" "call" "idleInhibitor" "toggleInhibitor"; }
Mod+Alt+R { spawn "qs" "ipc" "call" "recording" "startOrStopRecording"; } Mod+Alt+R { spawn "qs" "ipc" "call" "recording" "startOrStopRecording"; }
@@ -38,7 +39,6 @@ binds {
// Actions // Actions
Mod+V { spawn-sh "fzfclip-wrap"; } Mod+V { spawn-sh "fzfclip-wrap"; }
Mod+Period { spawn-sh "pkill -x rofi || rofi-emoji"; } 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"; } Print { spawn "niri" "msg" "action" "screenshot-screen"; }
Mod+Shift+S { spawn "niri" "msg" "action" "screenshot"; } Mod+Shift+S { spawn "niri" "msg" "action" "screenshot"; }
Mod+Ctrl+Shift+S { spawn "niri" "msg" "action" "screenshot-window"; } Mod+Ctrl+Shift+S { spawn "niri" "msg" "action" "screenshot-window"; }
@@ -48,18 +48,18 @@ binds {
Mod+L { spawn "loginctl" "lock-session"; } Mod+L { spawn "loginctl" "lock-session"; }
// Media // Media
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"; } XF86AudioRaiseVolume allow-when-locked=true { spawn "qs" "ipc" "call" "media" "volumeUp"; }
XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%-"; } XF86AudioLowerVolume allow-when-locked=true { spawn "qs" "ipc" "call" "media" "volumeDown"; }
XF86AudioMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; } XF86AudioMute allow-when-locked=true { spawn "qs" "ipc" "call" "media" "toggleOutputMute"; }
XF86AudioMicMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; } XF86AudioMicMute allow-when-locked=true { spawn "qs" "ipc" "call" "media" "toggleInputMute"; }
XF86AudioPlay allow-when-locked=true { spawn-sh "playerctl play-pause"; } XF86AudioPlay allow-when-locked=true { spawn "qs" "ipc" "call" "media" "playPause"; }
XF86AudioPause allow-when-locked=true { spawn-sh "playerctl play-pause"; } XF86AudioPause allow-when-locked=true { spawn "qs" "ipc" "call" "media" "playPause"; }
XF86AudioNext allow-when-locked=true { spawn-sh "playerctl next"; } XF86AudioNext allow-when-locked=true { spawn "qs" "ipc" "call" "media" "next"; }
XF86AudioPrev allow-when-locked=true { spawn-sh "playerctl previous"; } XF86AudioPrev allow-when-locked=true { spawn "qs" "ipc" "call" "media" "previous"; }
// Brightness // Brightness
XF86MonBrightnessUp 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 "set-brightness" "10%-"; } XF86MonBrightnessDown allow-when-locked=true { spawn "qs" "ipc" "call" "brightness" "brightnessDown"; }
// Window management // Window management
Mod+Tab repeat=false { toggle-overview; } Mod+Tab repeat=false { toggle-overview; }
+1 -4
View File
@@ -1,9 +1,6 @@
// Switch configs // Switch configs
spawn-at-startup "config-switch" "niri" spawn-at-startup "config-switch" "niri"
// Wallpaper
spawn-at-startup "wallpaper-daemon"
// Not necessary maybe ... // Not necessary maybe ...
spawn-at-startup "fcitx5" spawn-at-startup "fcitx5"
@@ -23,7 +20,7 @@ spawn-at-startup "wl-paste" "--type" "image" "--watch" "cliphist" "store"
spawn-at-startup "solaar" "-w" "hide" spawn-at-startup "solaar" "-w" "hide"
// Some other heavy apps // Some other heavy apps
spawn-at-startup "sunshine" // spawn-at-startup "sunshine"
// spawn-at-startup "spotify" // spawn-at-startup "spotify"
// spawn-at-startup "thunderbird" // spawn-at-startup "thunderbird"
+1 -1
View File
@@ -60,7 +60,7 @@ animations {
} }
layer-rule { layer-rule {
match namespace="^swww-daemonbackdrop$" match namespace="backdrop$"
place-within-backdrop true place-within-backdrop true
} }
@@ -1,2 +1 @@
file:///home/kolkas/Desktop 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 settings.json
GeoInfoToken.txt
IpAliases.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 QtQuick
import qs.Constants import qs.Constants
import qs.Noctalia
Item { Item {
id: root id: root
property bool running: true property bool running: true
property color color: Color.mPrimary property color color: Colors.mPrimary
property int size: Style.baseWidgetSize property int size: Style.baseWidgetSize
property int strokeWidth: Style.borderL property int strokeWidth: Style.borderL
property int duration: Style.animationSlow * 2 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.Controls
import QtQuick.Layouts import QtQuick.Layouts
import qs.Constants 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 { Popup {
id: root id: root
property alias model: listView.model property var model: []
property real itemHeight: 36 property real itemHeight: 36
property real itemPadding: Style.marginM 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) signal triggered(string action)
@@ -30,22 +69,24 @@ Popup {
width: 180 width: 180
padding: Style.marginS padding: Style.marginS
onOpened: PanelService.willOpenPopup(root)
onClosed: PanelService.willClosePopup(root)
background: Rectangle { background: Rectangle {
color: Color.mSurfaceVariant color: Colors.mSurfaceVariant
border.color: Color.mOutline border.color: Colors.mOutline
border.width: Math.max(1, Style.borderS) border.width: Style.borderS
radius: Style.radiusM radius: Style.radiusM
} }
contentItem: NListView { contentItem: UListView {
id: listView id: listView
implicitHeight: contentHeight implicitHeight: Math.max(contentHeight, root.itemHeight)
spacing: Style.marginXXS spacing: Style.marginXXS
interactive: contentHeight > root.height interactive: contentHeight > root.height
verticalPolicy: root.verticalPolicy
horizontalPolicy: root.horizontalPolicy
reserveScrollbarSpace: false
model: root.filteredModel
delegate: ItemDelegate { delegate: ItemDelegate {
id: menuItem id: menuItem
@@ -53,9 +94,8 @@ Popup {
// Store reference to the popup // Store reference to the popup
property var popup: root property var popup: root
width: listView.width width: listView.availableWidth
height: modelData.visible !== false ? root.itemHeight : 0 height: root.itemHeight
visible: modelData.visible !== false
opacity: modelData.enabled !== false ? 1 : 0.5 opacity: modelData.enabled !== false ? 1 : 0.5
enabled: modelData.enabled !== false enabled: modelData.enabled !== false
onClicked: { onClicked: {
@@ -66,27 +106,19 @@ Popup {
} }
background: Rectangle { background: Rectangle {
color: menuItem.hovered && menuItem.enabled ? Color.mTertiary : Color.transparent color: menuItem.hovered && menuItem.enabled ? Colors.mHover : "transparent"
radius: Style.radiusS radius: Style.radiusS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
} }
contentItem: RowLayout { contentItem: RowLayout {
spacing: Style.marginS spacing: Style.marginS
// Optional icon // Optional icon
NIcon { UIcon {
visible: modelData.icon !== undefined visible: modelData.icon !== undefined
icon: modelData.icon || "" iconName: modelData.icon || ""
pointSize: Style.fontSizeM iconSize: Style.fontSizeM
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface color: menuItem.hovered && menuItem.enabled ? Colors.mOnHover : Colors.mOnSurface
Layout.leftMargin: root.itemPadding Layout.leftMargin: root.itemPadding
Behavior on color { Behavior on color {
@@ -98,10 +130,10 @@ Popup {
} }
NText { UText {
text: modelData.label || modelData.text || "" text: modelData.label || modelData.text || ""
pointSize: Style.fontSizeM pointSize: Style.fontSizeM
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface color: menuItem.hovered && menuItem.enabled ? Colors.mOnHover : Colors.mOnSurface
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0 Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0
@@ -12,22 +12,22 @@ Rectangle {
GradientStop { GradientStop {
position: 0 position: 0
color: Color.transparent color: Colors.transparent
} }
GradientStop { GradientStop {
position: 0.1 position: 0.1
color: Color.mOutline color: Colors.mOutline
} }
GradientStop { GradientStop {
position: 0.9 position: 0.9
color: Color.mOutline color: Colors.mOutline
} }
GradientStop { GradientStop {
position: 1 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.Controls
import QtQuick.Templates as T import QtQuick.Templates as T
import qs.Constants import qs.Constants
import qs.Noctalia
Item { Item {
id: root 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 handleHoverColor: handleColor
property color handlePressedColor: handleColor property color handlePressedColor: handleColor
property color trackColor: Color.transparent property color trackColor: "transparent"
property real handleWidth: 6 property real handleWidth: 6
property real handleRadius: Style.radiusM property real handleRadius: Style.radiusM
property int verticalPolicy: ScrollBar.AsNeeded 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 // Forward ListView properties
property alias model: listView.model property alias model: listView.model
property alias delegate: listView.delegate property alias delegate: listView.delegate
@@ -53,6 +65,8 @@ Item {
property alias dragging: listView.dragging property alias dragging: listView.dragging
property alias horizontalVelocity: listView.horizontalVelocity property alias horizontalVelocity: listView.horizontalVelocity
property alias verticalVelocity: listView.verticalVelocity property alias verticalVelocity: listView.verticalVelocity
// Scroll speed multiplier for mouse wheel (1.0 = default, higher = faster)
property real wheelScrollMultiplier: 2
// Forward ListView methods // Forward ListView methods
function positionViewAtIndex(index, mode) { function positionViewAtIndex(index, mode) {
@@ -99,33 +113,110 @@ Item {
return listView.itemAtIndex(index); 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 // Set reasonable implicit sizes for Layout usage
implicitWidth: 200 implicitWidth: 200
implicitHeight: 200 implicitHeight: 200
Component.onCompleted: {
createGradients();
}
ListView { ListView {
id: listView id: listView
anchors.fill: parent anchors.fill: parent
// Enable clipping to keep content within bounds anchors.rightMargin: root.reserveScrollbarSpace ? root.handleWidth + Style.marginXS : 0
clip: true clip: true
// Enable flickable for smooth scrolling
boundsBehavior: Flickable.StopAtBounds 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 { ScrollBar.vertical: ScrollBar {
parent: listView parent: root
x: listView.mirrored ? 0 : listView.width - width x: root.mirrored ? 0 : root.width - width
y: 0 y: 0
height: listView.height height: root.height
active: listView.ScrollBar.horizontal.active
policy: root.verticalPolicy policy: root.verticalPolicy
visible: policy === ScrollBar.AlwaysOn || root.verticalScrollBarActive
contentItem: Rectangle { contentItem: Rectangle {
implicitWidth: root.handleWidth implicitWidth: root.handleWidth
implicitHeight: 100 implicitHeight: 100
radius: root.handleRadius radius: root.handleRadius
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor 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 { Behavior on opacity {
NumberAnimation { NumberAnimation {
@@ -147,58 +238,7 @@ Item {
implicitWidth: root.handleWidth implicitWidth: root.handleWidth
implicitHeight: 100 implicitHeight: 100
color: root.trackColor 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 {
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
radius: root.handleRadius / 2 radius: root.handleRadius / 2
Behavior on opacity { Behavior on opacity {
@@ -7,12 +7,12 @@ import qs.Constants
Item { Item {
id: root id: root
required property string symbol required property string iconName
property int symbolSize: Fonts.icon property int iconSize: Style.fontSizeM
property real maxValue: 100 property real maxValue: 100
property real value: 100 property real value: 100
property string textValue: "" // override value in textDisplay if set property string textValue: "" // override value in textDisplay if set
property color fillColor: Colors.primary property color fillColor: Colors.mPrimary
property string textSuffix: "" property string textSuffix: ""
property bool pointerCursor: true property bool pointerCursor: true
property bool expandOnValueChange: false property bool expandOnValueChange: false
@@ -22,7 +22,7 @@ Item {
property bool _isFirst: true property bool _isFirst: true
property bool disableHover: false property bool disableHover: false
property bool critical: false property bool critical: false
property color criticalColor: Colors.red property color criticalColor: Colors.mRed
readonly property real ratio: value / maxValue readonly property real ratio: value / maxValue
property color realColor: critical ? criticalColor : fillColor property color realColor: critical ? criticalColor : fillColor
@@ -32,8 +32,8 @@ Item {
signal rightClicked() signal rightClicked()
signal middleClicked() signal middleClicked()
implicitHeight: parent.height - 5 implicitHeight: Math.max(iconSize, textLabel.implicitHeight) + 12
implicitWidth: parent.height + (_expand ? textDisplay.width : 0) implicitWidth: height + textDisplay.implicitWidth
Loader { Loader {
id: connectionLoader id: connectionLoader
@@ -95,15 +95,14 @@ Item {
} }
RowLayout { RowLayout {
anchors.top: parent.top anchors.fill: parent
anchors.bottom: parent.bottom
spacing: 0 spacing: 0
Item { Item {
id: progressDisplay id: progressDisplay
Layout.preferredHeight: parent.height Layout.preferredHeight: root.height
Layout.preferredWidth: parent.height Layout.preferredWidth: root.height
Canvas { Canvas {
id: progressCircle id: progressCircle
@@ -140,16 +139,15 @@ Item {
} }
Text { UIcon {
id: symbolText id: symbolIcon
anchors.fill: parent anchors.fill: parent
text: symbol
font.family: Fonts.nerd
font.pointSize: symbolSize
color: root.realColor
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
iconName: root.iconName
iconSize: root.iconSize
color: root.realColor
} }
} }
@@ -161,17 +159,15 @@ Item {
implicitWidth: root._expand ? textLabel.implicitWidth + 10 : 0 implicitWidth: root._expand ? textLabel.implicitWidth + 10 : 0
clip: true clip: true
Text { UText {
id: textLabel id: textLabel
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 5 anchors.leftMargin: 5
text: (textValue || Math.round(root.value)) + root.textSuffix text: (textValue || Math.round(root.value)) + root.textSuffix
font.pointSize: Fonts.small font.pointSize: Style.fontSizeS
font.family: Fonts.primary
color: root.realColor color: root.realColor
opacity: root._expand ? 1 : 0
} }
Behavior on implicitWidth { Behavior on implicitWidth {
@@ -6,10 +6,10 @@ import qs.Constants
T.ScrollView { T.ScrollView {
id: root 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 handleHoverColor: handleColor
property color handlePressedColor: handleColor property color handlePressedColor: handleColor
property color trackColor: Color.transparent property color trackColor: Colors.transparent
property real handleWidth: 6 property real handleWidth: 6
property real handleRadius: Style.radiusM property real handleRadius: Style.radiusM
property int verticalPolicy: ScrollBar.AsNeeded 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
import QtQuick.Layouts import QtQuick.Layouts
import qs.Constants import qs.Constants
import qs.Noctalia
Text { Text {
id: root id: root
property string family: Fonts.primary property string family: Fonts.primary
property real pointSize: Style.fontSizeM property real pointSize: Style.fontSizeM
property real fontScale: 1
font.family: root.family font.family: root.family
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
font.pointSize: root.pointSize * fontScale font.pointSize: root.pointSize
color: Color.mOnSurface color: Colors.mOnSurface
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components
import qs.Constants import qs.Constants
RowLayout { RowLayout {
@@ -8,38 +9,54 @@ RowLayout {
property string label: "" property string label: ""
property string description: "" property string description: ""
property string icon: ""
property bool checked: false property bool checked: false
property bool hovering: false property bool hovering: false
property int baseSize: Math.round(Style.baseWidgetSize * 0.8) 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 toggled(bool checked)
signal entered() signal entered()
signal exited() signal exited()
Layout.fillWidth: true Layout.fillWidth: true
opacity: enabled ? 1 : 0.6
spacing: Style.marginM
NLabel { ULabel {
Layout.fillWidth: true
label: root.label label: root.label
description: root.description 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 { Rectangle {
id: switcher id: switcher
Layout.alignment: Qt.AlignVCenter
implicitWidth: Math.round(root.baseSize * 0.85) * 2 implicitWidth: Math.round(root.baseSize * 0.85) * 2
implicitHeight: Math.round(root.baseSize * 0.5) * 2 implicitHeight: Math.round(root.baseSize * 0.5) * 2
radius: height * 0.5 radius: Math.min(Style.radiusL, height / 2)
color: root.checked ? Color.mPrimary : Color.mSurface color: root.checked ? Colors.mPrimary : Colors.mSurface
border.color: Color.mOutline border.color: Colors.mOutline
border.width: Math.max(1, Style.borderS) border.width: Style.borderS
Rectangle { Rectangle {
implicitWidth: Math.round(root.baseSize * 0.4) * 2 implicitWidth: Math.round(root.baseSize * 0.4) * 2
implicitHeight: Math.round(root.baseSize * 0.4) * 2 implicitHeight: Math.round(root.baseSize * 0.4) * 2
radius: height * 0.5 radius: Math.min(Style.radiusL, height / 2)
color: root.checked ? Color.mOnPrimary : Color.mPrimary color: root.checked ? Colors.mOnPrimary : Colors.mPrimary
border.color: root.checked ? Color.mSurface : Color.mSurface border.color: root.checked ? Colors.mSurface : Colors.mSurface
border.width: Math.max(1, Style.borderM) border.width: Style.borderM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 0 anchors.verticalCenterOffset: 0
x: root.checked ? switcher.width - width - 3 : 3 x: root.checked ? switcher.width - width - 3 : 3
@@ -55,18 +72,28 @@ RowLayout {
} }
MouseArea { MouseArea {
enabled: root.enabled
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
onEntered: { onEntered: {
if (!enabled)
return ;
hovering = true; hovering = true;
root.entered(); root.entered();
} }
onExited: { onExited: {
if (!enabled)
return ;
hovering = false; hovering = false;
root.exited(); root.exited();
} }
onClicked: { onClicked: {
if (!enabled)
return ;
root.toggled(!root.checked); 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 QtQuick
import Quickshell import Quickshell
import qs.Services import Quickshell.Io
import qs.Constants
import qs.Utils
pragma Singleton pragma Singleton
Singleton { Singleton {
id: root id: root
readonly property color primary: SettingsService.primaryColor // Part of material3 color scheme
readonly property color transparent: "transparent" property color mPrimary: defaultColors.mPrimary
readonly property color rosewater: "#f5e0dc" property color mOnPrimary: defaultColors.mOnPrimary
readonly property color flamingo: "#f2cdcd" property color mError: defaultColors.mError
readonly property color pink: "#f5c2e7" property color mOnError: defaultColors.mOnError
readonly property color mauve: "#cba6f7" property color mSurface: defaultColors.mSurface
readonly property color red: "#f38ba8" property color mOnSurface: defaultColors.mOnSurface
readonly property color maroon: "#eba0ac" property color mSurfaceVariant: defaultColors.mSurfaceVariant
readonly property color peach: "#fab387" property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant
readonly property color yellow: "#f9e2af" property color mOutline: defaultColors.mOutline
readonly property color green: "#a6e3a1" property color mShadow: defaultColors.mShadow
readonly property color teal: "#94e2d5" property color mHover: defaultColors.mHover
readonly property color sky: "#89dceb" property color mOnHover: defaultColors.mOnHover
readonly property color sapphire: "#74c7ec" // Supplementary colors
readonly property color blue: "#89b4fa" property color mPink: defaultColors.mPink
readonly property color lavender: "#b4befe" property color mPurple: defaultColors.mPurple
readonly property color text: "#cdd6f4" property color mRed: defaultColors.mRed
readonly property color subtext1: "#bac2de" property color mOrange: defaultColors.mOrange
readonly property color subtext0: "#a6adc8" property color mYellow: defaultColors.mYellow
readonly property color overlay2: "#9399b2" property color mGreen: defaultColors.mGreen
readonly property color overlay1: "#7f849c" property color mCyan: defaultColors.mCyan
readonly property color overlay0: "#6c7086" property color mSky: defaultColors.mSky
readonly property color surface2: "#585b70" property color mBlue: defaultColors.mBlue
readonly property color surface1: "#45475a" property color mLavender: defaultColors.mLavender
readonly property color surface0: "#313244" // Special colors
readonly property color surface: "#292a3c" property color distro: "#74c7ec"
readonly property color base: "#1e1e2e" property color transparent: "#00000000"
readonly property color mantle: "#181825" readonly property var cavaList: [mLavender, mBlue, mSky, mCyan, mGreen, mYellow, mOrange, mRed]
readonly property color crust: "#11111b"
readonly property color distroColor: "#74c7ec" function reloadColors(newColors) {
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"] 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 primary: "LXGW WenKai"
readonly property string nerd: "Meslo LGM Nerd Font Mono" readonly property string nerd: "Meslo LGM Nerd Font Mono"
readonly property string icon: Icons.fontFamily
readonly property string sans: "LXGW WenKai" 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
import QtQuick.Controls
import Quickshell import Quickshell
import qs.Constants
import qs.Utils import qs.Utils
pragma Singleton pragma Singleton
Singleton { Singleton {
id: root 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 // Expose the font family name for easy access
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : "" readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
readonly property string defaultIcon: TablerIcons.defaultIcon readonly property string defaultIcon: IconsTabler.defaultIcon
readonly property var icons: TablerIcons.icons readonly property var icons: IconsTabler.icons
readonly property var aliases: TablerIcons.aliases readonly property var aliases: IconsTabler.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf" readonly property string fontPath: "/Assets/Fonts/tabler/noctalia-tabler-icons.ttf"
// Current active font loader // Current active font loader
property FontLoader currentFontLoader: null property FontLoader currentFontLoader: null
property int fontVersion: 0 property int fontVersion: 0
@@ -68,6 +34,7 @@ Singleton {
} }
function loadFontWithCacheBusting() { function loadFontWithCacheBusting() {
Logger.d("Icons", "Loading font with cache busting");
// Destroy old loader first // Destroy old loader first
if (currentFontLoader) { if (currentFontLoader) {
currentFontLoader.destroy(); currentFontLoader.destroy();
@@ -82,24 +49,29 @@ Singleton {
`, root, "dynamicFontLoader_" + fontVersion); `, root, "dynamicFontLoader_" + fontVersion);
// Connect to the new loader's status changes // Connect to the new loader's status changes
currentFontLoader.statusChanged.connect(function() { 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(); fontReloaded();
else if (currentFontLoader.status === FontLoader.Error) } else if (currentFontLoader.status === FontLoader.Error) {
Logger.error("Icons", "Font failed to load (version " + fontVersion + ")"); Logger.e("Icons", "Font failed to load (version " + fontVersion + ")");
}
}); });
} }
function reloadFont() { function reloadFont() {
Logger.d("Icons", "Forcing font reload...");
fontVersion++; fontVersion++;
loadFontWithCacheBusting(); loadFontWithCacheBusting();
} }
Component.onCompleted: { Component.onCompleted: {
Logger.i("Icons", "Service started");
loadFontWithCacheBusting(); loadFontWithCacheBusting();
} }
Connections { Connections {
function onReloadCompleted() { function onReloadCompleted() {
Logger.d("Icons", "Quickshell reload completed - forcing font reload");
reloadFont(); reloadFont();
} }
@@ -33,6 +33,7 @@ Singleton {
"media-next": "player-skip-forward-filled", "media-next": "player-skip-forward-filled",
"download-speed": "download", "download-speed": "download",
"upload-speed": "upload", "upload-speed": "upload",
"cpu-intensive": "alert-octagon",
"cpu-usage": "brand-speedtest", "cpu-usage": "brand-speedtest",
"cpu-temperature": "flame", "cpu-temperature": "flame",
"gpu-temperature": "device-desktop", "gpu-temperature": "device-desktop",
@@ -42,6 +43,7 @@ Singleton {
"powersaver": "leaf", "powersaver": "leaf",
"storage": "database", "storage": "database",
"ethernet": "sitemap", "ethernet": "sitemap",
"ethernet-off": "sitemap-off",
"keyboard": "keyboard", "keyboard": "keyboard",
"shutdown": "power", "shutdown": "power",
"lock": "lock", "lock": "lock",
@@ -49,6 +51,7 @@ Singleton {
"logout": "logout", "logout": "logout",
"reboot": "refresh", "reboot": "refresh",
"suspend": "player-pause", "suspend": "player-pause",
"hibernate": "zzz",
"nightlight-on": "moon", "nightlight-on": "moon",
"nightlight-off": "moon-off", "nightlight-off": "moon-off",
"nightlight-forced": "moon-stars", "nightlight-forced": "moon-stars",
@@ -71,15 +74,19 @@ Singleton {
"chevron-down": "chevron-down", "chevron-down": "chevron-down",
"caret-up": "caret-up-filled", "caret-up": "caret-up-filled",
"caret-down": "caret-down-filled", "caret-down": "caret-down-filled",
"caret-left": "caret-left-filled",
"caret-right": "caret-right-filled",
"star": "star", "star": "star",
"star-off": "star-off", "star-off": "star-off",
"battery-exclamation": "battery-exclamation", "battery-exclamation": "battery-exclamation",
"battery-charging": "battery-charging", "battery-charging": "battery-charging",
"battery-charging-2": "battery-charging-2",
"battery-4": "battery-4", "battery-4": "battery-4",
"battery-3": "battery-3", "battery-3": "battery-3",
"battery-2": "battery-2", "battery-2": "battery-2",
"battery-1": "battery-1", "battery-1": "battery-1",
"battery": "battery", "battery": "battery",
"battery-off": "battery-off",
"wifi-0": "wifi-0", "wifi-0": "wifi-0",
"wifi-1": "wifi-1", "wifi-1": "wifi-1",
"wifi-2": "wifi-2", "wifi-2": "wifi-2",
@@ -88,10 +95,14 @@ Singleton {
"microphone": "microphone", "microphone": "microphone",
"microphone-mute": "microphone-off", "microphone-mute": "microphone-off",
"volume-mute": "volume-off", "volume-mute": "volume-off",
"volume-x": "volume-3",
"volume-zero": "volume-3", "volume-zero": "volume-3",
"volume-low": "volume-2", "volume-low": "volume-2",
"volume-high": "volume", "volume-high": "volume",
"weather-sun": "sun", "weather-sun": "sun",
"weather-moon": "moon",
"weather-moon-stars": "moon-stars",
"weather-cloud-off": "cloud-off",
"weather-cloud": "cloud", "weather-cloud": "cloud",
"weather-cloud-haze": "cloud-fog", "weather-cloud-haze": "cloud-fog",
"weather-cloud-lightning": "cloud-bolt", "weather-cloud-lightning": "cloud-bolt",
@@ -101,24 +112,33 @@ Singleton {
"brightness-low": "brightness-down-filled", "brightness-low": "brightness-down-filled",
"brightness-high": "brightness-up-filled", "brightness-high": "brightness-up-filled",
"settings-general": "adjustments-horizontal", "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-dock": "layout-bottombar",
"settings-launcher": "rocket", "settings-launcher": "rocket",
"settings-audio": "device-speaker", "settings-audio": "device-speaker",
"settings-display": "device-desktop", "settings-display": "device-desktop",
"settings-network": "sitemap", "settings-network": "circles-relation",
"settings-brightness": "brightness-up", "settings-brightness": "brightness-up",
"settings-location": "world-pin", "settings-location": "world-pin",
"settings-color-scheme": "palette", "settings-color-scheme": "palette",
"settings-wallpaper": "paint", "settings-wallpaper": "paint",
"settings-wallpaper-selector": "library-photo", "settings-wallpaper-selector": "library-photo",
"settings-screen-recorder": "video",
"settings-hooks": "link", "settings-hooks": "link",
"settings-notifications": "bell", "settings-notifications": "bell",
"settings-osd": "picture-in-picture", "settings-osd": "picture-in-picture",
"settings-about": "info-square-rounded", "settings-about": "info-square-rounded",
"settings-idle": "moon",
"settings-lock-screen": "lock",
"settings-session-menu": "power",
"settings-system-monitor": "activity",
"bluetooth": "bluetooth", "bluetooth": "bluetooth",
"bt-device-generic": "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-headphones": "headphones",
"bt-device-mouse": "mouse-2", "bt-device-mouse": "mouse-2",
"bt-device-keyboard": "bluetooth", "bt-device-keyboard": "bluetooth",
@@ -126,6 +146,12 @@ Singleton {
"bt-device-watch": "device-watch", "bt-device-watch": "device-watch",
"bt-device-speaker": "device-speaker", "bt-device-speaker": "device-speaker",
"bt-device-tv": "device-tv", "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", "noctalia": "noctalia",
"hyprland": "hyprland", "hyprland": "hyprland",
"filepicker-folder": "folder", "filepicker-folder": "folder",
@@ -152,7 +178,10 @@ Singleton {
"filepicker-text": "file-text", "filepicker-text": "file-text",
"filepicker-eye": "eye", "filepicker-eye": "eye",
"filepicker-eye-off": "eye-off", "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! // Fonts Codepoints - do not change!
@@ -295,8 +324,8 @@ Singleton {
"align-left": "\u{ea09}", "align-left": "\u{ea09}",
"align-left-2": "\u{ff00}", "align-left-2": "\u{ff00}",
"align-right": "\u{ea0a}", "align-right": "\u{ea0a}",
"alpha"//"align-right-2": "\u{feff}", //"align-right-2": "\u{feff}",
: "\u{f543}", "alpha": "\u{f543}",
"alphabet-arabic": "\u{ff2f}", "alphabet-arabic": "\u{ff2f}",
"alphabet-bangla": "\u{ff2e}", "alphabet-bangla": "\u{ff2e}",
"alphabet-cyrillic": "\u{f1df}", "alphabet-cyrillic": "\u{f1df}",
@@ -2084,7 +2113,7 @@ Singleton {
"cloud-snow": "\u{ea73}", "cloud-snow": "\u{ea73}",
"cloud-star": "\u{f85b}", "cloud-star": "\u{f85b}",
"cloud-storm": "\u{ea74}", "cloud-storm": "\u{ea74}",
"cloud-sun": "\u{ea7a}", "cloud-sun": "\u{ec6d}",
"cloud-up": "\u{f85c}", "cloud-up": "\u{f85c}",
"cloud-upload": "\u{ea75}", "cloud-upload": "\u{ea75}",
"cloud-x": "\u{f85d}", "cloud-x": "\u{f85d}",
@@ -3128,8 +3157,8 @@ Singleton {
"friends": "\u{eab0}", "friends": "\u{eab0}",
"friends-off": "\u{f136}", "friends-off": "\u{f136}",
"frustum": "\u{fa9f}", "frustum": "\u{fa9f}",
"frustum-plus"//"frustum-off": "\u{fa9d}", //"frustum-off": "\u{fa9d}",
: "\u{fa9e}", "frustum-plus": "\u{fa9e}",
"function": "\u{f225}", "function": "\u{f225}",
"function-filled": "\u{fc2b}", "function-filled": "\u{fc2b}",
"function-off": "\u{f3f0}", "function-off": "\u{f3f0}",
@@ -3388,13 +3417,13 @@ Singleton {
"hexagon-letter-x": "\u{f479}", "hexagon-letter-x": "\u{f479}",
"hexagon-letter-x-filled": "\u{fe30}", "hexagon-letter-x-filled": "\u{fe30}",
"hexagon-letter-y": "\u{f47a}", "hexagon-letter-y": "\u{f47a}",
"hexagon-letter-z"//"hexagon-letter-y-filled": "\u{fe2f}", //"hexagon-letter-y-filled": "\u{fe2f}",
: "\u{f47b}", "hexagon-letter-z": "\u{f47b}",
"hexagon-minus"//"hexagon-letter-z-filled": "\u{fe2e}", //"hexagon-letter-z-filled": "\u{fe2e}",
: "\u{fc8f}", "hexagon-minus": "\u{fc8f}",
"hexagon-minus-2": "\u{fc8e}", "hexagon-minus-2": "\u{fc8e}",
"hexagon-number-0"//"hexagon-minus-filled": "\u{fe2d}", //"hexagon-minus-filled": "\u{fe2d}",
: "\u{f459}", "hexagon-number-0": "\u{f459}",
"hexagon-number-0-filled": "\u{f74c}", "hexagon-number-0-filled": "\u{f74c}",
"hexagon-number-1": "\u{f45a}", "hexagon-number-1": "\u{f45a}",
"hexagon-number-1-filled": "\u{f74d}", "hexagon-number-1-filled": "\u{f74d}",
@@ -3417,8 +3446,8 @@ Singleton {
"hexagon-off": "\u{ee9c}", "hexagon-off": "\u{ee9c}",
"hexagon-plus": "\u{fc45}", "hexagon-plus": "\u{fc45}",
"hexagon-plus-2": "\u{fc90}", "hexagon-plus-2": "\u{fc90}",
"hexagonal-prism"//"hexagon-plus-filled": "\u{fe2c}", //"hexagon-plus-filled": "\u{fe2c}",
: "\u{faa5}", "hexagonal-prism": "\u{faa5}",
"hexagonal-prism-off": "\u{faa3}", "hexagonal-prism-off": "\u{faa3}",
"hexagonal-prism-plus": "\u{faa4}", "hexagonal-prism-plus": "\u{faa4}",
"hexagonal-pyramid": "\u{faa8}", "hexagonal-pyramid": "\u{faa8}",
@@ -3448,8 +3477,8 @@ Singleton {
"home-eco": "\u{f351}", "home-eco": "\u{f351}",
"home-edit": "\u{f352}", "home-edit": "\u{f352}",
"home-exclamation": "\u{f33c}", "home-exclamation": "\u{f33c}",
"home-hand"//"home-filled": "\u{fe2b}", //"home-filled": "\u{fe2b}",
: "\u{f504}", "home-hand": "\u{f504}",
"home-heart": "\u{f353}", "home-heart": "\u{f353}",
"home-infinity": "\u{f505}", "home-infinity": "\u{f505}",
"home-link": "\u{f354}", "home-link": "\u{f354}",
@@ -3567,8 +3596,8 @@ Singleton {
"ironing-2-filled": "\u{1006e}", "ironing-2-filled": "\u{1006e}",
"ironing-3": "\u{f2f6}", "ironing-3": "\u{f2f6}",
"ironing-3-filled": "\u{1006d}", "ironing-3-filled": "\u{1006d}",
"ironing-off"//"ironing-filled": "\u{fe2a}", //"ironing-filled": "\u{fe2a}",
: "\u{f2f7}", "ironing-off": "\u{f2f7}",
"ironing-steam": "\u{f2f9}", "ironing-steam": "\u{f2f9}",
"ironing-steam-filled": "\u{1006c}", "ironing-steam-filled": "\u{1006c}",
"ironing-steam-off": "\u{f2f8}", "ironing-steam-off": "\u{f2f8}",
@@ -3578,8 +3607,8 @@ Singleton {
"italic": "\u{eb93}", "italic": "\u{eb93}",
"jacket": "\u{f661}", "jacket": "\u{f661}",
"jetpack": "\u{f581}", "jetpack": "\u{f581}",
"jewish-star"//"jetpack-filled": "\u{fe29}", //"jetpack-filled": "\u{fe29}",
: "\u{f3ff}", "jewish-star": "\u{f3ff}",
"jewish-star-filled": "\u{f67e}", "jewish-star-filled": "\u{f67e}",
"join-bevel": "\u{ff4c}", "join-bevel": "\u{ff4c}",
"join-round": "\u{ff4b}", "join-round": "\u{ff4b}",
@@ -3593,8 +3622,8 @@ Singleton {
"kering": "\u{efb8}", "kering": "\u{efb8}",
"kerning": "\u{efb8}", "kerning": "\u{efb8}",
"key": "\u{eac7}", "key": "\u{eac7}",
"key-off"//"key-filled": "\u{fe28}", //"key-filled": "\u{fe28}",
: "\u{f14b}", "key-off": "\u{f14b}",
"keyboard": "\u{ebd6}", "keyboard": "\u{ebd6}",
"keyboard-filled": "\u{100a2}", "keyboard-filled": "\u{100a2}",
"keyboard-hide": "\u{ec7e}", "keyboard-hide": "\u{ec7e}",
@@ -3650,20 +3679,20 @@ Singleton {
"layers-union": "\u{eacb}", "layers-union": "\u{eacb}",
"layout": "\u{eadb}", "layout": "\u{eadb}",
"layout-2": "\u{eacc}", "layout-2": "\u{eacc}",
"layout-align-left"//"layout-2-filled": "\u{fe27}", //"layout-2-filled": "\u{fe27}",
//"layout-align-bottom": "\u{eacd}", //"layout-align-bottom": "\u{eacd}",
//"layout-align-bottom-filled": "\u{fe26}", //"layout-align-bottom-filled": "\u{fe26}",
//"layout-align-center": "\u{eace}", //"layout-align-center": "\u{eace}",
//"layout-align-center-filled": "\u{fe25}", //"layout-align-center-filled": "\u{fe25}",
: "\u{eacf}", "layout-align-left": "\u{eacf}",
"layout-align-middle"// "layout-align-left-filled": "\u{fe24}", //"layout-align-left-filled": "\u{fe24}",
: "\u{ead0}", "layout-align-middle": "\u{ead0}",
"layout-align-right"//"layout-align-middle-filled": "\u{fe23}", //"layout-align-middle-filled": "\u{fe23}",
: "\u{ead1}", "layout-align-right": "\u{ead1}",
"layout-align-top"//"layout-align-right-filled": "\u{fe22}", //"layout-align-right-filled": "\u{fe22}",
: "\u{ead2}", "layout-align-top": "\u{ead2}",
"layout-board"//"layout-align-top-filled": "\u{fe21}", //"layout-align-top-filled": "\u{fe21}",
: "\u{ef95}", "layout-board": "\u{ef95}",
"layout-board-filled": "\u{10182}", "layout-board-filled": "\u{10182}",
"layout-board-split": "\u{ef94}", "layout-board-split": "\u{ef94}",
"layout-board-split-filled": "\u{10183}", "layout-board-split-filled": "\u{10183}",
@@ -3675,8 +3704,8 @@ Singleton {
"layout-bottombar-filled": "\u{fc37}", "layout-bottombar-filled": "\u{fc37}",
"layout-bottombar-inactive": "\u{fd45}", "layout-bottombar-inactive": "\u{fd45}",
"layout-cards": "\u{ec13}", "layout-cards": "\u{ec13}",
"layout-collage"// "layout-cards-filled": "\u{fe20}", //"layout-cards-filled": "\u{fe20}",
: "\u{f389}", "layout-collage": "\u{f389}",
"layout-columns": "\u{ead4}", "layout-columns": "\u{ead4}",
"layout-dashboard": "\u{f02c}", "layout-dashboard": "\u{f02c}",
"layout-dashboard-filled": "\u{fe1f}", "layout-dashboard-filled": "\u{fe1f}",
@@ -4157,14 +4186,14 @@ Singleton {
"microphone": "\u{eaf0}", "microphone": "\u{eaf0}",
"microphone-2": "\u{ef2c}", "microphone-2": "\u{ef2c}",
"microphone-2-off": "\u{f40d}", "microphone-2-off": "\u{f40d}",
"microphone-off"//"microphone-filled": "\u{fe0f}", //"microphone-filled": "\u{fe0f}",
: "\u{ed16}", "microphone-off": "\u{ed16}",
"microscope": "\u{ef64}", "microscope": "\u{ef64}",
"microscope-filled": "\u{10166}", "microscope-filled": "\u{10166}",
"microscope-off": "\u{f40e}", "microscope-off": "\u{f40e}",
"microwave": "\u{f248}", "microwave": "\u{f248}",
"microwave-off"//"microwave-filled": "\u{fe0e}", //"microwave-filled": "\u{fe0e}",
: "\u{f264}", "microwave-off": "\u{f264}",
"military-award": "\u{f079}", "military-award": "\u{f079}",
"military-rank": "\u{efcf}", "military-rank": "\u{efcf}",
"military-rank-filled": "\u{ff5e}", "military-rank-filled": "\u{ff5e}",
@@ -4398,7 +4427,7 @@ Singleton {
"number-4-small": "\u{fcf9}", "number-4-small": "\u{fcf9}",
"number-40-small": "\u{fffa}", "number-40-small": "\u{fffa}",
"number-41-small": "\u{fff9}", "number-41-small": "\u{fff9}",
"number-5"//"number-42-small": "\u{fff8}", //"number-42-small": "\u{fff8}",
//"number-43-small": "\u{fff7}", //"number-43-small": "\u{fff7}",
//"number-44-small": "\u{fff6}", //"number-44-small": "\u{fff6}",
//"number-45-small": "\u{fff5}", //"number-45-small": "\u{fff5}",
@@ -4406,10 +4435,10 @@ Singleton {
//"number-47-small": "\u{fff3}", //"number-47-small": "\u{fff3}",
//"number-48-small": "\u{fff2}", //"number-48-small": "\u{fff2}",
//"number-49-small": "\u{fff1}", //"number-49-small": "\u{fff1}",
: "\u{edf5}", "number-5": "\u{edf5}",
"number-5-small": "\u{fcfa}", "number-5-small": "\u{fcfa}",
"number-51-small"// "number-50-small": "\u{fff0}", //"number-50-small": "\u{fff0}",
: "\u{ffef}", "number-51-small": "\u{ffef}",
"number-52-small": "\u{ffee}", "number-52-small": "\u{ffee}",
"number-53-small": "\u{ffed}", "number-53-small": "\u{ffed}",
"number-54-small": "\u{ffec}", "number-54-small": "\u{ffec}",
@@ -4761,6 +4790,7 @@ Singleton {
"playstation-triangle": "\u{f2af}", "playstation-triangle": "\u{f2af}",
"playstation-x": "\u{f2b0}", "playstation-x": "\u{f2b0}",
"plug": "\u{ebd9}", "plug": "\u{ebd9}",
"plug-filled": "\u{f6b3}",
"plug-connected": "\u{f00a}", "plug-connected": "\u{f00a}",
"plug-connected-x": "\u{f0a0}", "plug-connected-x": "\u{f0a0}",
"plug-off": "\u{f180}", "plug-off": "\u{f180}",
@@ -4849,11 +4879,11 @@ Singleton {
"quote": "\u{efbe}", "quote": "\u{efbe}",
"quote-filled": "\u{1009c}", "quote-filled": "\u{1009c}",
"quote-off": "\u{f188}", "quote-off": "\u{f188}",
"radar"//"quotes": "\u{fb1e}", //"quotes": "\u{fb1e}",
: "\u{f017}", "radar": "\u{f017}",
"radar-2": "\u{f016}", "radar-2": "\u{f016}",
"radar-off"//"radar-filled": "\u{fe0d}", //"radar-filled": "\u{fe0d}",
: "\u{f41f}", "radar-off": "\u{f41f}",
"radio": "\u{ef2d}", "radio": "\u{ef2d}",
"radio-off": "\u{f420}", "radio-off": "\u{f420}",
"radioactive": "\u{ecc0}", "radioactive": "\u{ecc0}",
@@ -4913,12 +4943,12 @@ Singleton {
"regex-off": "\u{f421}", "regex-off": "\u{f421}",
"registered": "\u{eb14}", "registered": "\u{eb14}",
"relation-many-to-many": "\u{ed7f}", "relation-many-to-many": "\u{ed7f}",
"relation-one-to-many"//"relation-many-to-many-filled": "\u{fe0c}", //"relation-many-to-many-filled": "\u{fe0c}",
: "\u{ed80}", "relation-one-to-many": "\u{ed80}",
"relation-one-to-one"//"relation-one-to-many-filled": "\u{fe0b}", //"relation-one-to-many-filled": "\u{fe0b}",
: "\u{ed81}", "relation-one-to-one": "\u{ed81}",
"reload"//"relation-one-to-one-filled": "\u{fe0a}", //"relation-one-to-one-filled": "\u{fe0a}",
: "\u{f3ae}", "reload": "\u{f3ae}",
"reorder": "\u{fc15}", "reorder": "\u{fc15}",
"repeat": "\u{eb72}", "repeat": "\u{eb72}",
"repeat-off": "\u{f18e}", "repeat-off": "\u{f18e}",
@@ -5070,8 +5100,8 @@ Singleton {
"search": "\u{eb1c}", "search": "\u{eb1c}",
"search-off": "\u{f19c}", "search-off": "\u{f19c}",
"section": "\u{eed5}", "section": "\u{eed5}",
"section-sign"//"section-filled": "\u{fe09}", //"section-filled": "\u{fe09}",
: "\u{f019}", "section-sign": "\u{f019}",
"seeding": "\u{ed51}", "seeding": "\u{ed51}",
"seeding-filled": "\u{10006}", "seeding-filled": "\u{10006}",
"seeding-off": "\u{f19d}", "seeding-off": "\u{f19d}",
@@ -5279,8 +5309,8 @@ Singleton {
"sort-z-a": "\u{f550}", "sort-z-a": "\u{f550}",
"sos": "\u{f24a}", "sos": "\u{f24a}",
"soup": "\u{ef2e}", "soup": "\u{ef2e}",
"soup-off"//"soup-filled": "\u{fe08}", //"soup-filled": "\u{fe08}",
: "\u{f42d}", "soup-off": "\u{f42d}",
"source-code": "\u{f4a2}", "source-code": "\u{f4a2}",
"space": "\u{ec0c}", "space": "\u{ec0c}",
"space-off": "\u{f1aa}", "space-off": "\u{f1aa}",
@@ -5373,22 +5403,22 @@ Singleton {
"square-half": "\u{effb}", "square-half": "\u{effb}",
"square-key": "\u{f638}", "square-key": "\u{f638}",
"square-letter-a": "\u{f47c}", "square-letter-a": "\u{f47c}",
"square-letter-b"//"square-letter-a-filled": "\u{fe07}", //"square-letter-a-filled": "\u{fe07}",
: "\u{f47d}", "square-letter-b": "\u{f47d}",
"square-letter-c"//"square-letter-b-filled": "\u{fe06}", //"square-letter-b-filled": "\u{fe06}",
: "\u{f47e}", "square-letter-c": "\u{f47e}",
"square-letter-d"//"square-letter-c-filled": "\u{fe05}", //"square-letter-c-filled": "\u{fe05}",
: "\u{f47f}", "square-letter-d": "\u{f47f}",
"square-letter-e"//"square-letter-d-filled": "\u{fe04}", //"square-letter-d-filled": "\u{fe04}",
: "\u{f480}", "square-letter-e": "\u{f480}",
"square-letter-f"//"square-letter-e-filled": "\u{fe03}", //"square-letter-e-filled": "\u{fe03}",
: "\u{f481}", "square-letter-f": "\u{f481}",
"square-letter-g"//"square-letter-f-filled": "\u{fe02}", //"square-letter-f-filled": "\u{fe02}",
: "\u{f482}", "square-letter-g": "\u{f482}",
"square-letter-h"//"square-letter-g-filled": "\u{fe01}", //"square-letter-g-filled": "\u{fe01}",
: "\u{f483}", "square-letter-h": "\u{f483}",
"square-letter-i"//"square-letter-h-filled": "\u{fe00}", //"square-letter-h-filled": "\u{fe00}",
: "\u{f484}", "square-letter-i": "\u{f484}",
"square-letter-i-filled": "\u{fdff}", "square-letter-i-filled": "\u{fdff}",
"square-letter-j": "\u{f485}", "square-letter-j": "\u{f485}",
"square-letter-j-filled": "\u{fdfe}", "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 pragma Singleton
Singleton { Singleton {
/*
Preset sizes for font, radii, ?
*/
id: root id: root
// Font size // Font size
@@ -19,6 +15,7 @@ Singleton {
readonly property real fontSizeXL: 16 readonly property real fontSizeXL: 16
readonly property real fontSizeXXL: 18 readonly property real fontSizeXXL: 18
readonly property real fontSizeXXXL: 24 readonly property real fontSizeXXXL: 24
readonly property real fontNerd: 16
// Font weight // Font weight
readonly property int fontWeightRegular: 400 readonly property int fontWeightRegular: 400
readonly property int fontWeightMedium: 500 readonly property int fontWeightMedium: 500
@@ -50,19 +47,22 @@ Singleton {
readonly property real opacityHeavy: 0.75 readonly property real opacityHeavy: 0.75
readonly property real opacityAlmost: 0.95 readonly property real opacityAlmost: 0.95
readonly property real opacityFull: 1 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) // Animation duration (ms)
readonly property int animationFast: 150 readonly property int animationFast: 150
readonly property int animationNormal: 300 readonly property int animationNormal: 300
readonly property int animationSlow: 450 readonly property int animationSlow: 450
readonly property int animationSlowest: 1000 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 // Settings widgets base size
readonly property real baseWidgetSize: 33 readonly property int baseWidgetSize: 33
readonly property real sliderWidth: 200 readonly property int sliderWidth: 200
// Bar Dimensions // Bar Dimensions
readonly property real barHeight: 45 readonly property int barHeight: 45
readonly property real capsuleHeight: 35 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 QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.UPower
import Quickshell.Wayland import Quickshell.Wayland
import qs.Components
import qs.Constants import qs.Constants
import qs.Modules.Bar.Components import qs.Modules.Bar.Components
import qs.Modules.Bar.Misc import qs.Modules.Bar.Modules
import qs.Modules.Misc
import qs.Services import qs.Services
Variants { Variants {
@@ -22,6 +21,7 @@ Variants {
screen: modelData screen: modelData
WlrLayershell.namespace: "quickshell-bar" WlrLayershell.namespace: "quickshell-bar"
WlrLayershell.layer: WlrLayer.Top
color: Colors.transparent color: Colors.transparent
implicitHeight: Style.barHeight implicitHeight: Style.barHeight
@@ -35,12 +35,11 @@ Variants {
id: barBackground id: barBackground
anchors.fill: parent anchors.fill: parent
color: Niri.noFocus ? null : Colors.base
gradient: Gradient { gradient: Gradient {
GradientStop { GradientStop {
position: 0 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 { Behavior on color {
ColorAnimation { ColorAnimation {
@@ -54,7 +53,7 @@ Variants {
GradientStop { GradientStop {
position: 1 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 { Behavior on color {
ColorAnimation { ColorAnimation {
@@ -81,42 +80,22 @@ Variants {
leftMargin: 5 leftMargin: 5
} }
SymbolButton { UIconButton {
symbol: Icons.distro textOverride: "󰣇"
buttonColor: Colors.distroColor fontFamily: Fonts.nerd
onClicked: { baseSize: parent.height - Style.marginXXS * 2
PanelService.getPanel("controlCenterPanel")?.toggle(this) iconSize: Style.fontNerd
colorFg: Colors.distro
onClicked: () => {
BarService.toggleLeft();
} }
onRightClicked: { onRightClicked: () => {
Quickshell.execDetached(["rofi", "-show", "drun"]); 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 { Separator {
implicitWidth: Style.marginXL
} }
Workspace { Workspace {
@@ -124,28 +103,17 @@ Variants {
} }
Separator { Separator {
} implicitWidth: Style.marginXL
Item {
width: 10
} }
CavaBar { CavaBar {
} }
Item {
width: 10
}
Separator { Separator {
} implicitWidth: Style.marginXL
Item {
width: 10
} }
FocusedWindow { FocusedWindow {
maxWidth: 400
} }
} }
@@ -176,21 +144,27 @@ Variants {
rightMargin: 5 rightMargin: 5
} }
Loader {
sourceComponent: LyricsService.showLyricsBar ? lyricsComponent : monitorsComponent
Component {
id: monitorsComponent
RowLayout { RowLayout {
id: monitorsLayout id: monitorsLayout
visible: !SettingsService.showLyricsBar
height: parent.height height: rightLayout.height
spacing: Style.marginM
Component.onCompleted: {
SystemStatService.registerComponent("BarMonitors");
}
NetworkSpeed { NetworkSpeed {
} }
Separator { Separator {
} }
Item {
width: 10
}
RecordIndicator { RecordIndicator {
} }
@@ -215,43 +189,49 @@ Variants {
Volume { Volume {
} }
} }
}
Component {
id: lyricsComponent
LyricsBar { LyricsBar {
id: lyricsBar
visible: SettingsService.showLyricsBar
width: 600
} }
Item { }
width: 5
} }
Separator { Separator {
} }
Item { RowLayout {
width: 5 height: rightLayout.height
} spacing: Style.marginS
TrayExpander { TrayExpander {
screen: modelData screen: modelData
baseSize: rightLayout.height - Style.marginXXS * 2
} }
SymbolButton { UIconButton {
symbol: Caffeine.isInhibited ? Icons.idleInhibitorActivated : Icons.idleInhibitorDeactivated iconName: Caffeine.isInhibited ? "mug-off" : "mug"
buttonColor: Caffeine.isInhibited ? Colors.peach : Colors.yellow colorFg: Caffeine.isInhibited ? Colors.mOrange : Colors.mYellow
onClicked: { baseSize: rightLayout.height - Style.marginXXS * 2
alwaysHover: Caffeine.isInhibited
onClicked: () => {
Caffeine.manualToggle(); Caffeine.manualToggle();
} }
} }
SymbolButton { UIconButton {
symbol: Icons.powerMenu iconName: "power"
buttonColor: Colors.red colorFg: Colors.mRed
onClicked: { baseSize: rightLayout.height - Style.marginXXS * 2
Quickshell.execDetached(["wlogout"]); onClicked: () => {
BarService.toggleRight();
} }
} }
@@ -262,3 +242,5 @@ 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 { Item {
id: root id: root
implicitHeight: parent.height implicitHeight: Style.barHeight - Style.marginL * 2
implicitWidth: Style.marginM
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
width: 1.5 width: 1.5
height: parent.height * 0.32 height: parent.height
color: Colors.text color: Colors.mOnSurface
} }
} }
@@ -5,7 +5,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
import qs.Modules.Bar.Misc import qs.Modules.Bar.Components
import qs.Constants import qs.Constants
import qs.Services import qs.Services
import qs.Utils import qs.Utils
@@ -107,7 +107,7 @@ Rectangle {
trayMenu.item.menu = modelData.menu trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY) trayMenu.item.showAt(parent, menuX, menuY)
} else { } 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 { Loader {
id: trayMenu id: trayMenu
Component.onCompleted: { Component.onCompleted: {
setSource("../Misc/TrayMenu.qml", { setSource("./TrayMenu.qml", {
"screen": root.screen "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 Quickshell.Widgets
import qs.Constants import qs.Constants
import qs.Utils import qs.Utils
import qs.Noctalia import qs.Components
PopupWindow { PopupWindow {
id: root id: root
@@ -86,8 +86,8 @@ PopupWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Colors.base color: Colors.mSurface
border.color: Colors.primary border.color: Colors.mPrimary
border.width: 2 border.width: 2
radius: Style.radiusM radius: Style.radiusM
} }
@@ -126,7 +126,7 @@ PopupWindow {
color: Colors.transparent color: Colors.transparent
property var subMenu: null property var subMenu: null
NDivider { UDivider {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - (Style.marginM * 2) width: parent.width - (Style.marginM * 2)
visible: modelData?.isSeparator ?? false visible: modelData?.isSeparator ?? false
@@ -134,7 +134,7 @@ PopupWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: mouseArea.containsMouse ? Colors.primary : Colors.transparent color: mouseArea.containsMouse ? Colors.mPrimary : Colors.transparent
radius: Style.radiusS radius: Style.radiusS
visible: !(modelData?.isSeparator ?? false) visible: !(modelData?.isSeparator ?? false)
@@ -144,10 +144,10 @@ PopupWindow {
anchors.rightMargin: Style.marginM anchors.rightMargin: Style.marginM
spacing: Style.marginS spacing: Style.marginS
NText { UText {
id: text id: text
Layout.fillWidth: true 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, ' ') : "..." text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
pointSize: Style.fontSizeS pointSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -162,12 +162,12 @@ PopupWindow {
visible: (modelData?.icon ?? "") !== "" visible: (modelData?.icon ?? "") !== ""
} }
NIcon { UIcon {
icon: modelData?.hasChildren ? "menu" : "" iconName: modelData?.hasChildren ? "menu" : ""
pointSize: Style.fontSizeS iconSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false 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 QtQuick
import Quickshell.Services.UPower import Quickshell.Services.UPower
import qs.Components
import qs.Constants import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem { UProgressExpand {
readonly property var battery: UPower.displayDevice readonly property var battery: UPower.displayDevice
readonly property bool isReady: (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) readonly property bool isReady: (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
readonly property real percent: (isReady ? (battery.percentage * 100) : 0) readonly property real percent: (isReady ? (battery.percentage * 100) : 0)
readonly property bool charging: (isReady ? battery.state === UPowerDeviceState.Charging : false) readonly property bool charging: (isReady ? battery.state === UPowerDeviceState.Charging : false)
property int lowBatteryThreshold: 20 property int lowBatteryThreshold: 20
symbol: { iconName: {
return charging ? Icons.charging : percent >= 80 ? Icons.battery100 : percent >= 60 ? Icons.battery75 : percent >= 40 ? Icons.battery50 : percent >= 20 ? Icons.battery25 : Icons.battery00; 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 value: percent
critical: isReady && !charging && percent <= lowBatteryThreshold critical: isReady && !charging && percent <= lowBatteryThreshold
maxValue: 100 maxValue: 100
@@ -1,18 +1,18 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Components
import qs.Constants import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services import qs.Services
MonitorItem { UProgressExpand {
property ShellScreen screen: null property ShellScreen screen: null
function getMonitor() { function getMonitor() {
return BrightnessService.getMonitorForScreen(screen) || null; return BrightnessService.getMonitorForScreen(screen) || null;
} }
symbol: Icons.brightness iconName: "sun-filled"
fillColor: Colors.blue fillColor: Colors.mBlue
value: { value: {
const monitor = getMonitor(); const monitor = getMonitor();
return monitor ? Math.round(monitor.brightness * 100) : "N/A"; return monitor ? Math.round(monitor.brightness * 100) : "N/A";
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import qs.Constants import qs.Constants
import qs.Modules.Bar.Misc import qs.Modules.Bar.Services
import qs.Services import qs.Services
import qs.Utils import qs.Utils
@@ -14,7 +14,7 @@ Item {
property int mode: 0 property int mode: 0
implicitWidth: root.barWidth * CavaBarService.count + root.barSpacing * (CavaBarService.count - 1) implicitWidth: root.barWidth * CavaBarService.count + root.barSpacing * (CavaBarService.count - 1)
implicitHeight: parent.height - 10 implicitHeight: Style.barHeight - Style.marginS * 2
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@@ -53,9 +53,7 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => { onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
MusicManager.playPause(); MediaService.playPause();
} else if (mouse.button === Qt.RightButton) {
SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
mode = (mode + 1) % 3; mode = (mode + 1) % 3;
if (mode === 0) { if (mode === 0) {
@@ -71,13 +69,15 @@ Item {
CavaBarService.forceEnable = false; CavaBarService.forceEnable = false;
CavaBarService.forceDisable = true; CavaBarService.forceDisable = true;
} }
} else if (mouse.button === Qt.RightButton) {
LyricsService.toggleLyricsBar();
} }
} }
onWheel: function(wheel) { onWheel: function(wheel) {
if (wheel.angleDelta.y > 0) if (wheel.angleDelta.y > 0)
MusicManager.previous(); MediaService.previous();
else if (wheel.angleDelta.y < 0) 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 QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import qs.Components
import qs.Constants import qs.Constants
import qs.Services import qs.Services
import qs.Utils import qs.Utils
@@ -10,7 +11,7 @@ import qs.Utils
Item { Item {
id: root id: root
property real maxWidth: 250 property real maxWidth: 320
property string fallbackIcon: "application-x-executable" property string fallbackIcon: "application-x-executable"
function getAppIcon(appId) { function getAppIcon(appId) {
@@ -23,24 +24,25 @@ Item {
return iconResult; return iconResult;
} catch (iconError) { } 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); return ThemeIcons.iconFromName(root.fallbackIcon);
} catch (e) { } catch (e) {
Logger.warn("FocusedWindow", "Error in getAppIcon:", e); Logger.w("FocusedWindow", "Error in getAppIcon:", e);
return ThemeIcons.iconFromName(root.fallbackIcon); return ThemeIcons.iconFromName(root.fallbackIcon);
} }
} }
implicitHeight: parent.height implicitWidth: layout.implicitWidth
implicitHeight: Math.max(windowIcon.implicitHeight, windowTitle.implicitHeight)
RowLayout { RowLayout {
id: layout id: layout
anchors.fill: parent anchors.fill: parent
spacing: 10 spacing: 10
visible: Niri.focusedWindowId !== -1 visible: Niri.hasFocusedWindow
Item { Item {
// Layout.alignment: Qt.AlignVCenter // Layout.alignment: Qt.AlignVCenter
@@ -79,24 +81,20 @@ Item {
height: parent.height height: parent.height
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Text { UText {
id: windowTitle id: windowTitle
text: Niri.focusedWindowTitle text: Niri.focusedWindowTitle
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
font.pointSize: Fonts.medium color: Colors.mPrimary
font.family: Fonts.primary
color: Colors.primary
} }
Text { UText {
text: Niri.focusedWindowTitle text: Niri.focusedWindowTitle
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: windowTitle.right anchors.left: windowTitle.right
anchors.leftMargin: titleContainer.scrollSpacing anchors.leftMargin: titleContainer.scrollSpacing
font.pointSize: Fonts.medium color: Colors.mPrimary
font.family: Fonts.primary
color: Colors.primary
visible: titleContainer.shouldScroll visible: titleContainer.shouldScroll
} }
@@ -3,15 +3,14 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Constants import qs.Constants
import qs.Services import qs.Services
import qs.Modules.Bar.Misc import qs.Components
MonitorItem { UProgressExpand {
symbol: Icons.ip iconName: "world"
fillColor: Colors.peach fillColor: Colors.mOrange
value: 100 value: 100
maxValue: 100 maxValue: 100
textValue: displayText textValue: displayText
symbolSize: 18
property int displayIndex: 0 property int displayIndex: 0
readonly property list<string> displayTexts: [IpService.countryCode, IpService.ip, IpService.alias] readonly property list<string> displayTexts: [IpService.countryCode, IpService.ip, IpService.alias]
@@ -1,84 +1,76 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Components
import qs.Constants import qs.Constants
import qs.Noctalia
import qs.Services import qs.Services
Rectangle { Rectangle {
implicitHeight: parent.height
radius: Style.radiusS radius: Style.radiusS
color: Colors.base color: Colors.mSurface
border.color: Colors.primary border.color: Colors.mPrimary
border.width: Style.borderS border.width: Style.borderS
Component.onCompleted: {
Connections {
function onShowLyricsBarChanged() {
visible = SettingsService.showLyricsBar;
if (visible)
LyricsService.startSyncing(); LyricsService.startSyncing();
else }
Component.onDestruction: {
LyricsService.stopSyncing(); LyricsService.stopSyncing();
} }
implicitHeight: Style.barHeight - Style.marginXS * 2
target: SettingsService implicitWidth: 600
}
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Style.marginM anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginM anchors.rightMargin: Style.marginM
spacing: Style.marginS spacing: Style.marginXS
Item { Item {
implicitWidth: parent.width - slowerButton.implicitWidth * 3 - parent.spacing * 3 - parent.anchors.leftMargin - parent.anchors.rightMargin implicitWidth: parent.width - slowerButton.implicitWidth * 3 - parent.spacing * 3 - parent.anchors.leftMargin - parent.anchors.rightMargin
Layout.fillHeight: true Layout.fillHeight: true
clip: true clip: true
NText { UText {
text: LyricsService.lyrics[LyricsService.currentIndex] || "" text: LyricsService.lyrics[LyricsService.currentIndex] || ""
family: Fonts.sans family: Fonts.sans
pointSize: Style.fontSizeS pointSize: Style.fontSizeM
maximumLineCount: 1 maximumLineCount: 1
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
NIconButton { UIconButton {
id: slowerButton id: slowerButton
baseSize: 24 colorFg: Colors.mBlue
colorBg: Color.transparent iconName: "rotate-2"
colorBgHover: Colors.blue baseSize: parent.height - Style.marginXS * 2
colorFg: Colors.blue iconSize: Style.fontSizeM
icon: "rotate-2"
onClicked: { onClicked: {
LyricsService.increaseOffset(); LyricsService.increaseOffset();
} }
} }
NIconButton { UIconButton {
id: playPauseButton id: playPauseButton
baseSize: 24 colorFg: Colors.mYellow
colorBg: Color.transparent iconName: "rotate-clockwise-2"
colorBgHover: Colors.yellow baseSize: parent.height - Style.marginXS * 2
colorFg: Colors.yellow iconSize: Style.fontSizeM
icon: "rotate-clockwise-2"
onClicked: { onClicked: {
LyricsService.decreaseOffset(); LyricsService.decreaseOffset();
} }
} }
NIconButton { UIconButton {
id: nextButton id: nextButton
baseSize: 24 colorFg: Colors.mGreen
colorBg: Color.transparent iconName: "rotate-clockwise"
colorBgHover: Colors.green baseSize: parent.height - Style.marginXS * 2
colorFg: Colors.green iconSize: Style.fontSizeM
icon: "rotate-clockwise"
onClicked: { onClicked: {
LyricsService.resetOffset(); LyricsService.resetOffset();
} }
@@ -1,34 +1,23 @@
import QtQuick import QtQuick
import Quickshell.Io import qs.Components
import qs.Constants import qs.Constants
import qs.Modules.Bar.Misc import qs.Modules.Bar.Services
import qs.Services import qs.Services
MonitorItem { UProgressExpand {
property bool _showPercent: false property bool _showPercent: false
symbol: Icons.memory iconName: "database"
fillColor: Colors.green fillColor: Colors.mGreen
critical: SystemStatService.memPercent > 90 critical: SystemStatService.memPercent > 90
value: Math.round(SystemStatService.memPercent) value: Math.round(SystemStatService.memPercent)
maxValue: 100 maxValue: 100
textValue: _showPercent ? SystemStatService.memPercent : SystemStatService.memGb textValue: _showPercent ? SystemStatService.memPercent : SystemStatService.memGb
textSuffix: _showPercent ? "%" : "GB" textSuffix: _showPercent ? "%" : "GB"
onClicked: { onClicked: {
if (action.running) { MonitorProcess.toggle();
action.signal(15);
return ;
}
action.exec(["wezterm", "start", "--", "btop"]);
} }
onRightClicked: { onRightClicked: {
_showPercent = !_showPercent; _showPercent = !_showPercent;
} }
Process {
id: action
running: false
}
} }
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Components
import qs.Constants import qs.Constants
import qs.Services import qs.Services
@@ -16,35 +17,30 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
spacing: 5 spacing: 5
Text { UIcon {
text: Icons.download iconName: "arrow-big-down-line-filled"
font.pointSize: Fonts.icon - 3
color: Colors.primary
Layout.leftMargin: 10
} }
Text { Text {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.pointSize: Fonts.medium font.pointSize: Style.fontSizeM
font.family: Fonts.primary font.family: Fonts.primary
color: Colors.primary color: Colors.mPrimary
} }
Item { Item {
width: 5 width: 5
} }
Text { UIcon {
text: Icons.upload iconName: "arrow-big-up-line-filled"
font.pointSize: Fonts.icon - 3
color: Colors.primary
} }
Text { Text {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed) text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.pointSize: Fonts.medium font.pointSize: Style.fontSizeM
font.family: Fonts.primary font.family: Fonts.primary
color: Colors.primary color: Colors.mPrimary
} }
} }
@@ -1,18 +1,20 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Components
import qs.Constants import qs.Constants
import qs.Services import qs.Services
Item { Item {
id: root id: root
property color fillColor: Colors.red property color fillColor: Colors.mRed
property color _actualColor: Colors.red property color _actualColor: Colors.mRed
property bool _expand: mouseArea.containsMouse
visible: RecordService.isRecording visible: RecordService.isRecording
implicitHeight: parent.height implicitHeight: Math.max(symbolIcon.implicitHeight, textLabel.implicitHeight)
implicitWidth: layout.width + 10 implicitWidth: height + expander.implicitWidth
SequentialAnimation { SequentialAnimation {
id: blinkAnimation id: blinkAnimation
@@ -45,34 +47,36 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
spacing: 0 spacing: 0
Text { UIcon {
text: Icons.record id: symbolIcon
font.pointSize: 18
color: _actualColor iconName: "capture-filled"
iconSize: Style.fontSizeM + 12
color: root._actualColor
Layout.preferredWidth: parent.height
Layout.preferredHeight: parent.height
} }
Item { Item {
id: expander id: expander
implicitWidth: mouseArea.containsMouse ? ipText.implicitWidth + 10 : 0
implicitHeight: parent.height implicitHeight: parent.height
implicitWidth: root._expand ? textLabel.implicitWidth + 10 : 0
clip: true clip: true
Text { UText {
id: ipText id: textLabel
text: RecordService.recordingDisplay
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: fillColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 5 anchors.leftMargin: 5
text: RecordService.recordingDisplay || "Recording"
color: root.fillColor
} }
Behavior on implicitWidth { Behavior on implicitWidth {
NumberAnimation { NumberAnimation {
duration: Style.animationFast duration: Style.animationNormal
easing.type: Easing.InOutCubic 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.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Components
import qs.Constants import qs.Constants
import qs.Modules.Bar.Misc import qs.Modules.Bar.Components
Item { Item {
id: root id: root
property ShellScreen screen property ShellScreen screen
property int baseSize: Style.baseWidgetSize
implicitHeight: parent.height implicitWidth: baseSize + trayContainer.implicitWidth
implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight
RowLayout { RowLayout {
id: layout id: layout
@@ -20,9 +22,10 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
spacing: 0 spacing: 0
SymbolButton { UIconButton {
symbol: Icons.tray iconName: "layout-sidebar-right-expand-filled"
buttonColor: Colors.green colorFg: Colors.mGreen
baseSize: root.baseSize
disabledHover: true disabledHover: true
} }
@@ -41,7 +44,7 @@ Item {
Behavior on implicitWidth { Behavior on implicitWidth {
NumberAnimation { NumberAnimation {
duration: 200 duration: Style.animationNormal
easing.type: Easing.InOutCubic easing.type: Easing.InOutCubic
} }
@@ -1,12 +1,12 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Components
import qs.Constants import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services import qs.Services
MonitorItem { UProgressExpand {
symbol: AudioService.muted ? Icons.volumeMuted : (AudioService.volume >= 0.5 ? Icons.volumeHigh : (AudioService.volume >= 0.2 ? Icons.volumeMedium : Icons.volumeLow)) iconName: AudioService.muted ? "volume-3" : (AudioService.volume >= 0.5 ? "volume" : (AudioService.volume >= 0.2 ? "volume-2" : "volume-2"))
fillColor: Colors.lavender fillColor: Colors.mLavender
value: Math.round(AudioService.volume * 100) value: Math.round(AudioService.volume * 100)
maxValue: 100 maxValue: 100
textSuffix: "%" textSuffix: "%"
@@ -18,7 +18,7 @@ MonitorItem {
AudioService.decreaseVolume(); AudioService.decreaseVolume();
} }
onClicked: { onClicked: {
AudioService.toggleMute(); AudioService.setOutputMuted(!AudioService.muted);
} }
onRightClicked: { onRightClicked: {
Quickshell.execDetached(["sh", "-c", "pkill -x -n pwvucontrol || pwvucontrol"]); Quickshell.execDetached(["sh", "-c", "pkill -x -n pwvucontrol || pwvucontrol"]);
@@ -1,4 +1,3 @@
import Qt5Compat.GraphicalEffects
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
@@ -12,78 +11,59 @@ Item {
id: root id: root
required property ShellScreen screen required property ShellScreen screen
property bool hovered: false
property ListModel localWorkspaces property ListModel localWorkspaces
property real masterProgress: 0 property real masterProgress: 0
property bool effectsActive: false property bool effectsActive: false
property color effectColor: Colors.primary property color effectColor: Colors.mPrimary
property int horizontalPadding: 16 property int horizontalPadding: 16
property int spacingBetweenPills: 8 property int spacingBetweenPills: 8
property bool isDestroying: false
signal workspaceChanged(int workspaceId, color primaryColor)
function triggerUnifiedWave() { function triggerUnifiedWave() {
effectColor = Colors.primary; effectColor = Colors.mPrimary;
masterAnimation.restart(); masterAnimation.restart();
} }
function updateWorkspaceFocus() { function syncWorkspaces() {
for (let i = 0; i < localWorkspaces.count; i++) { let j = 0;
const ws = localWorkspaces.get(i); let focusChanged = false;
if (ws.isFocused === true) { for (let i = 0; i < Niri.workspaces.count; i++) {
root.triggerUnifiedWave(); const ws = Niri.workspaces.get(i);
root.workspaceChanged(ws.id, Colors.primary); if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
break; if (j < localWorkspaces.count) {
} const existing = localWorkspaces.get(j);
} if (ws.isFocused && !existing.isFocused)
} focusChanged = true;
implicitWidth: { localWorkspaces.setProperty(j, "id", ws.id);
let total = 0; localWorkspaces.setProperty(j, "idx", ws.idx);
for (let i = 0; i < localWorkspaces.count; i++) { localWorkspaces.setProperty(j, "isFocused", ws.isFocused);
const ws = localWorkspaces.get(i); localWorkspaces.setProperty(j, "isActive", ws.isActive);
if (ws.isFocused) localWorkspaces.setProperty(j, "isUrgent", ws.isUrgent);
total += 44; localWorkspaces.setProperty(j, "isOccupied", ws.isOccupied);
else if (ws.isActive) } else {
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.append(ws); localWorkspaces.append(ws);
if (ws.isFocused)
focusChanged = true;
} }
workspaceRepeater.model = localWorkspaces; j++;
updateWorkspaceFocus();
} }
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 { Connections {
function onWorkspacesChanged() { function onWorkspaceChanged() {
localWorkspaces.clear(); syncWorkspaces();
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();
} }
target: WorkspaceManager target: Niri
} }
SequentialAnimation { 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 { Row {
id: pillRow id: pillRow
spacing: spacingBetweenPills spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter anchors.centerIn: parent
width: root.width - horizontalPadding * 2
x: horizontalPadding
Repeater { Repeater {
id: workspaceRepeater id: workspaceRepeater
@@ -172,23 +128,18 @@ Item {
id: workspacePill id: workspacePill
anchors.fill: parent anchors.fill: parent
radius: { radius: height / 2
if (model.isFocused)
return 12;
else
return 6;
}
color: { color: {
if (model.isFocused) if (model.isFocused)
return Colors.primary; return Colors.mPrimary;
if (model.isActive) if (model.isActive)
return Colors.overlay2; return Colors.mOnSurfaceVariant;
if (model.isUrgent) if (model.isUrgent)
return Theme.error; return Theme.error;
return Colors.surface2; return Colors.mSurfaceVariant;
} }
scale: model.isFocused ? 1 : 0.9 scale: model.isFocused ? 1 : 0.9
z: 0 z: 0
@@ -199,28 +150,11 @@ Item {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
WorkspaceManager.switchToWorkspace(model.idx); Niri.switchToWorkspace(model);
} }
z: 20 z: 20
hoverEnabled: true 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 { Behavior on scale {
NumberAnimation { NumberAnimation {
@@ -246,14 +180,6 @@ Item {
} }
Behavior on radius {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
} }
Rectangle { Rectangle {
@@ -262,8 +188,8 @@ Item {
anchors.centerIn: workspacePillContainer anchors.centerIn: workspacePillContainer
width: workspacePillContainer.width + 18 * root.masterProgress width: workspacePillContainer.width + 18 * root.masterProgress
height: workspacePillContainer.height + 18 * root.masterProgress height: workspacePillContainer.height + 18 * root.masterProgress
radius: width / 2 radius: height / 2
color: "transparent" color: root.effectColor
border.color: root.effectColor border.color: root.effectColor
border.width: 2 + 6 * (1 - root.masterProgress) border.width: 2 + 6 * (1 - root.masterProgress)
opacity: root.effectsActive && model.isFocused ? (1 - root.masterProgress) * 0.7 : 0 opacity: root.effectsActive && model.isFocused ? (1 - root.masterProgress) * 0.7 : 0
@@ -6,7 +6,7 @@ pragma Singleton
Singleton { Singleton {
id: root id: root
property int count: 6 property int count: 7
property bool forceEnable: false property bool forceEnable: false
property bool forceDisable: false property bool forceDisable: false
property alias values: cavaProcess.values 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 concaveHeight: 60 * size
property int offsetX: -20 property int offsetX: -20
property int offsetY: -20 property int offsetY: -20
property color fillColor: Colors.base property color fillColor: Colors.mSurface
property int arcRadius: 20 * size property int arcRadius: 20 * size
property var modelData: null property var modelData: null
// Position flags derived from position string // Position flags derived from position string
@@ -10,12 +10,11 @@ import qs.Services
Scope { Scope {
id: rootScope id: rootScope
property var shell
property string namespace: "quickshell-corners" property string namespace: "quickshell-corners"
property int topMargin: 45 property int topMargin: 45
property int cornerHeight: 20 property int cornerHeight: 20
property real cornerSize: 1 property real cornerSize: 1
property real opacity: Niri.noFocus ? 0 : 1 property real opacity: BarService.focusMode ? 1 : 0
Item { Item {
id: cornersRootItem id: cornersRootItem
@@ -26,7 +25,15 @@ Scope {
model: Quickshell.screens model: Quickshell.screens
Item { Item {
id: screenItem
property var modelData 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 { PanelWindow {
id: fakeBar id: fakeBar
@@ -45,7 +52,7 @@ Scope {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Colors.base color: Colors.mSurface
opacity: rootScope.opacity opacity: rootScope.opacity
} }
@@ -59,9 +66,10 @@ Scope {
color: "transparent" color: "transparent"
screen: modelData screen: modelData
margins.top: topMargin margins.top: topMargin
margins.left: screenItem.leftOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: true
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Top
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: namespace WlrLayershell.namespace: namespace
implicitHeight: cornerHeight implicitHeight: cornerHeight
@@ -87,9 +95,10 @@ Scope {
color: "transparent" color: "transparent"
screen: modelData screen: modelData
margins.top: topMargin margins.top: topMargin
margins.right: screenItem.rightOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: true
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Top
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: namespace WlrLayershell.namespace: namespace
implicitHeight: cornerHeight implicitHeight: cornerHeight
@@ -114,9 +123,10 @@ Scope {
anchors.left: true anchors.left: true
color: "transparent" color: "transparent"
screen: modelData screen: modelData
margins.left: screenItem.leftOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: true
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Top
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: namespace WlrLayershell.namespace: namespace
implicitHeight: cornerHeight implicitHeight: cornerHeight
@@ -141,9 +151,10 @@ Scope {
anchors.right: true anchors.right: true
color: "transparent" color: "transparent"
screen: modelData screen: modelData
margins.right: screenItem.rightOffset
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true visible: true
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Top
aboveWindows: false aboveWindows: false
WlrLayershell.namespace: namespace WlrLayershell.namespace: namespace
implicitHeight: cornerHeight 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 { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: 1000 duration: Style.animationSlowest
easing.type: Easing.InOutCubic 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.Bluetooth
import Quickshell.Wayland import Quickshell.Wayland
import qs.Constants import qs.Constants
import qs.Noctalia import qs.Components
import qs.Services import qs.Services
ColumnLayout { ColumnLayout {
@@ -17,12 +17,12 @@ ColumnLayout {
} }
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM spacing: Style.marginS
NText { UText {
text: root.label text: root.label
pointSize: Style.fontSizeL pointSize: Style.fontSizeL
color: Color.mSecondary color: Colors.mPrimary
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
Layout.fillWidth: true Layout.fillWidth: true
visible: root.model.length > 0 visible: root.model.length > 0
@@ -35,39 +35,41 @@ ColumnLayout {
model: root.model model: root.model
visible: BluetoothService.adapter && BluetoothService.adapter.enabled visible: BluetoothService.adapter && BluetoothService.adapter.enabled
NBox { UBox {
id: device id: device
readonly property bool canConnect: BluetoothService.canConnect(modelData) readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData) readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(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) if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary; return Colors.mPrimary;
if (modelData.blocked) if (modelData.blocked)
return Color.mError; return Colors.mError;
return defaultColor; return defaultColor;
} }
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * 2) Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginS * 2)
RowLayout { RowLayout {
id: deviceLayout id: deviceLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM anchors.margins: Style.marginS
spacing: Style.marginM spacing: Style.marginS
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
// One device BT icon // One device BT icon
NIcon { UIcon {
icon: BluetoothService.getDeviceIcon(modelData) iconName: BluetoothService.getDeviceIcon(modelData)
pointSize: Style.fontSizeXXL iconSize: Style.fontSizeXXL
color: getContentColor(Color.mOnSurface) color: getContentColor(Colors.mOnSurface)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -76,21 +78,21 @@ ColumnLayout {
spacing: Style.marginXXS spacing: Style.marginXXS
// Device name // Device name
NText { UText {
text: modelData.name || modelData.deviceName text: modelData.name || modelData.deviceName
pointSize: Style.fontSizeM pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
elide: Text.ElideRight elide: Text.ElideRight
color: getContentColor(Color.mOnSurface) color: getContentColor(Colors.mOnSurface)
Layout.fillWidth: true Layout.fillWidth: true
} }
// Status // Status
NText { UText {
text: BluetoothService.getStatusString(modelData) text: BluetoothService.getStatusString(modelData)
visible: text !== "" visible: text !== ""
pointSize: Style.fontSizeXS pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant) color: getContentColor(Colors.mOnSurfaceVariant)
} }
// Signal Strength // Signal Strength
@@ -100,34 +102,34 @@ ColumnLayout {
spacing: Style.marginXS spacing: Style.marginXS
// Device signal strength - "Unknown" when not connected // Device signal strength - "Unknown" when not connected
NText { UText {
text: BluetoothService.getSignalStrength(modelData) text: BluetoothService.getSignalStrength(modelData)
pointSize: Style.fontSizeXS pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant) color: getContentColor(Colors.mOnSurfaceVariant)
} }
NIcon { UIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
icon: BluetoothService.getSignalIcon(modelData) iconName: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS iconSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface) color: getContentColor(Colors.mOnSurface)
} }
NText { UText {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
pointSize: Style.fontSizeXS pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface) color: getContentColor(Colors.mOnSurface)
} }
} }
// Battery // Battery
NText { UText {
visible: modelData.batteryAvailable visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData) text: BluetoothService.getBattery(modelData)
pointSize: Style.fontSizeXS pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant) color: getContentColor(Colors.mOnSurfaceVariant)
} }
} }
@@ -138,19 +140,18 @@ ColumnLayout {
} }
// Call to action // Call to action
NButton { UButton {
id: button id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting) visible: (modelData.state !== BluetoothDeviceState.Connecting)
enabled: (canConnect || canDisconnect) && !isBusy enabled: (canConnect || canDisconnect) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeXS fontSize: Style.fontSizeXS
fontWeight: Style.fontWeightMedium fontWeight: Style.fontWeightMedium
backgroundColor: { backgroundColor: {
if (device.canDisconnect && !isBusy) if (device.canDisconnect && !isBusy)
return Color.mError; return Colors.mError;
return Color.mPrimary; return Colors.mPrimary;
} }
text: { text: {
if (modelData.pairing) 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
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Components
import qs.Constants import qs.Constants
import qs.Noctalia
import qs.Services import qs.Services
import qs.Utils import qs.Utils
NBox { UBox {
id: lyricsBox id: lyricsBox
Component.onCompleted: { Component.onCompleted: {
@@ -25,13 +25,13 @@ NBox {
Repeater { Repeater {
model: LyricsService.lyrics model: LyricsService.lyrics
NText { UText {
Layout.fillWidth: true Layout.fillWidth: true
text: modelData 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.weight: index === LyricsService.currentIndex ? Style.fontWeightBold : Style.fontWeightRegular
font.family: Fonts.sans font.family: Fonts.sans
color: index === LyricsService.currentIndex ? Color.mOnSurface : Color.mOnSurfaceVariant color: index === LyricsService.currentIndex ? Colors.mOnSurface : Colors.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight 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