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