Compare commits

...

74 commits

Author SHA1 Message Date
1311192235 make NSpinBox border width not hardcoded 2025-09-06 22:00:38 +02:00
b03f877c27 Merge branch 'main' into never 2025-09-06 21:56:07 +02:00
LemmyCook
7860c41959 Network/Wi-Fi: Removed auto polling every 30sec. Factorized more code and cleaned logs 2025-09-06 14:14:47 -04:00
LemmyCook
fc1ee9fb2f Network/WiFi: improve UI with more immediate feedback on operations.
+ proper deletion of profiles when forgetting a network
2025-09-06 13:03:22 -04:00
LemmyCook
5bc8f410e7 Network/Wi-Fi: smarter logging to avoid flood 2025-09-06 09:32:02 -04:00
Ly-sec
3d9ef8c2ed switch to dev version 2025-09-06 14:20:31 +02:00
Ly-sec
0e53ce3ac0 Release v2.6.0
SettingsPanel: added keyboard navigation
BluetoothPanel: UI enhancements
WiFiPanel: UI enhancements
NotificationPanel: UI enhancements
ColorPicker: UI enhancements
Toast: handle switching between toasts much better
Notification: add DND option
Notification: add actions
LauncherTab: add app2unit toggle
Spacer: added spacer widget with configurable width
ActiveWindow: fix hyprland icon display
PowerPanel: add keybind controls
NetworkService: make it way more reliable

More QoL fixes & changes
2025-09-06 14:17:12 +02:00
Ly-sec
4131e6503b Implement keyboard controls for PowerPanel as requested in ##227
PowerPanel: add support for keyboard controls
2025-09-06 12:44:19 +02:00
Ly-sec
0aaf78fc51 ActiveWindow: fix hyprland icon display (fixes #201) 2025-09-06 12:40:29 +02:00
Ly-sec
977b2d9e7c Added a Spacer widget so people can add spacing between other widgets
(as requested in ##226).
Spacer: create variable width invisible rectangle
BarWidgetSettingsDialog: add Spacer support
BarWidgetRegistry: add Spacer
2025-09-06 12:27:06 +02:00
Ly-sec
e76b2c5497 Launcher: fix app2unit execution, implemented #202 2025-09-06 12:18:14 +02:00
Ly-sec
8658e11c1d NotificationHistoryPanel: fix layout alignment 2025-09-06 12:16:53 +02:00
LemmyCook
b3e4486699 Network: better refresh vs wifi scan 2025-09-06 01:14:40 -04:00
LemmyCook
2398961473 Wifi: more clean ups and improvements 2025-09-06 01:04:08 -04:00
LemmyCook
a57bfeba31 Background: Qt.callLater does not accept a delay as parameter. 2025-09-06 00:36:03 -04:00
LemmyCook
2f416a87f0 Wifi/Network: refactoring to something simpler to maintain 2025-09-06 00:02:32 -04:00
LemmyCook
9a6c98c134 WiFi: removed status indicator 2025-09-05 23:18:23 -04:00
LemmyCook
35ca346246 Tooltip 2025-09-05 23:17:18 -04:00
LemmyCook
0fd9ac15cd One more tooltip 2025-09-05 22:38:20 -04:00
LemmyCook
ae12d77e29 Tooltips: should end with a coma. 2025-09-05 22:37:54 -04:00
Lemmy
9065257961
Merge pull request #225 from lonerOrz/fix/keyboard-layout-alignment
fix: align KeyboardLayout widget with other bar components
2025-09-05 22:31:41 -04:00
LemmyCook
561b55cb9e Autoformatting 2025-09-05 22:18:08 -04:00
LemmyCook
4f871296ae ColorPicker: splitted in two NColorPicker + NColorPickerDialog
+ fixed layout and a few little bugs
2025-09-05 22:17:58 -04:00
loner
55b74ad38f
fix: align KeyboardLayout widget with other bar components 2025-09-06 09:46:42 +08:00
LemmyCook
8426e36f46 Time: improved human readable time + fixed a few tooltips. 2025-09-05 21:08:30 -04:00
LemmyCook
85d94aca01 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 21:04:09 -04:00
LemmyCook
39c7089cbc Notification: fixed persistent DND toast. 2025-09-05 21:04:02 -04:00
Ly-sec
eb072ff88a Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-06 02:16:13 +02:00
Ly-sec
0c4046b993 ColorPicker: UI overhaul 2025-09-06 02:15:51 +02:00
LemmyCook
90cd5467fe Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 19:57:24 -04:00
LemmyCook
05bfb6fc37 Do Not Disturb: factorized logic and toast in its proper service. 2025-09-05 19:57:22 -04:00
Lemmy
966b2410d3
Update README.md 2025-09-05 19:49:01 -04:00
LemmyCook
8ec1ad7255 TaskBar converted to Layout 2025-09-05 19:12:32 -04:00
LemmyCook
1efa1f4aa3 ActiveWindow: Converted to Layout 2025-09-05 19:06:15 -04:00
LemmyCook
0a48e5f34f Clock: text was too big 2025-09-05 18:59:53 -04:00
LemmyCook
ad305b3754 Dock: converted to Layout 2025-09-05 18:53:24 -04:00
LemmyCook
78cb7d4c15 MediaMini: fix visualizer not showing when track length is unknown (twitch) 2025-09-05 18:49:59 -04:00
LemmyCook
7b5c97f38a Tray: converted to Layout 2025-09-05 18:49:34 -04:00
Ly-sec
59bf98e04c Vesktop Template: fix placeholder text 2025-09-06 00:44:05 +02:00
LemmyCook
7feab63e5b Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 18:33:53 -04:00
LemmyCook
5d7e168a57 NCircleStat + KeyboardLayout: converted to Layout 2025-09-05 18:33:51 -04:00
Ly-sec
8038b7f6a0 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-06 00:31:28 +02:00
Ly-sec
2533c52e27 Launcher: add app2unit options (hopefully) 2025-09-06 00:30:47 +02:00
LemmyCook
cf624f4d65 Notification: Converted to Layout
+ removed fontPointSize on NIconButton. use sizeRatio instead.
2025-09-05 18:29:06 -04:00
LemmyCook
a4c98f1382 NotificationHistory: fully converted to Layout 2025-09-05 18:19:27 -04:00
LemmyCook
4768485974 LockScreen: converted to Layout 2025-09-05 18:15:28 -04:00
LemmyCook
9a14a5cc10 SettingsPanel: converted to layout 2025-09-05 18:05:42 -04:00
LemmyCook
cbffc1a14c SidePanel: height fix 2025-09-05 18:05:23 -04:00
Lysec
25e1c6e759
Merge pull request #224 from ThatOneCalculator/refactor/notification-default-action-text
make default notification action text "Open"
2025-09-05 23:50:06 +02:00
Kainoa Kanter
e41c35cb5b
make default notification action text "Open" 2025-09-05 14:47:32 -07:00
LemmyCook
078e111ecd Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:47:20 -04:00
LemmyCook
01aeceddf4 PowerPanel: converted to Layout 2025-09-05 17:47:19 -04:00
LemmyCook
93a3bc2090 SysMonCard: converted to layout 2025-09-05 17:45:17 -04:00
Ly-sec
28b0536916 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-05 23:44:52 +02:00
Ly-sec
86734f17c4 Notification: remove some logging, implement #223 2025-09-05 23:44:49 +02:00
LemmyCook
94293e4c63 Bar SysMon: converted to Layout 2025-09-05 17:44:04 -04:00
LemmyCook
f06d0f4e1e Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:32:54 -04:00
Ly-sec
a5fc9d9ca9 Notification: add actions
README: add fix for niri action buttons for notifications
2025-09-05 23:31:55 +02:00
LemmyCook
c85a309aeb MediaMini: converted to Layout 2025-09-05 17:23:02 -04:00
Ly-sec
4cac584409 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-05 23:07:10 +02:00
Ly-sec
b30d3df15c Notification: only display app icon/avatar if the notification requested it 2025-09-05 23:07:05 +02:00
LemmyCook
6f69654816 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:06:10 -04:00
LemmyCook
8fedd7612d NToast: Column => ColumnLayout 2025-09-05 17:06:09 -04:00
Ly-sec
c16e6e7423 Notification: adjust layout 2025-09-05 23:00:03 +02:00
Ly-sec
c8a056f332 Notification: add DND option to widget and notification panel as requested in #212 2025-09-05 22:42:40 +02:00
Ly-sec
60950fb461 dock: add opacity slider as requested in #222 2025-09-05 22:36:04 +02:00
Ly-sec
a3aba8d0db Toast: update visibility for newest toast 2025-09-05 22:29:20 +02:00
LemmyCook
f9a48becce SettingsPanel: finaly fixed the conflict between scrollview and textinput! 2025-09-05 15:47:07 -04:00
LemmyCook
3140039ccb NTextInput: simplified code in an attempt to fix text selection issues with mouse.
Not fixed yet, but I know where the conflict is!
2025-09-05 15:08:45 -04:00
LemmyCook
56fedcf495 HooksTab: removed ScrollView which already exists in parent (SettingsPanel.qml) 2025-09-05 15:07:31 -04:00
LemmyCook
783e9fb140 AboutTab: improved look of "Download latest release" 2025-09-05 14:46:08 -04:00
LemmyCook
b69d6f57d4 Bump dev version 2025-09-05 14:41:04 -04:00
LemmyCook
125d844e3b NInputAction simplification 2025-09-05 14:19:08 -04:00
LemmyCook
f04ac180f0 NInputAction: use proper label/description + autoformatting 2025-09-05 14:13:05 -04:00
59 changed files with 2911 additions and 2148 deletions

View file

@ -510,7 +510,7 @@
}
.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before {
content: "Message #general" !important;
content: "send a message" !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}

View file

@ -278,12 +278,14 @@ Singleton {
property string position: "center"
property real backgroundOpacity: 1.0
property list<string> pinnedExecs: []
property bool useApp2Unit: false
}
// dock
property JsonObject dock: JsonObject {
property bool autoHide: false
property bool exclusive: false
property real backgroundOpacity: 1.0
property list<string> monitors: []
}
@ -295,6 +297,7 @@ Singleton {
// notifications
property JsonObject notifications: JsonObject {
property bool doNotDisturb: false
property list<string> monitors: []
}

View file

@ -29,6 +29,7 @@ Singleton {
property int fontWeightBold: 700
// Radii
property int radiusXXS: 4 * Settings.data.general.radiusRatio
property int radiusXS: 8 * Settings.data.general.radiusRatio
property int radiusS: 12 * Settings.data.general.radiusRatio
property int radiusM: 16 * Settings.data.general.radiusRatio

View file

@ -78,23 +78,34 @@ Singleton {
}
// Format an easy to read approximate duration ex: 4h32m
// Used to display the time remaining on the Battery widget
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
}
var str = ""
if (hours) {
str += hours.toString() + "h"
}
if (minutes) {
str += minutes.toString() + "m"
}
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
str += seconds.toString() + "s"
parts.push(`${seconds}s`)
}
return str
return parts.join('')
}
Timer {

View file

@ -237,7 +237,7 @@ Variants {
transitionProgress = 0.0
Qt.callLater(() => {
currentWallpaper.asynchronous = true
}, 100)
})
}
}

View file

@ -2,38 +2,45 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
Row {
RowLayout {
id: root
property ShellScreen screen
property real scaling: 1.0
readonly property real minWidth: 160
readonly property real maxWidth: 400
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
function getTitle() {
// Use the service's focusedWindowTitle property which is updated immediately
// when WindowOpenedOrChanged events are received
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
}
function getAppIcon() {
// Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow()
if (!focusedWindow || !focusedWindow.appId)
return ""
if (focusedWindow && focusedWindow.appId) {
return Icons.iconForAppId(focusedWindow.appId.toLowerCase())
}
return Icons.iconForAppId(focusedWindow.appId)
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) {
return Icons.iconForAppId(activeToplevel.appId.toLowerCase())
}
}
return ""
}
// A hidden text element to safely measure the full title width
// A hidden text element to safely measure the full title width
NText {
id: fullTitleMetrics
visible: false
@ -43,15 +50,13 @@ Row {
}
Rectangle {
// Let the Rectangle size itself based on its content (the Row)
id: windowTitleRect
visible: root.visible
width: row.width + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
Item {
id: mainContainer
anchors.fill: parent
@ -59,16 +64,16 @@ Row {
anchors.rightMargin: Style.marginS * scaling
clip: true
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
RowLayout {
id: contentLayout
anchors.centerIn: parent
spacing: Style.marginS * scaling
// Window icon
Item {
width: Style.fontSizeL * scaling * 1.2
height: Style.fontSizeL * scaling * 1.2
anchors.verticalCenter: parent.verticalCenter
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
IconImage {
@ -83,26 +88,24 @@ Row {
NText {
id: titleText
// For short titles, show full. For long titles, truncate and expand on hover
width: {
Layout.preferredWidth: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
}
}
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mSecondary
clip: true
Behavior on width {
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
@ -120,4 +123,14 @@ Row {
}
}
}
Connections {
target: CompositorService
function onActiveWindowChanged() {
windowIcon.source = Qt.binding(getAppIcon)
}
function onWindowListChanged() {
windowIcon.source = Qt.binding(getAppIcon)
}
}
}

View file

@ -23,7 +23,7 @@ Rectangle {
NTooltip {
id: tooltip
text: Time.dateString
text: `${Time.dateString}.`
target: clock
positionAbove: Settings.data.bar.position === "bottom"
}

View file

@ -47,13 +47,13 @@ NIconButton {
} else {
var lines = []
if (userLeftClickExec !== "") {
lines.push(`Left click: <i>${userLeftClickExec}</i>`)
lines.push(`Left click: <i>${userLeftClickExec}</i>.`)
}
if (userRightClickExec !== "") {
lines.push(`Right click: <i>${userRightClickExec}</i>`)
lines.push(`Right click: <i>${userRightClickExec}</i>.`)
}
if (userMiddleClickExec !== "") {
lines.push(`Middle click: <i>${userMiddleClickExec}</i>`)
lines.push(`Middle click: <i>${userMiddleClickExec}</i>.`)
}
return lines.join("<br/>")
}

View file

@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
@ -6,7 +7,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
Row {
Item {
id: root
property ShellScreen screen
@ -18,12 +19,13 @@ Row {
// Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout
width: pill.width
height: pill.height
implicitWidth: pill.width
implicitHeight: pill.height
NPill {
id: pill
anchors.verticalCenter: parent.verticalCenter
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: "keyboard_alt"
iconCircleColor: Color.mPrimary

View file

@ -7,7 +7,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
Row {
RowLayout {
id: root
property ShellScreen screen
@ -15,10 +15,10 @@ Row {
readonly property real minWidth: 160
readonly property real maxWidth: 400
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: MediaService.currentPlayer !== null && MediaService.canPlay
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
@ -35,15 +35,13 @@ Row {
Rectangle {
id: mediaMini
// Let the Rectangle size itself based on its content (the Row)
width: row.width + Style.marginM * 2 * scaling
Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
// Used to anchor the tooltip, so the tooltip does not move when the content expands
Item {
id: anchor
@ -61,7 +59,7 @@ Row {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
&& MediaService.isPlaying && MediaService.trackLength > 0
&& MediaService.isPlaying
z: 0
sourceComponent: LinearSpectrum {
@ -71,42 +69,42 @@ Row {
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
&& MediaService.isPlaying
z: 0
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Row {
id: row
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
&& MediaService.isPlaying
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
z: 1 // Above the visualizer
@ -116,17 +114,18 @@ Row {
text: MediaService.isPlaying ? "pause" : "play_arrow"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
}
Column {
anchors.verticalCenter: parent.verticalCenter
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.audio.showMiniplayerAlbumArt
spacing: 0
Item {
width: Math.round(18 * scaling)
height: Math.round(18 * scaling)
Layout.preferredWidth: Math.round(18 * scaling)
Layout.preferredHeight: Math.round(18 * scaling)
NImageCircled {
id: trackArt
@ -142,23 +141,23 @@ Row {
NText {
id: titleText
// For short titles, show full. For long titles, truncate and expand on hover
width: {
Layout.preferredWidth: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
}
}
Layout.alignment: Qt.AlignVCenter
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mTertiary
Behavior on width {
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
@ -205,10 +204,10 @@ Row {
text: {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next\n"
str += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous\n"
str += "Middle click for previous."
}
return str
}

View file

@ -100,7 +100,7 @@ Item {
AudioService.setInputMuted(!AudioService.inputMuted)
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"]);
Quickshell.execDetached(["pwvucontrol"])
}
}
}

View file

@ -21,7 +21,7 @@ NIconButton {
colorBorderHover: Color.transparent
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled" : "disabled"}\nLeft click to toggle.\nRight click to access settings.`
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled." : "disabled."}\nLeft click to toggle.\nRight click to access settings.`
onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
onRightClicked: {

View file

@ -14,11 +14,14 @@ NIconButton {
property real scaling: 1.0
sizeRatio: 0.8
icon: "notifications"
tooltipText: "Notification history"
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this)
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}

View file

@ -12,7 +12,7 @@ NIconButton {
property real scaling: 1.0
icon: Settings.data.bar.useDistroLogo ? "" : "widgets"
tooltipText: "Open side panel"
tooltipText: "Open side panel."
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant

View file

@ -0,0 +1,56 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
// Widget properties passed from Bar.qml
property var screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Get user settings from Settings data - make it reactive
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Use settings or defaults from BarWidgetRegistry
readonly property int userWidth: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex].width || BarWidgetRegistry.widgetMetadata["Spacer"].width
}
}
return BarWidgetRegistry.widgetMetadata["Spacer"].width
}
// Set the width based on user settings
implicitWidth: userWidth * scaling
implicitHeight: Style.barHeight * scaling
width: implicitWidth
height: implicitHeight
// Optional: Add a subtle visual indicator in debug mode
Rectangle {
anchors.fill: parent
color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint
visible: Settings.data.general.debugMode || false
radius: 2 * scaling
}
}

View file

@ -1,145 +1,146 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
Row {
RowLayout {
id: root
property ShellScreen screen
property real scaling: 1.0
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
Rectangle {
// Let the Rectangle size itself based on its content (the Row)
width: row.width + Style.marginM * scaling * 2
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2
Layout.alignment: Qt.AlignVCenter
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
Item {
id: mainContainer
RowLayout {
id: mainLayout
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Row {
id: cpuUsageLayout
spacing: Style.marginXS * scaling
// CPU Usage Component
RowLayout {
id: cpuUsageLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NIcon {
id: cpuUsageIcon
text: "speed"
anchors.verticalCenter: parent.verticalCenter
}
NText {
id: cpuUsageText
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NIcon {
id: cpuUsageIcon
text: "speed"
Layout.alignment: Qt.AlignVCenter
}
// CPU Temperature Component
Row {
id: cpuTempLayout
// spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling
NText {
id: cpuUsageText
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon {
text: "thermometer"
anchors.verticalCenter: parent.verticalCenter
}
// CPU Temperature Component
RowLayout {
id: cpuTempLayout
// spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling
Layout.alignment: Qt.AlignVCenter
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NIcon {
text: "thermometer"
Layout.alignment: Qt.AlignVCenter
}
// Memory Usage Component
Row {
id: memoryUsageLayout
spacing: Style.marginXS * scaling
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon {
text: "memory"
anchors.verticalCenter: parent.verticalCenter
}
// Memory Usage Component
RowLayout {
id: memoryUsageLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NText {
text: `${SystemStatService.memoryUsageGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NIcon {
text: "memory"
Layout.alignment: Qt.AlignVCenter
}
// Network Download Speed Component
Row {
id: networkDownloadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NText {
text: `${SystemStatService.memoryUsageGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon {
text: "download"
anchors.verticalCenter: parent.verticalCenter
}
// Network Download Speed Component
RowLayout {
id: networkDownloadLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NIcon {
text: "download"
Layout.alignment: Qt.AlignVCenter
}
// Network Upload Speed Component
Row {
id: networkUploadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon {
text: "upload"
anchors.verticalCenter: parent.verticalCenter
}
// Network Upload Speed Component
RowLayout {
id: networkUploadLayout
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NIcon {
text: "upload"
Layout.alignment: Qt.AlignVCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}

View file

@ -2,6 +2,7 @@ pragma ComponentBehavior
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
@ -17,15 +18,14 @@ Rectangle {
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
// Always visible when there are toplevels
implicitWidth: taskbarRow.width + Style.marginM * scaling * 2
implicitWidth: taskbarLayout.implicitWidth + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
Row {
id: taskbarRow
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
RowLayout {
id: taskbarLayout
anchors.centerIn: parent
spacing: Style.marginXXS * root.scaling
Repeater {
@ -35,8 +35,10 @@ Rectangle {
required property Toplevel modelData
property Toplevel toplevel: modelData
property bool isActive: ToplevelManager.activeToplevel === modelData
width: root.itemSize
height: root.itemSize
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
Rectangle {
id: iconBackground
@ -89,7 +91,7 @@ Rectangle {
NTooltip {
id: taskbarTooltip
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App"
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
target: taskbarItem
positionAbove: Settings.data.bar.position === "bottom"
}

View file

@ -26,26 +26,26 @@ Rectangle {
}
visible: SystemTray.items.values.length > 0
implicitWidth: tray.width + Style.marginM * scaling * 2
implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
Layout.alignment: Qt.AlignVCenter
Row {
id: tray
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
RowLayout {
id: trayLayout
anchors.centerIn: parent
spacing: Style.marginS * scaling
Repeater {
id: repeater
model: SystemTray.items
delegate: Item {
width: itemSize
height: itemSize
Layout.preferredWidth: itemSize
Layout.preferredHeight: itemSize
Layout.alignment: Qt.AlignCenter
visible: modelData
IconImage {
@ -146,13 +146,14 @@ Rectangle {
function open() {
visible = true
PanelService.willOpenPanel(trayPanel)
}
function close() {
visible = false
trayMenu.item.hideMenu()
if (trayMenu.item) {
trayMenu.item.hideMenu()
}
}
// Clicking outside of the rectangle to close

View file

@ -63,8 +63,8 @@ Item {
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100) + "%"
tooltipText: "Volume: " + Math.round(
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
tooltipText: "Volume: " + Math.round(AudioService.volume * 100)
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
onWheel: function (delta) {
wheelAccumulator += delta
@ -85,7 +85,7 @@ Item {
AudioService.setMuted(!AudioService.muted)
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"]);
Quickshell.execDetached(["pwvucontrol"])
}
}
}

View file

@ -13,18 +13,8 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
visible: Settings.data.network.wifiEnabled
sizeRatio: 0.8
Component.onCompleted: {
Logger.log("WiFi", "Widget component completed")
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
if (NetworkService) {
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
}
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
@ -32,7 +22,7 @@ NIconButton {
icon: {
try {
if (NetworkService.ethernet) {
if (NetworkService.ethernetConnected) {
return "lan"
}
let connected = false
@ -46,10 +36,10 @@ NIconButton {
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
} catch (error) {
Logger.error("WiFi", "Error getting icon:", error)
Logger.error("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
tooltipText: "Network / Wi-Fi"
tooltipText: "Network / Wi-Fi."
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
}

View file

@ -11,7 +11,7 @@ import qs.Services
Item {
id: root
property ShellScreen screen: null
property ShellScreen screen
property real scaling: 1.0
property bool isDestroying: false

View file

@ -32,6 +32,8 @@ Variants {
screen: modelData
WlrLayershell.namespace: "noctalia-dock"
property bool autoHide: Settings.data.dock.autoHide
property bool hidden: autoHide
property int hideDelay: 500
@ -128,9 +130,9 @@ Variants {
Rectangle {
id: dockContainer
width: dock.width + 48 * scaling
width: dockLayout.implicitWidth + 48 * scaling
height: iconSize * 1.4 * scaling
color: Color.mSurface
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: dockSpacing
@ -176,7 +178,7 @@ Variants {
Item {
id: dock
width: runningAppsRow.width
width: dockLayout.implicitWidth
height: parent.height - (20 * scaling)
anchors.centerIn: parent
@ -192,10 +194,10 @@ Variants {
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
}
Row {
id: runningAppsRow
RowLayout {
id: dockLayout
spacing: Style.marginL * scaling
height: parent.height
Layout.preferredHeight: parent.height
anchors.centerIn: parent
Repeater {
@ -203,8 +205,10 @@ Variants {
delegate: Rectangle {
id: appButton
width: iconSize * scaling
height: iconSize * scaling
Layout.preferredWidth: iconSize * scaling
Layout.preferredHeight: iconSize * scaling
Layout.alignment: Qt.AlignCenter
color: Color.transparent
radius: Style.radiusM * scaling

View file

@ -38,7 +38,8 @@ Item {
function toggleHistory() {
notificationHistoryPanel.toggle(getActiveScreen())
}
function toggleDoNotDisturb() {// TODO
function toggleDND() {
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
}

View file

@ -243,52 +243,45 @@ NPanel {
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
Item {
id: searchInputWrap
NTextInput {
id: searchInput
Layout.fillWidth: true
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
NTextInput {
id: searchInput
anchors.fill: parent
inputMaxWidth: Number.MAX_SAFE_INTEGER
fontSize: Style.fontSizeL * scaling
fontWeight: Style.fontWeightSemiBold
fontSize: Style.fontSizeL * scaling
fontWeight: Style.fontWeightSemiBold
text: searchText
placeholderText: "Search entries... or use > for commands"
text: searchText
placeholderText: "Search entries... or use > for commands"
onTextChanged: searchText = text
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
// Override the TextField's default Home/End behavior
searchInput.inputItem.Keys.priority = Keys.BeforeItem
searchInput.inputItem.Keys.onPressed.connect(function (event) {
// Intercept Home and End BEFORE the TextField handles them
if (event.key === Qt.Key_Home) {
ui.selectFirst()
event.accepted = true
return
} else if (event.key === Qt.Key_End) {
ui.selectLast()
event.accepted = true
return
}
})
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
ui.selectNext()
})
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
ui.selectPrevious()
})
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
ui.activate()
})
}
// Override the TextField's default Home/End behavior
searchInput.inputItem.Keys.priority = Keys.BeforeItem
searchInput.inputItem.Keys.onPressed.connect(function (event) {
// Intercept Home and End BEFORE the TextField handles them
if (event.key === Qt.Key_Home) {
ui.selectFirst()
event.accepted = true
return
} else if (event.key === Qt.Key_End) {
ui.selectLast()
event.accepted = true
return
}
})
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
ui.selectNext()
})
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
ui.selectPrevious()
})
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
ui.activate()
})
}
}
}

View file

@ -82,7 +82,11 @@ Item {
"isImage": false,
"onActivate": function () {
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
if (app.execute) {
if (Settings.data.appLauncher.useApp2Unit && app.id) {
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
} else if (app.execute) {
app.execute()
} else if (app.exec) {
// Fallback to manual execution

View file

@ -155,7 +155,7 @@ Loader {
anchors.topMargin: 80 * scaling
spacing: 40 * scaling
Column {
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignHCenter
@ -168,6 +168,7 @@ Loader {
font.letterSpacing: -2 * scaling
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
SequentialAnimation on scale {
loops: Animation.Infinite
@ -192,22 +193,23 @@ Loader {
font.weight: Font.Light
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
width: timeText.width
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: timeText.implicitWidth
}
}
Column {
ColumnLayout {
spacing: Style.marginM * scaling
Layout.alignment: Qt.AlignHCenter
Rectangle {
width: 108 * scaling
height: 108 * scaling
Layout.preferredWidth: 108 * scaling
Layout.preferredHeight: 108 * scaling
Layout.alignment: Qt.AlignHCenter
radius: width * 0.5
color: Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderL * scaling)
anchors.horizontalCenter: parent.horizontalCenter
z: 10
Loader {
@ -375,377 +377,371 @@ Loader {
anchors.centerIn: parent
anchors.verticalCenterOffset: 50 * scaling
Item {
width: parent.width
height: 280 * scaling
Layout.fillWidth: true
Rectangle {
id: terminalBackground
anchors.fill: parent
radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
Repeater {
model: 20
Rectangle {
width: parent.width
height: 1
color: Qt.alpha(Color.mPrimary, 0.1)
y: index * 10 * scaling
opacity: Style.opacityMedium
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000 + Math.random() * 1000
}
NumberAnimation {
to: 0.1
duration: 2000 + Math.random() * 1000
}
}
}
}
Rectangle {
id: terminalBackground
anchors.fill: parent
radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
Repeater {
model: 20
Rectangle {
width: parent.width
height: 40 * scaling
color: Qt.alpha(Color.mPrimary, 0.2)
topLeftRadius: Style.radiusS * scaling
topRightRadius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.topMargin: Style.marginM * scaling
anchors.bottomMargin: Style.marginM * scaling
anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling
spacing: Style.marginM * scaling
NText {
text: "SECURE TERMINAL"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
Row {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: batteryIndicator.getIcon()
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
Row {
spacing: Style.marginS * scaling
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
NIcon {
text: "keyboard_alt"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
}
}
}
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Style.marginL * scaling
anchors.topMargin: 70 * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
id: welcomeText
text: ""
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (parent.currentIndex < parent.fullText.length) {
parent.text = parent.fullText.substring(0, parent.currentIndex + 1)
parent.currentIndex++
} else {
running = false
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
text: "sudo unlock-session"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
}
TextInput {
id: passwordInput
width: 0
height: 0
visible: false
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
echoMode: TextInput.Password
passwordCharacter: "*"
passwordMaskDelay: 0
text: lockContext.currentText
onTextChanged: {
lockContext.currentText = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lockContext.tryUnlock()
}
}
Component.onCompleted: {
forceActiveFocus()
}
}
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
}
}
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
Layout.leftMargin: -Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
}
}
}
NText {
text: {
if (lockContext.unlockInProgress)
return "Authenticating..."
if (lockContext.showFailure && lockContext.errorMessage)
return lockContext.errorMessage
if (lockContext.showFailure)
return "Authentication failed."
return ""
}
color: {
if (lockContext.unlockInProgress)
return Color.mPrimary
if (lockContext.showFailure)
return Color.mError
return Color.transparent
}
font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeL * scaling
Layout.fillWidth: true
SequentialAnimation on opacity {
running: lockContext.unlockInProgress
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 800
}
NumberAnimation {
to: 0.5
duration: 800
}
}
}
Row {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Rectangle {
width: 120 * scaling
height: 40 * scaling
radius: Style.radiusS * scaling
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
enabled: !lockContext.unlockInProgress
NText {
anchors.centerIn: parent
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
MouseArea {
id: executeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
lockContext.tryUnlock()
}
SequentialAnimation on scale {
running: executeButtonArea.containsMouse
NumberAnimation {
to: 1.05
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
SequentialAnimation on scale {
running: !executeButtonArea.containsMouse
NumberAnimation {
to: 1.0
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: lockContext.unlockInProgress
NumberAnimation {
to: 1.02
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
}
}
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: Qt.alpha(Color.mPrimary, 0.3)
border.width: Math.max(1, Style.borderS * scaling)
z: -1
height: 1
color: Qt.alpha(Color.mPrimary, 0.1)
y: index * 10 * scaling
opacity: Style.opacityMedium
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000
easing.type: Easing.InOutQuad
duration: 2000 + Math.random() * 1000
}
NumberAnimation {
to: 0.2
duration: 2000
easing.type: Easing.InOutQuad
to: 0.1
duration: 2000 + Math.random() * 1000
}
}
}
}
Rectangle {
width: parent.width
height: 40 * scaling
color: Qt.alpha(Color.mPrimary, 0.2)
topLeftRadius: Style.radiusS * scaling
topRightRadius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.topMargin: Style.marginM * scaling
anchors.bottomMargin: Style.marginM * scaling
anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling
spacing: Style.marginM * scaling
NText {
text: "SECURE TERMINAL"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: batteryIndicator.getIcon()
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
RowLayout {
spacing: Style.marginS * scaling
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
NIcon {
text: "keyboard_alt"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
}
}
}
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Style.marginL * scaling
anchors.topMargin: 70 * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
id: welcomeText
text: ""
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (parent.currentIndex < parent.fullText.length) {
parent.text = parent.fullText.substring(0, parent.currentIndex + 1)
parent.currentIndex++
} else {
running = false
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
text: "sudo unlock-session"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
}
TextInput {
id: passwordInput
width: 0
height: 0
visible: false
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
echoMode: TextInput.Password
passwordCharacter: "*"
passwordMaskDelay: 0
text: lockContext.currentText
onTextChanged: {
lockContext.currentText = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lockContext.tryUnlock()
}
}
Component.onCompleted: {
forceActiveFocus()
}
}
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
}
}
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
Layout.leftMargin: -Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
}
}
}
NText {
text: {
if (lockContext.unlockInProgress)
return "Authenticating..."
if (lockContext.showFailure && lockContext.errorMessage)
return lockContext.errorMessage
if (lockContext.showFailure)
return "Authentication failed."
return ""
}
color: {
if (lockContext.unlockInProgress)
return Color.mPrimary
if (lockContext.showFailure)
return Color.mError
return Color.transparent
}
font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeL * scaling
Layout.fillWidth: true
SequentialAnimation on opacity {
running: lockContext.unlockInProgress
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 800
}
NumberAnimation {
to: 0.5
duration: 800
}
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Rectangle {
Layout.preferredWidth: 120 * scaling
Layout.preferredHeight: 40 * scaling
radius: Style.radiusS * scaling
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
enabled: !lockContext.unlockInProgress
NText {
anchors.centerIn: parent
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
MouseArea {
id: executeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
lockContext.tryUnlock()
}
SequentialAnimation on scale {
running: executeButtonArea.containsMouse
NumberAnimation {
to: 1.05
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
SequentialAnimation on scale {
running: !executeButtonArea.containsMouse
NumberAnimation {
to: 1.0
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: lockContext.unlockInProgress
NumberAnimation {
to: 1.02
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
}
}
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: Qt.alpha(Color.mPrimary, 0.3)
border.width: Math.max(1, Style.borderS * scaling)
z: -1
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 0.2
duration: 2000
easing.type: Easing.InOutQuad
}
}
}
}
}
// Power buttons at bottom
Row {
// Power buttons at bottom right
RowLayout {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 50 * scaling
spacing: 20 * scaling
Rectangle {
width: 60 * scaling
height: 60 * scaling
Layout.preferredWidth: 60 * scaling
Layout.preferredHeight: 60 * scaling
radius: width * 0.5
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
border.color: Color.mError
@ -769,8 +765,8 @@ Loader {
}
Rectangle {
width: 60 * scaling
height: 60 * scaling
Layout.preferredWidth: 60 * scaling
Layout.preferredHeight: 60 * scaling
radius: width * 0.5
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
border.color: Color.mPrimary
@ -794,8 +790,8 @@ Loader {
}
Rectangle {
width: 60 * scaling
height: 60 * scaling
Layout.preferredWidth: 60 * scaling
Layout.preferredHeight: 60 * scaling
radius: width * 0.5
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
border.color: Color.mSecondary

View file

@ -78,7 +78,7 @@ Variants {
}
// Main notification container
Column {
ColumnLayout {
id: notificationStack
// Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
@ -92,8 +92,9 @@ Variants {
Repeater {
model: notificationModel
delegate: Rectangle {
width: 360 * scaling
height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling))
Layout.preferredWidth: 360 * scaling
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
Layout.maximumHeight: Layout.preferredHeight
clip: true
radius: Style.radiusL * scaling
border.color: Color.mOutline
@ -105,6 +106,17 @@ Variants {
property real opacityValue: 0.0
property bool isRemoving: false
// Right-click to dismiss
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton) {
animateOut()
}
}
}
// Scale and fade-in animation
scale: scaleValue
opacity: opacityValue
@ -156,104 +168,139 @@ Variants {
}
}
RowLayout {
id: contentRow
ColumnLayout {
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginL * scaling
anchors.margins: Style.marginM * scaling
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
spacing: Style.marginM * scaling
// Right: header on top, then avatar + texts
ColumnLayout {
id: textColumn
spacing: Style.marginS * scaling
// Header section with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
RowLayout {
spacing: Style.marginS * scaling
id: appHeaderRow
NText {
text: `${(model.appName || model.desktopEntry)
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Rectangle {
width: 6 * scaling
height: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NText {
text: `${(model.appName || model.desktopEntry)
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
RowLayout {
id: bodyRow
spacing: Style.marginM * scaling
Rectangle {
Layout.preferredWidth: 6 * scaling
Layout.preferredHeight: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
NImageCircled {
id: appAvatar
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
// Start avatar aligned with body (below the summary)
anchors.topMargin: textContent.childrenRect.y
// Prefer notification-provided image (e.g., user avatar) then fall back to app icon
imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName(
model.appIcon, "application-x-executable")
fallbackIcon: "apps"
borderColor: Color.transparent
borderWidth: 0
visible: (imagePath && imagePath !== "")
Item {
Layout.fillWidth: true
}
}
// Main content section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Avatar
NImageCircled {
id: appAvatar
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
imagePath: model.image && model.image !== "" ? model.image : ""
fallbackIcon: ""
borderColor: Color.transparent
borderWidth: 0
visible: (model.image && model.image !== "")
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
}
Column {
id: textContent
spacing: Style.marginS * scaling
NText {
text: model.body || ""
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
// Ensure a concrete width so text wraps
width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0))
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
width: parent.width
maximumLineCount: 3
elide: Text.ElideRight
}
NText {
text: model.body || ""
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
width: parent.width
maximumLineCount: 5
elide: Text.ElideRight
}
maximumLineCount: 5
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Actions removed
// Notification actions
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
visible: model.rawNotification && model.rawNotification.actions
&& model.rawNotification.actions.length > 0
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
Repeater {
model: parent.notificationActions
delegate: NButton {
text: {
var actionText = modelData.text || "Open"
// If text contains comma, take the part after the comma (the display text)
if (actionText.includes(",")) {
return actionText.split(",")[1] || actionText
}
return actionText
}
fontSize: Style.fontSizeS * scaling
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
hoverColor: Color.mSecondary
pressColor: Color.mTertiary
outlined: false
customHeight: 32 * scaling
Layout.preferredHeight: 32 * scaling
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
}
}
}
}
// Spacer to push buttons to the left if needed
Item {
Layout.fillWidth: true
}
}
}
// Close button positioned absolutely
NIconButton {
icon: "close"
tooltipText: "Close"
// Compact target (~24dp) and glyph (~16dp)
sizeRatio: 0.75
fontPointSize: 16
sizeRatio: 0.6
anchors.top: parent.top
anchors.topMargin: Style.marginM * scaling
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
anchors.rightMargin: Style.marginM * scaling
onClicked: {
animateOut()

View file

@ -25,6 +25,7 @@ NPanel {
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
@ -43,6 +44,13 @@ NPanel {
Layout.fillWidth: true
}
NIconButton {
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications_active"
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
sizeRatio: 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
NIconButton {
icon: "delete"
tooltipText: "Clear history"
@ -65,38 +73,44 @@ NPanel {
}
// Empty state when no notifications
Item {
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.historyModel.count === 0
spacing: Style.marginL * scaling
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
}
NIcon {
text: "notifications_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NIcon {
text: "notifications_off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Notification list
ListView {
id: notificationList
Layout.fillWidth: true
@ -108,21 +122,21 @@ NPanel {
visible: NotificationService.historyModel.count > 0
delegate: Rectangle {
width: notificationList ? notificationList.width : 380 * scaling
height: Math.max(80, notificationContent.height + 30)
width: notificationList.width
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors {
fill: parent
margins: Style.marginM * scaling
}
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
// Notification content
Column {
id: notificationContent
// Notification content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXXS * scaling
@ -133,7 +147,8 @@ NPanel {
font.weight: Font.Medium
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
wrapMode: Text.Wrap
width: parent.width - 60
Layout.fillWidth: true
Layout.maximumWidth: parent.width
maximumLineCount: 2
elide: Text.ElideRight
}
@ -143,23 +158,27 @@ NPanel {
font.pointSize: Style.fontSizeXS * scaling
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
wrapMode: Text.Wrap
width: parent.width - 60
Layout.fillWidth: true
Layout.maximumWidth: parent.width
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
}
NText {
text: NotificationService.formatTimestamp(timestamp)
font.pointSize: Style.fontSizeXS * scaling
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
Layout.fillWidth: true
}
}
// Trash icon button
// Delete button
NIconButton {
icon: "delete"
tooltipText: "Delete notification"
sizeRatio: 0.7
Layout.alignment: Qt.AlignTop
onClicked: {
Logger.log("NotificationHistory", "Removing notification:", summary)
@ -172,7 +191,7 @@ NPanel {
MouseArea {
id: notificationMouseArea
anchors.fill: parent
anchors.rightMargin: Style.marginL * 3 * scaling
anchors.rightMargin: Style.marginXL * scaling
hoverEnabled: true
}
}

View file

@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
@ -16,6 +17,7 @@ NPanel {
panelHeight: 380 * scaling
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Timer properties
property int timerDuration: 9000 // 9 seconds
@ -23,9 +25,44 @@ NPanel {
property bool timerActive: false
property int timeRemaining: 0
// Cancel timer when panel is closing
// Navigation properties
property int selectedIndex: 0
readonly property var powerOptions: [{
"action": "lock",
"icon": "lock_outline",
"title": "Lock",
"subtitle": "Lock your session"
}, {
"action": "suspend",
"icon": "bedtime",
"title": "Suspend",
"subtitle": "Put the system to sleep"
}, {
"action": "reboot",
"icon": "refresh",
"title": "Reboot",
"subtitle": "Restart the system"
}, {
"action": "logout",
"icon": "exit_to_app",
"title": "Logout",
"subtitle": "End your session"
}, {
"action": "shutdown",
"icon": "power_settings_new",
"title": "Shutdown",
"subtitle": "Turn off the system",
"isShutdown": true
}]
// Lifecycle handlers
onOpened: {
selectedIndex = 0
}
onClosed: {
cancelTimer()
selectedIndex = 0
}
// Timer management
@ -79,6 +116,38 @@ NPanel {
root.close()
}
// Navigation functions
function selectNext() {
if (powerOptions.length > 0) {
selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1)
}
}
function selectPrevious() {
if (powerOptions.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function selectFirst() {
selectedIndex = 0
}
function selectLast() {
if (powerOptions.length > 0) {
selectedIndex = powerOptions.length - 1
} else {
selectedIndex = 0
}
}
function activate() {
if (powerOptions.length > 0 && powerOptions[selectedIndex]) {
const option = powerOptions[selectedIndex]
startTimer(option.action)
}
}
// Countdown timer
Timer {
id: countdownTimer
@ -93,8 +162,92 @@ NPanel {
}
panelContent: Rectangle {
id: ui
color: Color.transparent
// Keyboard shortcuts
Shortcut {
sequence: "Ctrl+K"
onActivated: ui.selectPrevious()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
onActivated: ui.selectNext()
enabled: root.opened
}
Shortcut {
sequence: "Up"
onActivated: ui.selectPrevious()
enabled: root.opened
}
Shortcut {
sequence: "Down"
onActivated: ui.selectNext()
enabled: root.opened
}
Shortcut {
sequence: "Home"
onActivated: ui.selectFirst()
enabled: root.opened
}
Shortcut {
sequence: "End"
onActivated: ui.selectLast()
enabled: root.opened
}
Shortcut {
sequence: "Return"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Enter"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Escape"
onActivated: {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
root.close()
}
}
enabled: root.opened
}
// Navigation functions
function selectNext() {
root.selectNext()
}
function selectPrevious() {
root.selectPrevious()
}
function selectFirst() {
root.selectFirst()
}
function selectLast() {
root.selectLast()
}
function activate() {
root.activate()
}
ColumnLayout {
anchors.fill: parent
anchors.topMargin: Style.marginL * scaling
@ -144,55 +297,21 @@ NPanel {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Lock Screen
PowerButton {
Layout.fillWidth: true
icon: "lock_outline"
title: "Lock"
subtitle: "Lock your session"
onClicked: startTimer("lock")
pending: timerActive && pendingAction === "lock"
}
// Suspend
PowerButton {
Layout.fillWidth: true
icon: "bedtime"
title: "Suspend"
subtitle: "Put the system to sleep"
onClicked: startTimer("suspend")
pending: timerActive && pendingAction === "suspend"
}
// Reboot
PowerButton {
Layout.fillWidth: true
icon: "refresh"
title: "Reboot"
subtitle: "Restart the system"
onClicked: startTimer("reboot")
pending: timerActive && pendingAction === "reboot"
}
// Logout
PowerButton {
Layout.fillWidth: true
icon: "exit_to_app"
title: "Logout"
subtitle: "End your session"
onClicked: startTimer("logout")
pending: timerActive && pendingAction === "logout"
}
// Shutdown
PowerButton {
Layout.fillWidth: true
icon: "power_settings_new"
title: "Shutdown"
subtitle: "Turn off the system"
onClicked: startTimer("shutdown")
pending: timerActive && pendingAction === "shutdown"
isShutdown: true
Repeater {
model: powerOptions
delegate: PowerButton {
Layout.fillWidth: true
icon: modelData.icon
title: modelData.title
subtitle: modelData.subtitle
isShutdown: modelData.isShutdown || false
isSelected: index === selectedIndex
onClicked: {
selectedIndex = index
startTimer(modelData.action)
}
pending: timerActive && pendingAction === modelData.action
}
}
}
}
@ -207,6 +326,7 @@ NPanel {
property string subtitle: ""
property bool pending: false
property bool isShutdown: false
property bool isSelected: false
signal clicked
@ -216,7 +336,7 @@ NPanel {
if (pending) {
return Qt.alpha(Color.mPrimary, 0.08)
}
if (mouseArea.containsMouse) {
if (isSelected || mouseArea.containsMouse) {
return Color.mSecondary
}
return Color.transparent
@ -242,13 +362,12 @@ NPanel {
anchors.verticalCenter: parent.verticalCenter
text: buttonRoot.icon
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
return Color.mOnTertiary
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnSecondary
return Color.mOnSurface
}
font.pointSize: Style.fontSizeXXXL * scaling
@ -264,7 +383,7 @@ NPanel {
}
// Text content in the middle
Column {
ColumnLayout {
anchors.left: iconElement.right
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
anchors.verticalCenter: parent.verticalCenter
@ -279,10 +398,10 @@ NPanel {
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
return Color.mOnTertiary
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnSecondary
return Color.mOnSurface
}
@ -304,10 +423,10 @@ NPanel {
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
return Color.mOnTertiary
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnSecondary
return Color.mOnSurfaceVariant
}
opacity: Style.opacityHeavy

View file

@ -68,6 +68,8 @@ Popup {
sourceComponent: {
if (settingsPopup.widgetId === "CustomButton") {
return customButtonSettings
} else if (settingsPopup.widgetId === "Spacer") {
return spacerSettings
}
// Add more widget settings components here as needed
return null
@ -157,4 +159,28 @@ Popup {
}
}
}
// Spacer settings component
Component {
id: spacerSettings
ColumnLayout {
spacing: Style.marginM * scaling
function saveSettings() {
var settings = Object.assign({}, settingsPopup.widgetData)
settings.width = parseInt(widthInput.text) || 20
return settings
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: "Width (pixels)"
description: "Width of the spacer in pixels."
text: settingsPopup.widgetData.width || "20"
placeholderText: "Enter width in pixels"
}
}
}
}

View file

@ -267,234 +267,269 @@ NPanel {
}
panelContent: Rectangle {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
color: Color.transparent
// Scrolling via keyboard
Shortcut {
sequence: "Down"
onActivated: root.scrollDown()
enabled: root.opened
}
Shortcut {
sequence: "Up"
onActivated: root.scrollUp()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
onActivated: root.scrollDown()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+K"
onActivated: root.scrollUp()
enabled: root.opened
}
Shortcut {
sequence: "PgDown"
onActivated: root.scrollPageDown()
enabled: root.opened
}
Shortcut {
sequence: "PgUp"
onActivated: root.scrollPageUp()
enabled: root.opened
}
// Changing tab via keyboard
Shortcut {
sequence: "Tab"
onActivated: root.selectNextTab()
enabled: root.opened
}
Shortcut {
sequence: "Shift+Tab"
onActivated: root.selectPreviousTab()
enabled: root.opened
}
RowLayout {
// Main layout container that fills the panel
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
anchors.margins: Style.marginL * scaling
spacing: 0
Rectangle {
id: sidebar
Layout.preferredWidth: 220 * scaling
Layout.fillHeight: true
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
// Keyboard shortcuts container
Item {
Layout.preferredWidth: 0
Layout.preferredHeight: 0
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton // Don't interfere with clicks
property int wheelAccumulator: 0
onWheel: wheel => {
wheelAccumulator += wheel.angleDelta.y
if (wheelAccumulator >= 120) {
root.selectPreviousTab()
wheelAccumulator = 0
} else if (wheelAccumulator <= -120) {
root.selectNextTab()
wheelAccumulator = 0
}
wheel.accepted = true
}
// Scrolling via keyboard
Shortcut {
sequence: "Down"
onActivated: root.scrollDown()
enabled: root.opened
}
Column {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling
Shortcut {
sequence: "Up"
onActivated: root.scrollUp()
enabled: root.opened
}
Repeater {
id: sections
model: root.tabsModel
delegate: Rectangle {
id: tabItem
width: parent.width
height: 32 * scaling
radius: Style.radiusS * scaling
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
Shortcut {
sequence: "Ctrl+J"
onActivated: root.scrollDown()
enabled: root.opened
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: root.scrollUp()
enabled: root.opened
}
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
}
Shortcut {
sequence: "PgDown"
onActivated: root.scrollPageDown()
enabled: root.opened
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
// Tab icon on the left side
NIcon {
text: modelData.icon
color: tabTextColor
font.pointSize: Style.fontSizeL * scaling
}
// Tab label on the left side
NText {
text: modelData.label
color: tabTextColor
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
}
}
Shortcut {
sequence: "PgUp"
onActivated: root.scrollPageUp()
enabled: root.opened
}
// Changing tab via keyboard
Shortcut {
sequence: "Tab"
onActivated: root.selectNextTab()
enabled: root.opened
}
Shortcut {
sequence: "Shift+Tab"
onActivated: root.selectPreviousTab()
enabled: root.opened
}
}
// Content
Rectangle {
id: contentPane
// Main content area
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
spacing: Style.marginM * scaling
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling
// Sidebar
Rectangle {
id: sidebar
Layout.preferredWidth: 220 * scaling
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
RowLayout {
id: headerRow
Layout.fillWidth: true
spacing: Style.marginS * scaling
// Tab label on the main right side
NText {
text: root.tabsModel[currentTabIndex].label
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton // Don't interfere with clicks
property int wheelAccumulator: 0
onWheel: wheel => {
wheelAccumulator += wheel.angleDelta.y
if (wheelAccumulator >= 120) {
root.selectPreviousTab()
wheelAccumulator = 0
} else if (wheelAccumulator <= -120) {
root.selectNextTab()
wheelAccumulator = 0
}
wheel.accepted = true
}
}
NDivider {
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling
Repeater {
id: sections
model: root.tabsModel
delegate: Loader {
anchors.fill: parent
active: index === root.currentTabIndex
delegate: Rectangle {
id: tabItem
Layout.fillWidth: true
Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginS * scaling * 2
radius: Style.radiusS * scaling
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
onStatusChanged: {
if (status === Loader.Ready && item) {
// Find and store reference to the ScrollView
const scrollView = item.children[0]
if (scrollView && scrollView.toString().includes("ScrollView")) {
root.activeScrollView = scrollView
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
sourceComponent: ColumnLayout {
ScrollView {
id: scrollView
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
id: tabEntryRow
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
// Tab icon
NIcon {
text: modelData.icon
color: tabTextColor
font.pointSize: Style.fontSizeL * scaling
}
// Tab label
NText {
text: modelData.label
color: tabTextColor
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
}
}
Component.onCompleted: {
root.activeScrollView = scrollView
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
}
}
Item {
Layout.fillHeight: true
}
}
}
// Content pane
Rectangle {
id: contentPane
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling
// Header row
RowLayout {
id: headerRow
Layout.fillWidth: true
spacing: Style.marginS * scaling
// Tab title
NText {
text: root.tabsModel[currentTabIndex]?.label || ""
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
// Close button
NIconButton {
icon: "close"
tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}
}
// Divider
NDivider {
Layout.fillWidth: true
}
// Tab content area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.transparent
Repeater {
model: root.tabsModel
delegate: Loader {
anchors.fill: parent
active: index === root.currentTabIndex
onStatusChanged: {
if (status === Loader.Ready && item) {
// Find and store reference to the ScrollView
const scrollView = item.children[0]
if (scrollView && scrollView.toString().includes("ScrollView")) {
root.activeScrollView = scrollView
}
}
}
Loader {
active: true
sourceComponent: root.tabsModel[index].source
width: scrollView.availableWidth
sourceComponent: Flickable {
// Using a Flickable here with a pressDelay to fix conflict between
// ScrollView and NTextInput. This fixes the weird text selection issue.
id: flickable
anchors.fill: parent
pressDelay: 200
ScrollView {
id: scrollView
anchors.fill: parent
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
Component.onCompleted: {
root.activeScrollView = scrollView
}
Loader {
active: true
sourceComponent: root.tabsModel[index]?.source
width: scrollView.availableWidth
}
}
}
}

View file

@ -60,7 +60,7 @@ ColumnLayout {
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginS * scaling
Layout.preferredWidth: updateText.implicitWidth + 46 * scaling
Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2))
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusL * scaling
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
@ -85,11 +85,12 @@ ColumnLayout {
}
RowLayout {
id: updateRow
anchors.centerIn: parent
spacing: Style.marginS * scaling
NIcon {
text: "system_update"
text: "download"
font.pointSize: Style.fontSizeXXL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
}

View file

@ -22,9 +22,11 @@ ColumnLayout {
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
Layout.alignment: Qt.AlignTop
}
NTextInput {
Layout.fillWidth: true
label: `${Quickshell.env("USER") || "user"}'s profile picture`
description: "Your profile picture that appears throughout the interface."
text: Settings.data.general.avatarImage
@ -75,6 +77,45 @@ ColumnLayout {
onToggled: checked => Settings.data.dock.autoHide = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Dock Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the dock."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.dock.backgroundOpacity
onMoved: Settings.data.dock.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true

View file

@ -5,94 +5,85 @@ import qs.Commons
import qs.Services
import qs.Widgets
ScrollView {
id: root
ColumnLayout {
id: contentColumn
spacing: Style.marginL * scaling
width: root.width
property real scaling: 1.0
contentWidth: contentColumn.width
contentHeight: contentColumn.height
// Enable/Disable Toggle
NToggle {
label: "Enable Hooks"
description: "Enable or disable all hook commands."
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked
}
ColumnLayout {
id: contentColumn
visible: Settings.data.hooks.enabled
spacing: Style.marginL * scaling
width: root.width
Layout.fillWidth: true
// Enable/Disable Toggle
NToggle {
label: "Enable Hooks"
description: "Enable or disable all hook commands."
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked
NDivider {
Layout.fillWidth: true
}
// Wallpaper Hook Section
NInputAction {
id: wallpaperHookInput
label: "Wallpaper Change Hook"
description: "Command to be executed when wallpaper changes."
placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\""
text: Settings.data.hooks.wallpaperChange
onEditingFinished: {
Settings.data.hooks.wallpaperChange = wallpaperHookInput.text
}
onActionClicked: {
if (wallpaperHookInput.text) {
HooksService.executeWallpaperHook("test", "test-screen")
}
}
Layout.fillWidth: true
}
NDivider {
Layout.fillWidth: true
}
// Dark Mode Hook Section
NInputAction {
id: darkModeHookInput
label: "Theme Toggle Hook"
description: "Command to be executed when theme toggles between dark and light mode."
placeholderText: "e.g., notify-send \"Theme\" \"Toggled\""
text: Settings.data.hooks.darkModeChange
onEditingFinished: {
Settings.data.hooks.darkModeChange = darkModeHookInput.text
}
onActionClicked: {
if (darkModeHookInput.text) {
HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode)
}
}
Layout.fillWidth: true
}
NDivider {
Layout.fillWidth: true
}
// Info section
ColumnLayout {
visible: Settings.data.hooks.enabled
spacing: Style.marginL * scaling
spacing: Style.marginM * scaling
Layout.fillWidth: true
NDivider {
Layout.fillWidth: true
NLabel {
label: "Hook Command Information"
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
}
// Wallpaper Hook Section
NInputAction {
id: wallpaperHookInput
label: "Wallpaper Change Hook"
description: "Command to be executed when wallpaper changes."
placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\""
text: Settings.data.hooks.wallpaperChange
onEditingFinished: {
Settings.data.hooks.wallpaperChange = wallpaperHookInput.text
}
onActionClicked: {
if (wallpaperHookInput.text) {
HooksService.executeWallpaperHook("test", "test-screen")
}
}
Layout.fillWidth: true
}
NDivider {
Layout.fillWidth: true
}
// Dark Mode Hook Section
NInputAction {
id: darkModeHookInput
label: "Theme Toggle Hook"
description: "Command to be executed when theme toggles between dark and light mode."
placeholderText: "e.g., notify-send \"Theme\" \"Toggled\""
text: Settings.data.hooks.darkModeChange
onEditingFinished: {
Settings.data.hooks.darkModeChange = darkModeHookInput.text
}
onActionClicked: {
if (darkModeHookInput.text) {
HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode)
}
}
Layout.fillWidth: true
}
NDivider {
Layout.fillWidth: true
}
// Info section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NLabel {
label: "Hook Command Information"
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
}
NLabel {
label: "Available Parameters"
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
}
NLabel {
label: "Available Parameters"
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
}
}
}

View file

@ -59,6 +59,13 @@ ColumnLayout {
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
}
NToggle {
label: "Use App2Unit for Launching"
description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration."
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true

View file

@ -12,22 +12,14 @@ ColumnLayout {
spacing: Style.marginL * scaling
NToggle {
label: "WiFi Enabled"
description: "Enable WiFi connectivity."
label: "Enable Wi-Fi"
description: "Enable Wi-Fi connectivity."
checked: Settings.data.network.wifiEnabled
onToggled: checked => {
Settings.data.network.wifiEnabled = checked
NetworkService.setWifiEnabled(checked)
if (checked) {
ToastService.showNotice("WiFi", "Enabled")
} else {
ToastService.showNotice("WiFi", "Disabled")
}
}
onToggled: checked => NetworkService.setWifiEnabled(checked)
}
NToggle {
label: "Bluetooth Enabled"
label: "Enable Bluetooth"
description: "Enable Bluetooth connectivity."
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => {

View file

@ -115,7 +115,6 @@ ColumnLayout {
NColorPicker {
selectedColor: Settings.data.wallpaper.fillColor
onColorSelected: color => Settings.data.wallpaper.fillColor = color
onColorCancelled: selectedColor = Settings.data.wallpaper.fillColor
}
}
@ -278,7 +277,6 @@ ColumnLayout {
NTextInput {
label: "Custom Interval"
description: "Enter time as HH:MM (e.g., 01:30)."
inputMaxWidth: 100 * scaling
text: {
const s = Settings.data.wallpaper.randomIntervalSec
const h = Math.floor(s / 3600)

View file

@ -29,7 +29,7 @@ NBox {
// Performance
NIconButton {
icon: "speed"
tooltipText: "Set performance power profile"
tooltipText: "Set performance power profile."
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
@ -43,7 +43,7 @@ NBox {
// Balanced
NIconButton {
icon: "balance"
tooltipText: "Set balanced power profile"
tooltipText: "Set balanced power profile."
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
@ -57,7 +57,7 @@ NBox {
// Eco
NIconButton {
icon: "eco"
tooltipText: "Set eco power profile"
tooltipText: "Set eco power profile."
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant

View file

@ -59,7 +59,7 @@ NBox {
}
NIconButton {
icon: "settings"
tooltipText: "Open settings"
tooltipText: "Open settings."
onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.open(screen)
@ -69,7 +69,7 @@ NBox {
NIconButton {
id: powerButton
icon: "power_settings_new"
tooltipText: "Power menu"
tooltipText: "Power menu."
onClicked: {
powerPanel.open(screen)
sidePanel.close()
@ -79,7 +79,7 @@ NBox {
NIconButton {
id: closeButton
icon: "close"
tooltipText: "Close side panel"
tooltipText: "Close side panel."
onClicked: {
sidePanel.close()
}
@ -104,19 +104,7 @@ NBox {
stdout: StdioCollector {
onStreamFinished: {
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
var minutes = Math.floor(uptimeSeconds / 60) % 60
var hours = Math.floor(uptimeSeconds / 3600) % 24
var days = Math.floor(uptimeSeconds / 86400)
// Format the output
if (days > 0) {
uptimeText = days + "d " + hours + "h"
} else if (hours > 0) {
uptimeText = hours + "h" + minutes + "m"
} else {
uptimeText = minutes + "m"
}
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds)
uptimeProcess.running = false
}
}

View file

@ -11,7 +11,7 @@ NBox {
Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling
implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling
Column {
ColumnLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
@ -22,11 +22,6 @@ NBox {
anchors.bottomMargin: Style.marginM * scaling
spacing: Style.marginS * scaling
// Slight top padding
Item {
height: Style.marginXS * scaling
}
NCircleStat {
value: SystemStatService.cpuUsage
icon: "speed"
@ -60,10 +55,5 @@ NBox {
width: 72 * scaling
height: 68 * scaling
}
// Extra bottom padding to shift the perceived stack slightly upward
Item {
height: Style.marginM * scaling
}
}
}

View file

@ -26,7 +26,7 @@ NBox {
// Screen Recorder
NIconButton {
icon: "videocam"
tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording" : "Start screen recording"
tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording."
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: {
@ -42,7 +42,7 @@ NBox {
// Idle Inhibitor
NIconButton {
icon: "coffee"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake."
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: {
@ -54,7 +54,7 @@ NBox {
NIconButton {
visible: Settings.data.wallpaper.enabled
icon: "image"
tooltipText: "Left click: Open wallpaper selector\nRight click: Set random wallpaper"
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector

View file

@ -14,16 +14,11 @@ NPanel {
panelHeight: 500 * scaling
panelKeyboardFocus: true
property string passwordPromptSsid: ""
property string passwordSsid: ""
property string passwordInput: ""
property bool showPasswordPrompt: false
property string expandedNetwork: "" // Track which network shows options
property string expandedSsid: ""
onOpened: {
if (Settings.data.network.wifiEnabled) {
NetworkService.refreshNetworks()
}
}
onOpened: NetworkService.scan()
panelContent: Rectangle {
color: Color.transparent
@ -39,35 +34,32 @@ NPanel {
spacing: Style.marginM * scaling
NIcon {
text: "wifi"
text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: "WiFi"
text: "Wi-Fi"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling
}
// Connection status indicator
Rectangle {
visible: NetworkService.hasActiveConnection
width: 8 * scaling
height: 8 * scaling
radius: 4 * scaling
color: Color.mPrimary
NToggle {
id: wifiSwitch
checked: Settings.data.network.wifiEnabled
onToggled: checked => NetworkService.setWifiEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65 * scaling
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh networks"
tooltipText: "Refresh"
sizeRatio: 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading
onClicked: NetworkService.refreshNetworks()
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
onClicked: NetworkService.scan()
}
NIconButton {
@ -82,17 +74,18 @@ NPanel {
Layout.fillWidth: true
}
// Error banner
// Error message
Rectangle {
visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
visible: NetworkService.lastError.length > 0
Layout.fillWidth: true
Layout.preferredHeight: errorText.implicitHeight + (Style.marginM * scaling * 2)
Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * scaling * 2)
color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1)
radius: Style.radiusS * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mError
RowLayout {
id: errorRow
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
@ -104,8 +97,7 @@ NPanel {
}
NText {
id: errorText
text: NetworkService.connectError
text: NetworkService.lastError
color: Color.mError
font.pointSize: Style.fontSizeS * scaling
wrapMode: Text.Wrap
@ -115,301 +107,364 @@ NPanel {
NIconButton {
icon: "close"
sizeRatio: 0.6
onClicked: {
NetworkService.connectStatus = ""
NetworkService.connectError = ""
}
onClicked: NetworkService.lastError = ""
}
}
}
ScrollView {
// Main content area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
color: Color.transparent
// WiFi disabled state
ColumnLayout {
width: parent.width
visible: !Settings.data.network.wifiEnabled
anchors.fill: parent
spacing: Style.marginM * scaling
// Loading state
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys(
NetworkService.networks).length === 0
spacing: Style.marginM * scaling
NBusyIndicator {
running: true
color: Color.mPrimary
size: Style.baseWidgetSize * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning for networks..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
// WiFi disabled state
NIcon {
text: "wifi_off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Wi-Fi is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Wi-Fi to see available networks."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Scanning state
ColumnLayout {
visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(
NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL * scaling
Item {
Layout.fillHeight: true
}
NBusyIndicator {
running: true
color: Color.mPrimary
size: Style.baseWidgetSize * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Searching for nearby networks..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Networks list container
ScrollView {
visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(
NetworkService.networks).length > 0)
anchors.fill: parent
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
clip: true
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: !Settings.data.network.wifiEnabled
width: parent.width
spacing: Style.marginM * scaling
NIcon {
text: "wifi_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
// Network list
Repeater {
model: {
if (!Settings.data.network.wifiEnabled)
return []
NText {
text: "WiFi is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: "Enable WiFi"
icon: "wifi"
Layout.alignment: Qt.AlignHCenter
onClicked: {
Settings.data.network.wifiEnabled = true
Settings.save()
NetworkService.setWifiEnabled(true)
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
})
}
}
}
// Network list
Repeater {
model: {
if (!Settings.data.network.wifiEnabled || NetworkService.isLoading)
return []
// Sort networks: connected first, then by signal strength
const nets = Object.values(NetworkService.networks)
return nets.sort((a, b) => {
if (a.connected && !b.connected)
return -1
if (!a.connected && b.connected)
return 1
return b.signal - a.signal
})
}
Item {
Layout.fillWidth: true
implicitHeight: networkRect.implicitHeight
Rectangle {
id: networkRect
width: parent.width
implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2)
Layout.fillWidth: true
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling
// Add opacity for operations in progress
opacity: (NetworkService.disconnectingFrom === modelData.ssid
|| NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b,
0.05) : Color.mSurface
border.width: Math.max(1, Style.borderS * scaling)
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
clip: true
// Smooth opacity animation
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
ColumnLayout {
id: networkContent
id: netColumn
width: parent.width - (Style.marginM * scaling * 2)
x: Style.marginM * scaling
y: Style.marginM * scaling
spacing: Style.marginM * scaling
spacing: Style.marginS * scaling
// Main network row
// Main row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
// Signal icon
NIcon {
text: NetworkService.signalIcon(modelData.signal)
font.pointSize: Style.fontSizeXXL * scaling
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
}
// Network info
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
spacing: 2 * scaling
NText {
text: modelData.ssid || "Unknown Network"
text: modelData.ssid
font.pointSize: Style.fontSizeNormal * scaling
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
elide: Text.ElideRight
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: {
const security = modelData.security
&& modelData.security !== "--" ? modelData.security : "Open"
const signal = `${modelData.signal}%`
return `${signal} ${security}`
}
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
}
// Right-aligned items container
RowLayout {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
spacing: Style.marginS * scaling
// Connected badge
Rectangle {
visible: modelData.connected
color: Color.mPrimary
radius: width * 0.5
width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2)
height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2)
RowLayout {
spacing: Style.marginXS * scaling
NText {
id: connectedLabel
anchors.centerIn: parent
text: "Connected"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
// Saved badge - clickable
Rectangle {
visible: modelData.cached && !modelData.connected
color: Color.mSurfaceVariant
radius: width * 0.5
width: savedLabel.implicitWidth + (Style.marginS * scaling * 2)
height: savedLabel.implicitHeight + (Style.marginXS * scaling * 2)
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.color = Qt.darker(Color.mSurfaceVariant, 1.1)
onExited: parent.color = Color.mSurfaceVariant
onClicked: {
expandedNetwork = expandedNetwork === modelData.ssid ? "" : modelData.ssid
showPasswordPrompt = false
}
}
NText {
id: savedLabel
anchors.centerIn: parent
text: "Saved"
text: `${modelData.signal}%`
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
}
// Loading indicator
NBusyIndicator {
visible: NetworkService.connectingSsid === modelData.ssid
running: NetworkService.connectingSsid === modelData.ssid
color: Color.mPrimary
size: Style.baseWidgetSize * 0.6 * scaling
}
NText {
text: "•"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
// Action buttons
RowLayout {
spacing: Style.marginXS * scaling
visible: NetworkService.connectingSsid !== modelData.ssid
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
NButton {
visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt)
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
text: modelData.existing ? "Connect" : (NetworkService.isSecured(
modelData.security) ? "Password" : "Connect")
onClicked: {
if (modelData.existing || !NetworkService.isSecured(modelData.security)) {
NetworkService.connectNetwork(modelData.ssid, modelData.security)
} else {
expandedNetwork = modelData.ssid
passwordPromptSsid = modelData.ssid
showPasswordPrompt = true
passwordInput = ""
Qt.callLater(() => passwordInputField.forceActiveFocus())
}
Item {
Layout.preferredWidth: Style.marginXXS * scaling
}
// 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 * scaling * 2)
height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: connectedText
anchors.centerIn: parent
text: "Connected"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
NButton {
visible: modelData.connected
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
backgroundColor: Color.mError
text: "Disconnect"
onClicked: NetworkService.disconnectNetwork(modelData.ssid)
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Color.mError
radius: height * 0.5
width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2)
height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: disconnectingText
anchors.centerIn: parent
text: "Disconnecting..."
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Color.mError
radius: height * 0.5
width: forgettingText.implicitWidth + (Style.marginS * scaling * 2)
height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: forgettingText
anchors.centerIn: parent
text: "Forgetting..."
font.pointSize: Style.fontSizeXXS * scaling
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 * scaling)
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * scaling * 2)
height: savedText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: savedText
anchors.centerIn: parent
text: "Saved"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
}
}
}
// Action area
RowLayout {
spacing: Style.marginS * scaling
NBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid
|| NetworkService.disconnectingFrom === modelData.ssid
|| NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary
size: Style.baseWidgetSize * 0.5 * scaling
}
NIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected
&& NetworkService.connectingTo !== modelData.ssid
&& NetworkService.forgettingNetwork !== modelData.ssid
&& NetworkService.disconnectingFrom !== modelData.ssid
icon: "delete"
tooltipText: "Forget network"
sizeRatio: 0.7
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 "Password"
}
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
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 * scaling
backgroundColor: Color.mError
onClicked: NetworkService.disconnect(modelData.ssid)
}
}
}
// Password input section
// Password input
Rectangle {
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
&& NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
implicitHeight: visible ? 50 * scaling : 0
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusS * scaling
RowLayout {
id: passwordRow
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
spacing: Style.marginM * scaling
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusS * scaling
radius: Style.radiusXS * scaling
color: Color.mSurface
border.color: passwordInputField.activeFocus ? Color.mSecondary : Color.mOutline
border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
TextInput {
id: passwordInputField
id: pwdInput
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
height: parent.height
anchors.margins: Style.marginS * scaling
text: passwordInput
font.pointSize: Style.fontSizeM * scaling
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurface
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt
selectByMouse: true
echoMode: TextInput.Password
selectByMouse: true
focus: visible
passwordCharacter: "●"
onTextChanged: passwordInput = text
onVisibleChanged: if (visible)
forceActiveFocus()
onAccepted: {
if (passwordInput) {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
expandedNetwork = ""
if (text) {
NetworkService.connect(passwordSsid, text)
passwordSsid = ""
passwordInput = ""
}
}
@ -425,56 +480,75 @@ NPanel {
NButton {
text: "Connect"
icon: "check"
fontSize: Style.fontSizeXS * scaling
fontSize: Style.fontSizeXXS * scaling
enabled: passwordInput.length > 0
outlined: !enabled
outlined: true
onClicked: {
if (passwordInput) {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
expandedNetwork = ""
}
NetworkService.connect(passwordSsid, passwordInput)
passwordSsid = ""
passwordInput = ""
}
}
NIconButton {
icon: "close"
tooltipText: "Cancel"
sizeRatio: 0.9
sizeRatio: 0.8
onClicked: {
showPasswordPrompt = false
expandedNetwork = ""
passwordSsid = ""
passwordInput = ""
}
}
}
}
// Forget network option - appears when saved badge is clicked
RowLayout {
visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid
&& !showPasswordPrompt
// Forget network
Rectangle {
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
&& NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
Layout.topMargin: Style.marginXS * scaling
spacing: Style.marginS * scaling
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mOutline
Item {
Layout.fillWidth: true
}
RowLayout {
id: forgetRow
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginM * scaling
NButton {
id: forgetButton
text: "Forget Network"
icon: "delete_outline"
fontSize: Style.fontSizeXXS * scaling
backgroundColor: Color.mError
textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary
outlined: !forgetButton.hovered
Layout.preferredHeight: 28 * scaling
onClicked: {
NetworkService.forgetNetwork(modelData.ssid)
expandedNetwork = ""
RowLayout {
NIcon {
text: "delete_outline"
font.pointSize: Style.fontSizeL * scaling
color: Color.mError
}
NText {
text: "Forget this network?"
font.pointSize: Style.fontSizeS * scaling
color: Color.mError
Layout.fillWidth: true
}
}
NButton {
id: forgetButton
text: "Forget"
fontSize: Style.fontSizeXXS * scaling
backgroundColor: Color.mError
outlined: forgetButton.hovered ? false : true
onClicked: {
NetworkService.forget(modelData.ssid)
expandedSsid = ""
}
}
NIconButton {
icon: "close"
sizeRatio: 0.8
onClicked: expandedSsid = ""
}
}
}
@ -482,35 +556,42 @@ NPanel {
}
}
}
}
// No networks found
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys(
NetworkService.networks).length === 0
spacing: Style.marginM * scaling
// Empty state when no networks
ColumnLayout {
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(
NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL * scaling
NIcon {
text: "wifi_find"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
NText {
text: "No networks found"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NIcon {
text: "wifi_find"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: "Refresh"
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.refreshNetworks()
}
NText {
text: "No networks found"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: "Scan again"
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan()
}
Item {
Layout.fillHeight: true
}
}
}

View file

@ -215,6 +215,7 @@ Alternatively, you can add it to your NixOS configuration or flake:
| Toggle Settings Window | `qs -c noctalia-shell ipc call settings toggle` |
| Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen toggle` |
| Toggle Notification History | `qs -c noctalia-shell ipc call notifications toggleHistory` |
| Toggle Notification DND | `qs -c noctalia-shell ipc call notifications toggleDND` |
| Change Wallpaper | `qs -c noctalia-shell ipc call wallpaper set $path $monitor` |
| Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` |
| Toggle Dark Mode | `qs -c noctalia-shell ipc call darkMode toggle` |
@ -265,6 +266,10 @@ The launcher supports special commands for enhanced functionality:
For Niri:
```
debug {
honor-xdg-activation-with-invalid-serial
}
window-rule {
geometry-corner-radius 20
clip-to-geometry true
@ -279,6 +284,8 @@ layer-rule {
place-within-backdrop true
}
```
`honor-xdg-activation-with-invalid-serial` allows notification actions (like view etc) to work.
---

View file

@ -28,6 +28,7 @@ Singleton {
"PowerToggle": powerToggleComponent,
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
"SidePanelToggle": sidePanelToggleComponent,
"Spacer": spacerComponent,
"SystemMonitor": systemMonitorComponent,
"Taskbar": taskbarComponent,
"Tray": trayComponent,
@ -43,6 +44,11 @@ Singleton {
"leftClickExec": "",
"rightClickExec": "",
"middleClickExec": ""
},
"Spacer": {
"allowUserSettings": true,
"icon": "space_bar",
"width": 20
}
})
@ -101,6 +107,9 @@ Singleton {
property Component sidePanelToggleComponent: Component {
SidePanelToggle {}
}
property Component spacerComponent: Component {
Spacer {}
}
property Component systemMonitorComponent: Component {
SystemMonitor {}
}

View file

@ -8,215 +8,239 @@ import qs.Commons
Singleton {
id: root
// Core properties
// Core state
property var networks: ({})
property string connectingSsid: ""
property string connectStatus: ""
property string connectStatusSsid: ""
property string connectError: ""
property bool isLoading: false
property bool ethernet: false
property int retryCount: 0
property int maxRetries: 3
property bool scanning: false
property bool connecting: false
property string connectingTo: ""
property string lastError: ""
property bool ethernetConnected: false
property string disconnectingFrom: ""
property string forgettingNetwork: ""
// File path for persistent storage
// Persistent cache
property string cacheFile: Settings.cacheDir + "network.json"
readonly property string cachedLastConnected: cacheAdapter.lastConnected
readonly property var cachedNetworks: cacheAdapter.knownNetworks
// Stable properties for UI
readonly property alias cache: adapter
readonly property string lastConnectedNetwork: adapter.lastConnected
// File-based persistent storage
// Cache file handling
FileView {
id: cacheFileView
path: root.cacheFile
onAdapterUpdated: saveTimer.start()
onLoaded: {
Logger.log("Network", "Loaded network cache from disk")
// Try to auto-connect on startup if WiFi is enabled
if (Settings.data.network.wifiEnabled && adapter.lastConnected) {
autoConnectTimer.start()
}
}
onLoadFailed: function (error) {
Logger.log("Network", "No existing cache found, creating new one")
// Initialize with empty data
adapter.knownNetworks = ({})
adapter.lastConnected = ""
}
JsonAdapter {
id: adapter
id: cacheAdapter
property var knownNetworks: ({})
property string lastConnected: ""
property int lastRefresh: 0
}
onLoadFailed: {
cacheAdapter.knownNetworks = ({})
cacheAdapter.lastConnected = ""
}
}
// Save timer to batch writes
Connections {
target: Settings.data.network
function onWifiEnabledChanged() {
if (Settings.data.network.wifiEnabled) {
ToastService.showNotice("Wi-Fi", "Enabled")
} else {
ToastService.showNotice("Wi-Fi", "Disabled")
}
}
}
Component.onCompleted: {
Logger.log("Network", "Service initialized")
syncWifiState()
refresh()
}
// Save cache with debounce
Timer {
id: saveTimer
running: false
id: saveDebounce
interval: 1000
onTriggered: cacheFileView.writeAdapter()
}
Component.onCompleted: {
Logger.log("Network", "Service started")
function saveCache() {
saveDebounce.restart()
}
// Delayed scan timer
Timer {
id: delayedScanTimer
interval: 7000
onTriggered: scan()
}
// Core functions
function syncWifiState() {
wifiStateProcess.running = true
}
function setWifiEnabled(enabled) {
Settings.data.network.wifiEnabled = enabled
wifiToggleProcess.action = enabled ? "on" : "off"
wifiToggleProcess.running = true
}
function refresh() {
ethernetStateProcess.running = true
if (Settings.data.network.wifiEnabled) {
refreshNetworks()
scan()
}
}
// Signal strength icon mapping
function signalIcon(signal) {
const levels = [{
"threshold": 80,
"icon": "network_wifi"
}, {
"threshold": 60,
"icon": "network_wifi_3_bar"
}, {
"threshold": 40,
"icon": "network_wifi_2_bar"
}, {
"threshold": 20,
"icon": "network_wifi_1_bar"
}]
for (const level of levels) {
if (signal >= level.threshold)
return level.icon
}
return "signal_wifi_0_bar"
}
function isSecured(security) {
return security && security.trim() !== "" && security.trim() !== "--"
}
// Enhanced refresh with retry logic
function refreshNetworks() {
if (isLoading)
function scan() {
if (scanning)
return
isLoading = true
retryCount = 0
adapter.lastRefresh = Date.now()
performRefresh()
scanning = true
lastError = ""
scanProcess.running = true
Logger.log("Network", "Wi-Fi scan in progress...")
}
function performRefresh() {
checkEthernet.running = true
existingNetworkProcess.running = true
}
function connect(ssid, password = "") {
if (connecting)
return
// Retry mechanism for failed operations
function retryRefresh() {
if (retryCount < maxRetries) {
retryCount++
Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`)
retryTimer.start()
connecting = true
connectingTo = ssid
lastError = ""
// Check if we have a saved connection
if (networks[ssid]?.existing || cachedNetworks[ssid]) {
connectProcess.mode = "saved"
connectProcess.ssid = ssid
connectProcess.password = ""
} else {
isLoading = false
connectError = "Failed to refresh networks after multiple attempts"
connectProcess.mode = "new"
connectProcess.ssid = ssid
connectProcess.password = password
}
connectProcess.running = true
}
Timer {
id: retryTimer
interval: 1000 * retryCount // Progressive backoff
repeat: false
onTriggered: performRefresh()
function disconnect(ssid) {
disconnectingFrom = ssid
disconnectProcess.ssid = ssid
disconnectProcess.running = true
}
Timer {
id: autoConnectTimer
interval: 3000
repeat: false
onTriggered: {
if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) {
Logger.log("Network", `Auto-connecting to ${adapter.lastConnected}`)
connectToExisting(adapter.lastConnected)
}
}
}
// Forget network function
function forgetNetwork(ssid) {
Logger.log("Network", `Forgetting network: ${ssid}`)
function forget(ssid) {
forgettingNetwork = ssid
// Remove from cache
let known = adapter.knownNetworks
let known = cacheAdapter.knownNetworks
delete known[ssid]
adapter.knownNetworks = known
cacheAdapter.knownNetworks = known
// Clear last connected if it's this network
if (adapter.lastConnected === ssid) {
adapter.lastConnected = ""
if (cacheAdapter.lastConnected === ssid) {
cacheAdapter.lastConnected = ""
}
// Save changes
saveTimer.restart()
saveCache()
// Remove NetworkManager profile
// Remove from system
forgetProcess.ssid = ssid
forgetProcess.running = true
}
// Helper function to immediately update network status
function updateNetworkStatus(ssid, connected) {
let nets = networks
// Update all networks connected status
for (let key in nets) {
if (nets[key].connected && key !== ssid) {
nets[key].connected = false
}
}
// Update the target network if it exists
if (nets[ssid]) {
nets[ssid].connected = connected
nets[ssid].existing = true
nets[ssid].cached = true
} else if (connected) {
// Create a temporary entry if network doesn't exist yet
nets[ssid] = {
"ssid": ssid,
"security": "--",
"signal": 100,
"connected"// Default to good signal until real scan
: true,
"existing": true,
"cached": true
}
}
// Trigger property change notification
networks = ({})
networks = nets
}
// Helper functions
function signalIcon(signal) {
if (signal >= 80)
return "network_wifi"
if (signal >= 60)
return "network_wifi_3_bar"
if (signal >= 40)
return "network_wifi_2_bar"
if (signal >= 20)
return "network_wifi_1_bar"
return "signal_wifi_0_bar"
}
function isSecured(security) {
return security && security !== "--" && security.trim() !== ""
}
// Processes
Process {
id: forgetProcess
property string ssid: ""
id: ethernetStateProcess
running: false
command: ["nmcli", "connection", "delete", "id", ssid]
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector {
onStreamFinished: {
Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`)
refreshNetworks()
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
if (text.includes("no such connection profile")) {
Logger.log("Network", `Network profile not found: ${forgetProcess.ssid}`)
} else {
Logger.warn("Network", `Error forgetting network: ${text}`)
}
refreshNetworks()
const connected = text.split("\n").some(line => {
const parts = line.split(":")
return parts[1] === "ethernet" && parts[2] === "connected"
})
if (root.ethernetConnected !== connected) {
root.ethernetConnected = connected
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
}
}
}
}
// WiFi enable/disable functions
function setWifiEnabled(enabled) {
if (enabled) {
isLoading = true
wifiRadioProcess.action = "on"
wifiRadioProcess.running = true
} else {
// Save current connection for later
for (const ssid in networks) {
if (networks[ssid].connected) {
adapter.lastConnected = ssid
saveTimer.restart()
disconnectNetwork(ssid)
break
}
}
wifiRadioProcess.action = "off"
wifiRadioProcess.running = true
}
}
// Unified WiFi radio control
Process {
id: wifiRadioProcess
id: wifiStateProcess
running: false
command: ["nmcli", "radio", "wifi"]
stdout: StdioCollector {
onStreamFinished: {
const enabled = text.trim() === "enabled"
Logger.log("Network", "Wi-Fi enabled:", enabled)
if (Settings.data.network.wifiEnabled !== enabled) {
Settings.data.network.wifiEnabled = enabled
}
}
}
}
Process {
id: wifiToggleProcess
property string action: "on"
running: false
command: ["nmcli", "radio", "wifi", action]
@ -224,10 +248,12 @@ Singleton {
onRunningChanged: {
if (!running) {
if (action === "on") {
wifiEnableTimer.start()
// Clear networks immediately and start delayed scan
root.networks = ({})
delayedScanTimer.interval = 8000
delayedScanTimer.restart()
} else {
root.networks = ({})
root.isLoading = false
}
}
}
@ -235,137 +261,177 @@ Singleton {
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`)
Logger.warn("Network", "WiFi toggle error: " + text)
}
}
}
}
Timer {
id: wifiEnableTimer
interval: 2000
repeat: false
onTriggered: {
refreshNetworks()
if (adapter.lastConnected) {
reconnectTimer.start()
Process {
id: scanProcess
running: false
command: ["sh", "-c", `
# Get list of saved connection profiles (just the names)
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
# Get WiFi networks
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do
ssid=$(echo "$line" | cut -d: -f1)
security=$(echo "$line" | cut -d: -f2)
signal=$(echo "$line" | cut -d: -f3)
in_use=$(echo "$line" | cut -d: -f4)
# Skip empty SSIDs
if [ -z "$ssid" ]; then
continue
fi
# Check if SSID matches any profile name (simple check)
# This covers most cases where profile name equals or contains the SSID
existing=false
if echo "$profiles" | grep -qF "$ssid|"; then
existing=true
fi
echo "$ssid|$security|$signal|$in_use|$existing"
done
`]
stdout: StdioCollector {
onStreamFinished: {
const nets = {}
const lines = text.split("\n").filter(l => l.trim())
for (const line of lines) {
const parts = line.split("|")
if (parts.length < 5)
continue
const ssid = parts[0]
if (!ssid || ssid.trim() === "")
continue
const network = {
"ssid": ssid,
"security": parts[1] || "--",
"signal": parseInt(parts[2]) || 0,
"connected": parts[3] === "*",
"existing": parts[4] === "true",
"cached": ssid in cacheAdapter.knownNetworks
}
// Track connected network
if (network.connected && cacheAdapter.lastConnected !== ssid) {
cacheAdapter.lastConnected = ssid
saveCache()
}
// Keep best signal for duplicate SSIDs
if (!nets[ssid] || network.signal > nets[ssid].signal) {
nets[ssid] = network
}
}
// For logging purpose only
Logger.log("Network", "Wi-Fi scan completed")
const oldSSIDs = Object.keys(root.networks)
const newSSIDs = Object.keys(nets)
const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid))
const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid))
if (newNetworks.length > 0 || lostNetworks.length > 0) {
if (newNetworks.length > 0) {
Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", "))
}
if (lostNetworks.length > 0) {
Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", "))
}
Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length)
}
// Assign the results
root.networks = nets
root.scanning = false
}
}
stderr: StdioCollector {
onStreamFinished: {
root.scanning = false
if (text.trim()) {
Logger.warn("Network", "Scan error: " + text)
// If scan fails, set a short retry
if (Settings.data.network.wifiEnabled) {
delayedScanTimer.interval = 5000
delayedScanTimer.restart()
}
}
}
}
}
Timer {
id: reconnectTimer
interval: 3000
repeat: false
onTriggered: {
if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) {
connectToExisting(adapter.lastConnected)
}
}
}
// Connection management
function connectNetwork(ssid, security) {
connectingSsid = ssid
connectStatus = ""
connectStatusSsid = ssid
connectError = ""
// Check if profile exists
if (networks[ssid]?.existing) {
connectToExisting(ssid)
return
}
// Check cache for known network
const known = adapter.knownNetworks[ssid]
if (known?.profileName) {
connectToExisting(known.profileName)
return
}
// New connection - need password for secured networks
if (isSecured(security)) {
// Password will be provided through submitPassword
return
}
// Open network - connect directly
createAndConnect(ssid, "", security)
}
function submitPassword(ssid, password) {
const security = networks[ssid]?.security || ""
createAndConnect(ssid, password, security)
}
function connectToExisting(ssid) {
connectingSsid = ssid
upConnectionProcess.profileName = ssid
upConnectionProcess.running = true
}
function createAndConnect(ssid, password, security) {
connectingSsid = ssid
connectProcess.ssid = ssid
connectProcess.password = password
connectProcess.isSecured = isSecured(security)
connectProcess.running = true
}
function disconnectNetwork(ssid) {
disconnectProcess.ssid = ssid
disconnectProcess.running = true
}
// Connection process
Process {
id: connectProcess
property string mode: "new"
property string ssid: ""
property string password: ""
property bool isSecured: false
running: false
command: {
const cmd = ["nmcli", "device", "wifi", "connect", ssid]
if (isSecured && password) {
cmd.push("password", password)
}
return cmd
}
stdout: StdioCollector {
onStreamFinished: {
handleConnectionSuccess(connectProcess.ssid)
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
handleConnectionError(connectProcess.ssid, text)
if (mode === "saved") {
return ["nmcli", "connection", "up", "id", ssid]
} else {
const cmd = ["nmcli", "device", "wifi", "connect", ssid]
if (password) {
cmd.push("password", password)
}
return cmd
}
}
}
Process {
id: upConnectionProcess
property string profileName: ""
running: false
command: ["nmcli", "connection", "up", "id", profileName]
stdout: StdioCollector {
onStreamFinished: {
handleConnectionSuccess(upConnectionProcess.profileName)
// Success - update cache
let known = cacheAdapter.knownNetworks
known[connectProcess.ssid] = {
"profileName": connectProcess.ssid,
"lastConnected": Date.now()
}
cacheAdapter.knownNetworks = known
cacheAdapter.lastConnected = connectProcess.ssid
saveCache()
// Immediately update the UI before scanning
root.updateNetworkStatus(connectProcess.ssid, true)
root.connecting = false
root.connectingTo = ""
Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`)
// Still do a scan to get accurate signal and security info
delayedScanTimer.interval = 1000
delayedScanTimer.restart()
}
}
stderr: StdioCollector {
onStreamFinished: {
root.connecting = false
root.connectingTo = ""
if (text.trim()) {
handleConnectionError(upConnectionProcess.profileName, text)
// Parse common errors
if (text.includes("Secrets were required") || text.includes("no secrets provided")) {
root.lastError = "Incorrect password"
forget(connectProcess.ssid)
} else if (text.includes("No network with SSID")) {
root.lastError = "Network not found"
} else if (text.includes("Timeout")) {
root.lastError = "Connection timeout"
} else {
root.lastError = text.split("\n")[0].trim()
}
Logger.warn("Network", "Connect error: " + text)
}
}
}
@ -377,221 +443,101 @@ Singleton {
running: false
command: ["nmcli", "connection", "down", "id", ssid]
onRunningChanged: {
if (!running) {
connectingSsid = ""
connectStatus = ""
connectStatusSsid = ""
connectError = ""
refreshNetworks()
stdout: StdioCollector {
onStreamFinished: {
Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`)
// Immediately update UI on successful disconnect
root.updateNetworkStatus(disconnectProcess.ssid, false)
root.disconnectingFrom = ""
// Do a scan to refresh the list
delayedScanTimer.interval = 1000
delayedScanTimer.restart()
}
}
stderr: StdioCollector {
onStreamFinished: {
root.disconnectingFrom = ""
if (text.trim()) {
Logger.warn("Network", `Disconnect warning: ${text}`)
Logger.warn("Network", "Disconnect error: " + text)
}
// Still trigger a scan even on error
delayedScanTimer.interval = 1000
delayedScanTimer.restart()
}
}
}
// Connection result handlers
function handleConnectionSuccess(ssid) {
connectingSsid = ""
connectStatus = "success"
connectStatusSsid = ssid
connectError = ""
// Update cache
let known = adapter.knownNetworks
known[ssid] = {
"profileName": ssid,
"lastConnected": Date.now(),
"autoConnect": true
}
adapter.knownNetworks = known
adapter.lastConnected = ssid
saveTimer.restart()
Logger.log("Network", `Successfully connected to ${ssid}`)
refreshNetworks()
}
function handleConnectionError(ssid, error) {
connectingSsid = ""
connectStatus = "error"
connectStatusSsid = ssid
connectError = parseError(error)
Logger.warn("Network", `Failed to connect to ${ssid}: ${error}`)
}
function parseError(error) {
// Simplify common error messages
if (error.includes("Secrets were required") || error.includes("no secrets provided")) {
return "Incorrect password"
}
if (error.includes("No network with SSID")) {
return "Network not found"
}
if (error.includes("Connection activation failed")) {
return "Connection failed. Please try again."
}
if (error.includes("Timeout")) {
return "Connection timeout. Network may be out of range."
}
// Return first line only
return error.split("\n")[0].trim()
}
// Network scanning processes
Process {
id: existingNetworkProcess
id: forgetProcess
property string ssid: ""
running: false
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
// Try multiple common profile name patterns
command: ["sh", "-c", `
ssid="$1"
deleted=false
# Try exact SSID match first
if nmcli connection delete id "$ssid" 2>/dev/null; then
echo "Deleted profile: $ssid"
deleted=true
fi
# Try "Auto <SSID>" pattern
if nmcli connection delete id "Auto $ssid" 2>/dev/null; then
echo "Deleted profile: Auto $ssid"
deleted=true
fi
# Try "<SSID> 1", "<SSID> 2", etc. patterns
for i in 1 2 3; do
if nmcli connection delete id "$ssid $i" 2>/dev/null; then
echo "Deleted profile: $ssid $i"
deleted=true
fi
done
if [ "$deleted" = "false" ]; then
echo "No profiles found for SSID: $ssid"
fi
`, "--", ssid]
stdout: StdioCollector {
onStreamFinished: {
const profiles = {}
const lines = text.split("\n").filter(l => l.trim())
Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`)
Logger.log("Network", text.trim().replace(/[\r\n]/g, " "))
for (const line of lines) {
const parts = line.split(":")
const name = parts[0]
const type = parts[1]
if (name && type === "802-11-wireless") {
profiles[name] = {
"ssid": name,
"type": type
}
}
// Update both cached and existing status immediately
let nets = root.networks
if (nets[forgetProcess.ssid]) {
nets[forgetProcess.ssid].cached = false
nets[forgetProcess.ssid].existing = false
// Trigger property change
root.networks = ({})
root.networks = nets
}
scanProcess.existingProfiles = profiles
scanProcess.running = true
root.forgettingNetwork = ""
// Quick scan to verify the profile is gone
delayedScanTimer.interval = 500
delayedScanTimer.restart()
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
Logger.warn("Network", "Error listing connections:", text)
retryRefresh()
root.forgettingNetwork = ""
if (text.trim() && !text.includes("No profiles found")) {
Logger.warn("Network", "Forget error: " + text)
}
// Still Trigger a scan even on error
delayedScanTimer.interval = 500
delayedScanTimer.restart()
}
}
}
Process {
id: scanProcess
property var existingProfiles: ({})
running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
stdout: StdioCollector {
onStreamFinished: {
const networksMap = {}
const lines = text.split("\n").filter(l => l.trim())
for (const line of lines) {
const parts = line.split(":")
if (parts.length < 4)
continue
const ssid = parts[0]
const security = parts[1]
const signalStr = parts[2]
const inUse = parts[3]
if (!ssid)
continue
const signal = parseInt(signalStr) || 0
const connected = inUse === "*"
// Update last connected if we find the connected network
if (connected && adapter.lastConnected !== ssid) {
adapter.lastConnected = ssid
saveTimer.restart()
}
// Merge with existing or create new
if (!networksMap[ssid] || signal > networksMap[ssid].signal) {
networksMap[ssid] = {
"ssid": ssid,
"security": security || "--",
"signal": signal,
"connected": connected,
"existing": ssid in scanProcess.existingProfiles,
"cached": ssid in adapter.knownNetworks
}
}
}
root.networks = networksMap
root.isLoading = false
scanProcess.existingProfiles = {}
//Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`)
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
Logger.warn("Network", "Error scanning networks:", text)
retryRefresh()
}
}
}
}
Process {
id: checkEthernet
running: false
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector {
onStreamFinished: {
root.ethernet = text.split("\n").some(line => {
const parts = line.split(":")
return parts[1] === "ethernet" && parts[2] === "connected"
})
}
}
}
// Auto-refresh timer
Timer {
interval: 30000 // 30 seconds
running: Settings.data.network.wifiEnabled && !isLoading
repeat: true
onTriggered: {
// Only refresh if we should
const now = Date.now()
const timeSinceLastRefresh = now - adapter.lastRefresh
// Refresh if: connected, or it's been more than 30 seconds
if (hasActiveConnection || timeSinceLastRefresh > 30000) {
refreshNetworks()
}
}
}
property bool hasActiveConnection: {
return Object.values(networks).some(net => net.connected)
}
// Menu state management
function onMenuOpened() {
if (Settings.data.network.wifiEnabled) {
refreshNetworks()
}
}
function onMenuClosed() {
// Clean up temporary states
connectStatus = ""
connectError = ""
}
}

View file

@ -28,11 +28,11 @@ Singleton {
// Signal when notification is received
onNotification: function (notification) {
// Always add notification to history
root.addToHistory(notification)
// Check if notifications are suppressed
if (Settings.data.notifications && Settings.data.notifications.suppressed) {
// Still add to history but don't show notification
root.addToHistory(notification)
// Check if do-not-disturb is enabled
if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) {
return
}
@ -46,8 +46,6 @@ Singleton {
// Add to our model
root.addNotification(notification)
// Also add to history
root.addToHistory(notification)
}
}
@ -109,6 +107,15 @@ Singleton {
}
}
Connections {
target: Settings.data.notifications
function onDoNotDisturbChanged() {
const label = Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' enabled" : "'Do Not Disturb' disabled"
const description = Settings.data.notifications.doNotDisturb ? "You'll find these notifications in your history." : "Showing all notifications."
ToastService.showNotice(label, description)
}
}
// Function to add notification to model
function addNotification(notification) {
notificationModel.insert(0, {

View file

@ -22,7 +22,7 @@ Singleton {
if (bytesPerSecond < 1024) {
return bytesPerSecond.toFixed(0) + "B/s"
} else if (bytesPerSecond < 1024 * 1024) {
return (bytesPerSecond / 1024).toFixed(1) + "KB/s"
return (bytesPerSecond / 1024).toFixed(0) + "KB/s"
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s"
} else {

View file

@ -165,13 +165,21 @@ Singleton {
"timestamp": Date.now()
}
// If there's already a toast showing, instantly start hide animation and show new one
if (isShowingToast) {
// Instantly start hide animation of current toast
for (var i = 0; i < allToasts.length; i++) {
allToasts[i].hide()
}
// Clear the queue since we're showing the new toast immediately
messageQueue = []
}
// Add to queue
messageQueue.push(toastData)
// Process queue if not currently showing a toast
if (!isShowingToast) {
processQueue()
}
// Always process immediately for instant display
processQueue()
}
// Process the message queue
@ -181,11 +189,6 @@ Singleton {
return
}
if (isShowingToast) {
// Wait for current toast to finish
return
}
var toastData = messageQueue.shift()
isShowingToast = true

View file

@ -8,7 +8,7 @@ Singleton {
id: root
// Public properties
property string baseVersion: "2.5.0"
property string baseVersion: "2.6.0"
property bool isDevelopment: true
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`

View file

@ -1,9 +1,10 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
// Compact circular statistic display used in the SidePanel
// Compact circular statistic display using Layout management
Rectangle {
id: root
@ -28,20 +29,20 @@ Rectangle {
// Repaint gauge when the bound value changes
onValueChanged: gauge.requestPaint()
Row {
id: innerRow
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Style.marginS * scaling * contentScale
spacing: Style.marginS * scaling * contentScale
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 0
// Gauge with percentage label placed inside the open gap (right side)
// Main gauge container
Item {
id: gaugeWrap
anchors.verticalCenter: innerRow.verticalCenter
width: 68 * scaling * contentScale
height: 68 * scaling * contentScale
id: gaugeContainer
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: 68 * scaling * contentScale
Layout.preferredHeight: 68 * scaling * contentScale
Canvas {
id: gauge
@ -84,15 +85,13 @@ Rectangle {
horizontalAlignment: Text.AlignHCenter
}
// Tiny circular badge for the icon, inside the right-side gap
// Tiny circular badge for the icon, positioned using anchors within the gauge
Rectangle {
id: iconBadge
width: 28 * scaling * contentScale
height: width
radius: width / 2
color: Color.mSurface
// border.color: Color.mPrimary
// border.width: Math.max(1, Style.borderS * scaling)
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -6 * scaling * contentScale

View file

@ -18,6 +18,7 @@ Rectangle {
id: textItem
text: Time.time
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
}

View file

@ -8,39 +8,34 @@ Rectangle {
id: root
property color selectedColor: "#000000"
property bool expanded: false
signal colorSelected(color color)
signal colorCancelled
implicitWidth: expanded ? 320 * scaling : 150 * scaling
implicitHeight: expanded ? 300 * scaling : 40 * scaling
implicitWidth: 150 * scaling
implicitHeight: 40 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
property var presetColors: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000", "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E"]
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Style.animationFast
}
}
// Collapsed view - just show current color
// Minimized Look
MouseArea {
visible: !root.expanded
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = true
onClicked: {
var dialog = Qt.createComponent("NColorPickerDialog.qml").createObject(root, {
"selectedColor": selectedColor,
"parent": Overlay.overlay
})
// Connect the dialog's signal to the picker's signal
dialog.colorSelected.connect(function (color) {
root.selectedColor = color
root.colorSelected(color)
})
dialog.open()
}
RowLayout {
anchors.fill: parent
@ -68,119 +63,4 @@ Rectangle {
}
}
}
// Expanded view - color selection
ColumnLayout {
visible: root.expanded
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
// Header
RowLayout {
Layout.fillWidth: true
NText {
text: "Select Color"
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: root.expanded = false
}
}
// Preset colors grid
Grid {
columns: 9
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Repeater {
model: root.presetColors
Rectangle {
width: Math.round(29 * scaling)
height: width
radius: Style.radiusXS * scaling
color: modelData
border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline
border.width: root.selectedColor === modelData ? 2 : 1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedColor = modelData
// root.colorSelected(modelData)
}
}
}
}
}
// Custom color input
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NTextInput {
id: hexInput
label: "Hex Color"
text: root.selectedColor.toString().toUpperCase()
fontFamily: Settings.data.ui.fontFixed
Layout.minimumWidth: 100 * scaling
onEditingFinished: {
if (/^#[0-9A-F]{6}$/i.test(text)) {
root.selectedColor = text
root.colorSelected(text)
}
}
}
Rectangle {
Layout.preferredWidth: 32 * scaling
Layout.preferredHeight: 32 * scaling
radius: Layout.preferredWidth * 0.5
color: root.selectedColor
border.color: Color.mOutline
border.width: 1
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 5 * scaling
}
}
// Action buttons row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
Item {
Layout.fillWidth: true
} // Spacer
NButton {
text: "Cancel"
outlined: true
customHeight: Style.baseWidgetSize * scaling
fontSize: Style.fontSizeS * scaling
onClicked: {
root.colorCancelled()
root.expanded = false
}
}
NButton {
text: "Apply"
customHeight: Style.baseWidgetSize * scaling
fontSize: Style.fontSizeS * scaling
onClicked: {
root.colorSelected(root.selectedColor)
root.expanded = false
}
}
}
}
}

View file

@ -0,0 +1,516 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
Popup {
id: root
property color selectedColor: "#000000"
property real currentHue: 0
property real currentSaturation: 0
signal colorSelected(color color)
width: 580 * scaling
height: {
const h = scrollView.implicitHeight + padding * 2
Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2)
}
padding: Style.marginXL * scaling
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
modal: true
clip: true
function rgbToHsv(r, g, b) {
r /= 255
g /= 255
b /= 255
var max = Math.max(r, g, b), min = Math.min(r, g, b)
var h, s, v = max
var d = max - min
s = max === 0 ? 0 : d / max
if (max === min) {
h = 0
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0)
break
case g:
h = (b - r) / d + 2
break
case b:
h = (r - g) / d + 4
break
}
h /= 6
}
return [h * 360, s * 100, v * 100]
}
function hsvToRgb(h, s, v) {
h /= 360
s /= 100
v /= 100
var r, g, b
var i = Math.floor(h * 6)
var f = h * 6 - i
var p = v * (1 - s)
var q = v * (1 - f * s)
var t = v * (1 - (1 - f) * s)
switch (i % 6) {
case 0:
r = v
g = t
b = p
break
case 1:
r = q
g = v
b = p
break
case 2:
r = p
g = v
b = t
break
case 3:
r = p
g = q
b = v
break
case 4:
r = t
g = p
b = v
break
case 5:
r = v
g = p
b = q
break
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
}
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS * scaling
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
}
ScrollView {
id: scrollView
anchors.fill: parent
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
clip: true
ColumnLayout {
width: scrollView.availableWidth
spacing: Style.marginL * scaling
// Header
RowLayout {
Layout.fillWidth: true
RowLayout {
spacing: Style.marginS * scaling
NIcon {
text: "palette"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "Color Picker"
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
}
}
Item {
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: root.close()
}
}
// Color preview section
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80 * scaling
radius: Style.radiusS * scaling
color: root.selectedColor
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
ColumnLayout {
spacing: 0
anchors.fill: parent
Item {
Layout.fillHeight: true
}
NText {
text: root.selectedColor.toString().toUpperCase()
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Font.Bold
color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF"
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round(
root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")"
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF"
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
// Hex input
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NLabel {
label: "Hex Color"
description: "Enter a hexadecimal color code"
Layout.fillWidth: true
}
NTextInput {
text: root.selectedColor.toString().toUpperCase()
fontFamily: Settings.data.ui.fontFixed
Layout.fillWidth: true
onEditingFinished: {
if (/^#[0-9A-F]{6}$/i.test(text)) {
root.selectedColor = text
}
}
}
}
// RGB sliders section
NBox {
Layout.fillWidth: true
Layout.preferredHeight: slidersSection.implicitHeight + Style.marginL * scaling * 2
ColumnLayout {
id: slidersSection
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
NLabel {
label: "RGB Values"
description: "Adjust red, green, blue, and brightness values"
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "R"
font.weight: Font.Bold
Layout.preferredWidth: 20 * scaling
}
NSlider {
id: redSlider
Layout.fillWidth: true
from: 0
to: 255
value: Math.round(root.selectedColor.r * 255)
onMoved: {
root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1)
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
root.selectedColor.b * 255)
root.currentHue = hsv[0]
root.currentSaturation = hsv[1]
}
}
NText {
text: Math.round(redSlider.value)
font.family: Settings.data.ui.fontFixed
Layout.preferredWidth: 30 * scaling
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "G"
font.weight: Font.Bold
Layout.preferredWidth: 20 * scaling
}
NSlider {
id: greenSlider
Layout.fillWidth: true
from: 0
to: 255
value: Math.round(root.selectedColor.g * 255)
onMoved: {
root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1)
// Update stored hue and saturation when RGB changes
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
root.selectedColor.b * 255)
root.currentHue = hsv[0]
root.currentSaturation = hsv[1]
}
}
NText {
text: Math.round(greenSlider.value)
font.family: Settings.data.ui.fontFixed
Layout.preferredWidth: 30 * scaling
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "B"
font.weight: Font.Bold
Layout.preferredWidth: 20 * scaling
}
NSlider {
id: blueSlider
Layout.fillWidth: true
from: 0
to: 255
value: Math.round(root.selectedColor.b * 255)
onMoved: {
root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1)
// Update stored hue and saturation when RGB changes
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
root.selectedColor.b * 255)
root.currentHue = hsv[0]
root.currentSaturation = hsv[1]
}
}
NText {
text: Math.round(blueSlider.value)
font.family: Settings.data.ui.fontFixed
Layout.preferredWidth: 30 * scaling
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Brightness"
font.weight: Font.Bold
Layout.preferredWidth: 80 * scaling
}
NSlider {
id: brightnessSlider
Layout.fillWidth: true
from: 0
to: 100
value: {
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
root.selectedColor.b * 255)
return hsv[2]
}
onMoved: {
var hue = root.currentHue
var saturation = root.currentSaturation
if (hue === 0 && saturation === 0) {
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
root.selectedColor.b * 255)
hue = hsv[0]
saturation = hsv[1]
root.currentHue = hue
root.currentSaturation = saturation
}
var rgb = root.hsvToRgb(hue, saturation, value)
root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1)
}
}
NText {
text: Math.round(brightnessSlider.value) + "%"
font.family: Settings.data.ui.fontFixed
Layout.preferredWidth: 40 * scaling
}
}
}
}
NBox {
Layout.fillWidth: true
Layout.preferredHeight: themePalette.implicitHeight + Style.marginL * scaling * 2
ColumnLayout {
id: themePalette
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling
NLabel {
label: "Theme Colors"
description: "Quick access to your theme's color palette"
Layout.fillWidth: true
}
Flow {
spacing: 6 * scaling
Layout.fillWidth: true
flow: Flow.LeftToRight
Repeater {
model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000"]
Rectangle {
width: 24 * scaling
height: 24 * scaling
radius: 4 * scaling
color: modelData
border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline
border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedColor = modelData
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
root.selectedColor.b * 255)
root.currentHue = hsv[0]
root.currentSaturation = hsv[1]
}
}
}
}
}
}
}
NBox {
Layout.fillWidth: true
Layout.preferredHeight: genericPalette.implicitHeight + Style.marginL * scaling * 2
ColumnLayout {
id: genericPalette
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling
NLabel {
label: "Colors Palette"
description: "Choose from a wide range of predefined colors"
Layout.fillWidth: true
}
Flow {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 6 * scaling
flow: Flow.LeftToRight
Repeater {
model: ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", "#E74C3C", "#E67E22", "#F1C40F", "#2ECC71", "#1ABC9C", "#3498DB", "#2980B9", "#9B59B6", "#34495E", "#2C3E50", "#95A5A6", "#7F8C8D", "#FFFFFF", "#000000"]
Rectangle {
width: 24 * scaling
height: 24 * scaling
radius: Style.radiusXXS * scaling
color: modelData
border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline
border.width: Math.max(
1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedColor = modelData
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
root.selectedColor.b * 255)
root.currentHue = hsv[0]
root.currentSaturation = hsv[1]
}
}
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 20 * scaling
Layout.bottomMargin: 20 * scaling
spacing: 10 * scaling
Item {
Layout.fillWidth: true
}
NButton {
id: cancelButton
text: "Cancel"
icon: "close"
outlined: cancelButton.hovered ? false : true
customHeight: 36 * scaling
customWidth: 100 * scaling
onClicked: {
root.close()
}
}
NButton {
text: "Apply"
icon: "check"
customHeight: 36 * scaling
customWidth: 100 * scaling
onClicked: {
root.colorSelected(root.selectedColor)
root.close()
}
}
}
}
}
}

View file

@ -15,7 +15,6 @@ Rectangle {
property string tooltipText
property bool enabled: true
property bool hovering: false
property real fontPointSize: Style.fontSizeM
property color colorBg: Color.mSurfaceVariant
property color colorFg: Color.mPrimary
@ -41,7 +40,7 @@ Rectangle {
NIcon {
text: root.icon
font.pointSize: root.fontPointSize * scaling
font.pointSize: Style.fontSizeM * scaling
color: root.hovering ? colorFgHover : colorFg
// Center horizontally
x: (root.width - width) / 2

View file

@ -3,7 +3,8 @@ import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
// Input and button row
RowLayout {
id: root
// Public properties
@ -21,57 +22,35 @@ ColumnLayout {
// Internal properties
property real scaling: 1.0
spacing: Style.marginM * scaling
// Label
NText {
text: root.label
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
NTextInput {
id: textInput
label: root.label
description: root.description
placeholderText: root.placeholderText
text: root.text
onEditingFinished: {
root.text = text
root.editingFinished()
}
Layout.fillWidth: true
}
// Description
NText {
text: root.description
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.Wrap
Layout.fillWidth: true
}
NButton {
Layout.fillWidth: false
Layout.alignment: Qt.AlignBottom
// Input and button row
RowLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
text: root.actionButtonText
icon: root.actionButtonIcon
backgroundColor: Color.mSecondary
textColor: Color.mOnSecondary
hoverColor: Color.mTertiary
pressColor: Color.mPrimary
enabled: root.actionButtonEnabled
NTextInput {
id: textInput
placeholderText: root.placeholderText
text: root.text
onEditingFinished: {
root.text = text
root.editingFinished()
}
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
}
NButton {
text: root.actionButtonText
icon: root.actionButtonIcon
backgroundColor: Color.mSecondary
textColor: Color.mOnSecondary
hoverColor: Color.mTertiary
pressColor: Color.mPrimary
enabled: root.actionButtonEnabled
Layout.fillWidth: false
onClicked: {
root.actionClicked()
}
onClicked: {
root.actionClicked()
}
}
}

View file

@ -231,8 +231,7 @@ Item {
root.clicked()
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
}
else if (mouse.button === Qt.MiddleButton) {
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
}
}

View file

@ -44,7 +44,7 @@ RowLayout {
radius: height * 0.5 // Fully rounded like toggle
color: Color.mSurfaceVariant
border.color: root.hovering ? Color.mPrimary : Color.mOutline
border.width: 1
border.width: Math.max(1, Style.borderS * scaling)
Behavior on border.color {
ColorAnimation {

View file

@ -11,7 +11,6 @@ ColumnLayout {
property string description: ""
property bool readOnly: false
property bool enabled: true
property int inputMaxWidth: Math.round(420 * scaling)
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
property string fontFamily: Settings.data.ui.fontDefault
@ -26,7 +25,6 @@ ColumnLayout {
signal editingFinished
spacing: Style.marginS * scaling
implicitHeight: frame.height
NLabel {
label: root.label
@ -34,6 +32,7 @@ ColumnLayout {
labelColor: root.labelColor
descriptionColor: root.descriptionColor
visible: root.label !== "" || root.description !== ""
Layout.fillWidth: true
}
// Container
@ -42,50 +41,48 @@ ColumnLayout {
Layout.fillWidth: true
Layout.minimumWidth: 80 * scaling
Layout.maximumWidth: root.inputMaxWidth
implicitWidth: parent.width
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.color: input.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Focus ring
Rectangle {
anchors.fill: parent
radius: frame.radius
color: Color.transparent
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
TextField {
id: input
anchors.fill: parent
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
spacing: Style.marginS * scaling
TextField {
id: input
Layout.fillWidth: true
echoMode: TextInput.Normal
readOnly: root.readOnly
enabled: root.enabled
color: Color.mOnSurface
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
background: null
font.family: fontFamily
font.pointSize: fontSize
font.weight: fontWeight
onEditingFinished: root.editingFinished()
}
verticalAlignment: TextInput.AlignVCenter
echoMode: TextInput.Normal
readOnly: root.readOnly
enabled: root.enabled
color: Color.mOnSurface
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
selectByMouse: true
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
background: null
font.family: root.fontFamily
font.pointSize: root.fontSize
font.weight: root.fontWeight
onEditingFinished: root.editingFinished()
}
}
}

View file

@ -37,7 +37,16 @@ Item {
// NToast updates its scaling when showing.
scaling = ScalingService.getScreenScale(screen)
// Stop any running animations and reset state
showAnimation.stop()
hideAnimation.stop()
autoHideTimer.stop()
// Ensure we start from the hidden position
y = hiddenY
visible = true
// Start the show animation
showAnimation.start()
if (duration > 0 && !persistent) {
autoHideTimer.start()
@ -81,7 +90,6 @@ Item {
// Main toast container
Rectangle {
id: container
anchors.fill: parent
radius: Style.radiusL * scaling
@ -137,43 +145,41 @@ Item {
}
// Label and description
Column {
id: textColumn
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
NText {
id: labelText
Layout.fillWidth: true
text: root.label
color: Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
wrapMode: Text.WordWrap
width: parent.width
visible: text.length > 0
}
NText {
id: descriptionText
Layout.fillWidth: true
text: root.description
color: Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
wrapMode: Text.WordWrap
width: parent.width
visible: text.length > 0
}
}
// Close button (only if persistent or manual dismiss needed)
NIconButton {
id: closeButton
icon: "close"
visible: root.persistent || root.duration === 0
color: Color.mOnSurface
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.mOutline
fontPointSize: Style.fontSizeM * scaling
sizeRatio: 0.8
Layout.alignment: Qt.AlignTop