Merge branch 'main' into never

This commit is contained in:
Never Gude 2025-09-06 21:56:07 +02:00
commit b03f877c27
58 changed files with 2910 additions and 2147 deletions

View file

@ -510,7 +510,7 @@
} }
.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before { .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; color: {{colors.on_surface_variant.default.hex}} !important;
} }

View file

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

View file

@ -29,6 +29,7 @@ Singleton {
property int fontWeightBold: 700 property int fontWeightBold: 700
// Radii // Radii
property int radiusXXS: 4 * Settings.data.general.radiusRatio
property int radiusXS: 8 * Settings.data.general.radiusRatio property int radiusXS: 8 * Settings.data.general.radiusRatio
property int radiusS: 12 * Settings.data.general.radiusRatio property int radiusS: 12 * Settings.data.general.radiusRatio
property int radiusM: 16 * 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 // 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) { function formatVagueHumanReadableDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600) if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) return '0s'
const seconds = totalSeconds - (hours * 3600) - (minutes * 60) }
var str = "" // Floor the input to handle decimal seconds
if (hours) { totalSeconds = Math.floor(totalSeconds)
str += hours.toString() + "h"
} const days = Math.floor(totalSeconds / 86400)
if (minutes) { const hours = Math.floor((totalSeconds % 86400) / 3600)
str += minutes.toString() + "m" 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) { if (!hours && !minutes) {
str += seconds.toString() + "s" parts.push(`${seconds}s`)
} }
return str
return parts.join('')
} }
Timer { Timer {

View file

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

View file

@ -2,38 +2,45 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
Row { RowLayout {
id: root id: root
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
readonly property real minWidth: 160 readonly property real minWidth: 160
readonly property real maxWidth: 400 readonly property real maxWidth: 400
Layout.alignment: Qt.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: getTitle() !== "" visible: getTitle() !== ""
function 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 : "" return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
} }
function getAppIcon() { function getAppIcon() {
// Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow() const focusedWindow = CompositorService.getFocusedWindow()
if (!focusedWindow || !focusedWindow.appId) if (focusedWindow && focusedWindow.appId) {
return "" 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 { NText {
id: fullTitleMetrics id: fullTitleMetrics
visible: false visible: false
@ -43,15 +50,13 @@ Row {
} }
Rectangle { Rectangle {
// Let the Rectangle size itself based on its content (the Row) id: windowTitleRect
visible: root.visible visible: root.visible
width: row.width + Style.marginM * 2 * scaling Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling) Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
Item { Item {
id: mainContainer id: mainContainer
anchors.fill: parent anchors.fill: parent
@ -59,16 +64,16 @@ Row {
anchors.rightMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling
clip: true clip: true
Row { RowLayout {
id: row id: contentLayout
anchors.verticalCenter: parent.verticalCenter anchors.centerIn: parent
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Window icon // Window icon
Item { Item {
width: Style.fontSizeL * scaling * 1.2 Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
height: Style.fontSizeL * scaling * 1.2 Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
anchors.verticalCenter: parent.verticalCenter Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
IconImage { IconImage {
@ -83,26 +88,24 @@ Row {
NText { NText {
id: titleText id: titleText
Layout.preferredWidth: {
// For short titles, show full. For long titles, truncate and expand on hover
width: {
if (mouseArea.containsMouse) { if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else { } else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
} }
} }
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
text: getTitle() text: getTitle()
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mSecondary color: Color.mSecondary
clip: true clip: true
Behavior on width { Behavior on Layout.preferredWidth {
NumberAnimation { NumberAnimation {
duration: Style.animationSlow duration: Style.animationSlow
easing.type: Easing.InOutCubic 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 { NTooltip {
id: tooltip id: tooltip
text: Time.dateString text: `${Time.dateString}.`
target: clock target: clock
positionAbove: Settings.data.bar.position === "bottom" positionAbove: Settings.data.bar.position === "bottom"
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ NIconButton {
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off" 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 onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
onRightClicked: { onRightClicked: {

View file

@ -14,11 +14,14 @@ NIconButton {
property real scaling: 1.0 property real scaling: 1.0
sizeRatio: 0.8 sizeRatio: 0.8
icon: "notifications" icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications"
tooltipText: "Notification history" 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 colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this) 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 property real scaling: 1.0
icon: Settings.data.bar.useDistroLogo ? "" : "widgets" icon: Settings.data.bar.useDistroLogo ? "" : "widgets"
tooltipText: "Open side panel" tooltipText: "Open side panel."
sizeRatio: 0.8 sizeRatio: 0.8
colorBg: Color.mSurfaceVariant 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
import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
Row { RowLayout {
id: root id: root
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
anchors.verticalCenter: parent.verticalCenter Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
Rectangle { Rectangle {
// Let the Rectangle size itself based on its content (the Row) Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
width: row.width + Style.marginM * scaling * 2 Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2
Layout.alignment: Qt.AlignVCenter
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
anchors.verticalCenter: parent.verticalCenter RowLayout {
id: mainLayout
Item {
id: mainContainer
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
Row { // CPU Usage Component
id: row RowLayout {
anchors.verticalCenter: parent.verticalCenter id: cpuUsageLayout
spacing: Style.marginS * scaling spacing: Style.marginXS * scaling
Row { Layout.alignment: Qt.AlignVCenter
id: cpuUsageLayout
spacing: Style.marginXS * scaling
NIcon { NIcon {
id: cpuUsageIcon id: cpuUsageIcon
text: "speed" text: "speed"
anchors.verticalCenter: parent.verticalCenter Layout.alignment: Qt.AlignVCenter
}
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
}
} }
// CPU Temperature Component NText {
Row { id: cpuUsageText
id: cpuTempLayout text: `${SystemStatService.cpuUsage}%`
// spacing is thin here to compensate for the vertical thermometer icon font.family: Settings.data.ui.fontFixed
spacing: Style.marginXXS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon { // CPU Temperature Component
text: "thermometer" RowLayout {
anchors.verticalCenter: parent.verticalCenter id: cpuTempLayout
} // spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling
Layout.alignment: Qt.AlignVCenter
NText { NIcon {
text: `${SystemStatService.cpuTemp}°C` text: "thermometer"
font.family: Settings.data.ui.fontFixed Layout.alignment: Qt.AlignVCenter
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
// Memory Usage Component NText {
Row { text: `${SystemStatService.cpuTemp}°C`
id: memoryUsageLayout font.family: Settings.data.ui.fontFixed
spacing: Style.marginXS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon { // Memory Usage Component
text: "memory" RowLayout {
anchors.verticalCenter: parent.verticalCenter id: memoryUsageLayout
} spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NText { NIcon {
text: `${SystemStatService.memoryUsageGb}G` text: "memory"
font.family: Settings.data.ui.fontFixed Layout.alignment: Qt.AlignVCenter
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
// Network Download Speed Component NText {
Row { text: `${SystemStatService.memoryUsageGb}G`
id: networkDownloadLayout font.family: Settings.data.ui.fontFixed
spacing: Style.marginXS * scaling font.pointSize: Style.fontSizeS * scaling
visible: Settings.data.bar.showNetworkStats font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon { // Network Download Speed Component
text: "download" RowLayout {
anchors.verticalCenter: parent.verticalCenter id: networkDownloadLayout
} spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats
NText { NIcon {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) text: "download"
font.family: Settings.data.ui.fontFixed Layout.alignment: Qt.AlignVCenter
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
// Network Upload Speed Component NText {
Row { text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
id: networkUploadLayout font.family: Settings.data.ui.fontFixed
spacing: Style.marginXS * scaling font.pointSize: Style.fontSizeS * scaling
visible: Settings.data.bar.showNetworkStats font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
NIcon { // Network Upload Speed Component
text: "upload" RowLayout {
anchors.verticalCenter: parent.verticalCenter id: networkUploadLayout
} spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats
NText { NIcon {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed) text: "upload"
font.family: Settings.data.ui.fontFixed Layout.alignment: Qt.AlignVCenter
font.pointSize: Style.fontSizeS * scaling }
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter NText {
verticalAlignment: Text.AlignVCenter text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
color: Color.mPrimary 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
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
@ -17,15 +18,14 @@ Rectangle {
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
// Always visible when there are toplevels // 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) implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
Row { RowLayout {
id: taskbarRow id: taskbarLayout
anchors.verticalCenter: parent.verticalCenter anchors.centerIn: parent
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.marginXXS * root.scaling spacing: Style.marginXXS * root.scaling
Repeater { Repeater {
@ -35,8 +35,10 @@ Rectangle {
required property Toplevel modelData required property Toplevel modelData
property Toplevel toplevel: modelData property Toplevel toplevel: modelData
property bool isActive: ToplevelManager.activeToplevel === 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 { Rectangle {
id: iconBackground id: iconBackground
@ -89,7 +91,7 @@ Rectangle {
NTooltip { NTooltip {
id: taskbarTooltip id: taskbarTooltip
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App" text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
target: taskbarItem target: taskbarItem
positionAbove: Settings.data.bar.position === "bottom" positionAbove: Settings.data.bar.position === "bottom"
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -82,7 +82,11 @@ Item {
"isImage": false, "isImage": false,
"onActivate": function () { "onActivate": function () {
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`) 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() app.execute()
} else if (app.exec) { } else if (app.exec) {
// Fallback to manual execution // Fallback to manual execution

View file

@ -155,7 +155,7 @@ Loader {
anchors.topMargin: 80 * scaling anchors.topMargin: 80 * scaling
spacing: 40 * scaling spacing: 40 * scaling
Column { ColumnLayout {
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -168,6 +168,7 @@ Loader {
font.letterSpacing: -2 * scaling font.letterSpacing: -2 * scaling
color: Color.mOnSurface color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
SequentialAnimation on scale { SequentialAnimation on scale {
loops: Animation.Infinite loops: Animation.Infinite
@ -192,22 +193,23 @@ Loader {
font.weight: Font.Light font.weight: Font.Light
color: Color.mOnSurface color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
width: timeText.width Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: timeText.implicitWidth
} }
} }
Column { ColumnLayout {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Rectangle { Rectangle {
width: 108 * scaling Layout.preferredWidth: 108 * scaling
height: 108 * scaling Layout.preferredHeight: 108 * scaling
Layout.alignment: Qt.AlignHCenter
radius: width * 0.5 radius: width * 0.5
color: Color.transparent color: Color.transparent
border.color: Color.mPrimary border.color: Color.mPrimary
border.width: Math.max(1, Style.borderL * scaling) border.width: Math.max(1, Style.borderL * scaling)
anchors.horizontalCenter: parent.horizontalCenter
z: 10 z: 10
Loader { Loader {
@ -375,377 +377,371 @@ Loader {
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: 50 * scaling anchors.verticalCenterOffset: 50 * scaling
Item { Rectangle {
width: parent.width id: terminalBackground
height: 280 * scaling anchors.fill: parent
Layout.fillWidth: true radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
Rectangle { border.color: Color.mPrimary
id: terminalBackground border.width: Math.max(1, Style.borderM * scaling)
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
}
}
}
}
Repeater {
model: 20
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 40 * scaling height: 1
color: Qt.alpha(Color.mPrimary, 0.2) color: Qt.alpha(Color.mPrimary, 0.1)
topLeftRadius: Style.radiusS * scaling y: index * 10 * scaling
topRightRadius: Style.radiusS * scaling opacity: Style.opacityMedium
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
SequentialAnimation on opacity { SequentialAnimation on opacity {
loops: Animation.Infinite loops: Animation.Infinite
NumberAnimation { NumberAnimation {
to: 0.6 to: 0.6
duration: 2000 duration: 2000 + Math.random() * 1000
easing.type: Easing.InOutQuad
} }
NumberAnimation { NumberAnimation {
to: 0.2 to: 0.1
duration: 2000 duration: 2000 + Math.random() * 1000
easing.type: Easing.InOutQuad
} }
} }
} }
} }
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 // Power buttons at bottom right
Row { RowLayout {
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: 50 * scaling anchors.margins: 50 * scaling
spacing: 20 * scaling spacing: 20 * scaling
Rectangle { Rectangle {
width: 60 * scaling Layout.preferredWidth: 60 * scaling
height: 60 * scaling Layout.preferredHeight: 60 * scaling
radius: width * 0.5 radius: width * 0.5
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2) color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
border.color: Color.mError border.color: Color.mError
@ -769,8 +765,8 @@ Loader {
} }
Rectangle { Rectangle {
width: 60 * scaling Layout.preferredWidth: 60 * scaling
height: 60 * scaling Layout.preferredHeight: 60 * scaling
radius: width * 0.5 radius: width * 0.5
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight) color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
border.color: Color.mPrimary border.color: Color.mPrimary
@ -794,8 +790,8 @@ Loader {
} }
Rectangle { Rectangle {
width: 60 * scaling Layout.preferredWidth: 60 * scaling
height: 60 * scaling Layout.preferredHeight: 60 * scaling
radius: width * 0.5 radius: width * 0.5
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2) color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
border.color: Color.mSecondary border.color: Color.mSecondary

View file

@ -78,7 +78,7 @@ Variants {
} }
// Main notification container // Main notification container
Column { ColumnLayout {
id: notificationStack id: notificationStack
// Position based on bar location // Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
@ -92,8 +92,9 @@ Variants {
Repeater { Repeater {
model: notificationModel model: notificationModel
delegate: Rectangle { delegate: Rectangle {
width: 360 * scaling Layout.preferredWidth: 360 * scaling
height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling)) Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
Layout.maximumHeight: Layout.preferredHeight
clip: true clip: true
radius: Style.radiusL * scaling radius: Style.radiusL * scaling
border.color: Color.mOutline border.color: Color.mOutline
@ -105,6 +106,17 @@ Variants {
property real opacityValue: 0.0 property real opacityValue: 0.0
property bool isRemoving: false 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 and fade-in animation
scale: scaleValue scale: scaleValue
opacity: opacityValue opacity: opacityValue
@ -156,104 +168,139 @@ Variants {
} }
} }
RowLayout { ColumnLayout {
id: contentRow id: notificationLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginL * scaling anchors.margins: Style.marginM * scaling
spacing: Style.marginL * scaling anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
spacing: Style.marginM * scaling
// Right: header on top, then avatar + texts // Header section with app name and timestamp
ColumnLayout { RowLayout {
id: textColumn
spacing: Style.marginS * scaling
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginS * scaling
RowLayout { NText {
spacing: Style.marginS * scaling text: `${(model.appName || model.desktopEntry)
id: appHeaderRow || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
NText { color: Color.mSecondary
text: `${(model.appName || model.desktopEntry) font.pointSize: Style.fontSizeXS * scaling
|| "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
}
} }
RowLayout { Rectangle {
id: bodyRow Layout.preferredWidth: 6 * scaling
spacing: Style.marginM * 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 { Item {
id: appAvatar Layout.fillWidth: true
Layout.preferredWidth: 40 * scaling }
Layout.preferredHeight: 40 * scaling }
Layout.alignment: Qt.AlignTop
// Start avatar aligned with body (below the summary) // Main content section
anchors.topMargin: textContent.childrenRect.y RowLayout {
// Prefer notification-provided image (e.g., user avatar) then fall back to app icon Layout.fillWidth: true
imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName( spacing: Style.marginM * scaling
model.appIcon, "application-x-executable")
fallbackIcon: "apps" // Avatar
borderColor: Color.transparent NImageCircled {
borderWidth: 0 id: appAvatar
visible: (imagePath && imagePath !== "") 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 { NText {
id: textContent text: model.body || ""
spacing: Style.marginS * scaling font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true Layout.fillWidth: true
// Ensure a concrete width so text wraps maximumLineCount: 5
width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0)) elide: Text.ElideRight
visible: text.length > 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
}
} }
} }
} }
// 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 { NIconButton {
icon: "close" icon: "close"
tooltipText: "Close" tooltipText: "Close"
// Compact target (~24dp) and glyph (~16dp) sizeRatio: 0.6
sizeRatio: 0.75
fontPointSize: 16
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Style.marginM * scaling
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Style.marginS * scaling anchors.rightMargin: Style.marginM * scaling
onClicked: { onClicked: {
animateOut() animateOut()

View file

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

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
@ -16,6 +17,7 @@ NPanel {
panelHeight: 380 * scaling panelHeight: 380 * scaling
panelAnchorHorizontalCenter: true panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Timer properties // Timer properties
property int timerDuration: 9000 // 9 seconds property int timerDuration: 9000 // 9 seconds
@ -23,9 +25,44 @@ NPanel {
property bool timerActive: false property bool timerActive: false
property int timeRemaining: 0 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: { onClosed: {
cancelTimer() cancelTimer()
selectedIndex = 0
} }
// Timer management // Timer management
@ -79,6 +116,38 @@ NPanel {
root.close() 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 // Countdown timer
Timer { Timer {
id: countdownTimer id: countdownTimer
@ -93,8 +162,92 @@ NPanel {
} }
panelContent: Rectangle { panelContent: Rectangle {
id: ui
color: Color.transparent 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 { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Style.marginL * scaling anchors.topMargin: Style.marginL * scaling
@ -144,55 +297,21 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Lock Screen Repeater {
PowerButton { model: powerOptions
Layout.fillWidth: true delegate: PowerButton {
icon: "lock_outline" Layout.fillWidth: true
title: "Lock" icon: modelData.icon
subtitle: "Lock your session" title: modelData.title
onClicked: startTimer("lock") subtitle: modelData.subtitle
pending: timerActive && pendingAction === "lock" isShutdown: modelData.isShutdown || false
} isSelected: index === selectedIndex
onClicked: {
// Suspend selectedIndex = index
PowerButton { startTimer(modelData.action)
Layout.fillWidth: true }
icon: "bedtime" pending: timerActive && pendingAction === modelData.action
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
} }
} }
} }
@ -207,6 +326,7 @@ NPanel {
property string subtitle: "" property string subtitle: ""
property bool pending: false property bool pending: false
property bool isShutdown: false property bool isShutdown: false
property bool isSelected: false
signal clicked signal clicked
@ -216,7 +336,7 @@ NPanel {
if (pending) { if (pending) {
return Qt.alpha(Color.mPrimary, 0.08) return Qt.alpha(Color.mPrimary, 0.08)
} }
if (mouseArea.containsMouse) { if (isSelected || mouseArea.containsMouse) {
return Color.mSecondary return Color.mSecondary
} }
return Color.transparent return Color.transparent
@ -242,13 +362,12 @@ NPanel {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: buttonRoot.icon text: buttonRoot.icon
color: { color: {
if (buttonRoot.pending) if (buttonRoot.pending)
return Color.mPrimary return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse) if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError return Color.mError
if (mouseArea.containsMouse) if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary return Color.mOnSecondary
return Color.mOnSurface return Color.mOnSurface
} }
font.pointSize: Style.fontSizeXXXL * scaling font.pointSize: Style.fontSizeXXXL * scaling
@ -264,7 +383,7 @@ NPanel {
} }
// Text content in the middle // Text content in the middle
Column { ColumnLayout {
anchors.left: iconElement.right anchors.left: iconElement.right
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -279,10 +398,10 @@ NPanel {
color: { color: {
if (buttonRoot.pending) if (buttonRoot.pending)
return Color.mPrimary return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse) if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError return Color.mError
if (mouseArea.containsMouse) if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary return Color.mOnSecondary
return Color.mOnSurface return Color.mOnSurface
} }
@ -304,10 +423,10 @@ NPanel {
color: { color: {
if (buttonRoot.pending) if (buttonRoot.pending)
return Color.mPrimary return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse) if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError return Color.mError
if (mouseArea.containsMouse) if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary return Color.mOnSecondary
return Color.mOnSurfaceVariant return Color.mOnSurfaceVariant
} }
opacity: Style.opacityHeavy opacity: Style.opacityHeavy

View file

@ -68,6 +68,8 @@ Popup {
sourceComponent: { sourceComponent: {
if (settingsPopup.widgetId === "CustomButton") { if (settingsPopup.widgetId === "CustomButton") {
return customButtonSettings return customButtonSettings
} else if (settingsPopup.widgetId === "Spacer") {
return spacerSettings
} }
// Add more widget settings components here as needed // Add more widget settings components here as needed
return null 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 { panelContent: Rectangle {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
color: Color.transparent color: Color.transparent
// Scrolling via keyboard // Main layout container that fills the panel
Shortcut { ColumnLayout {
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 {
anchors.fill: parent anchors.fill: parent
spacing: Style.marginM * scaling anchors.margins: Style.marginL * scaling
spacing: 0
Rectangle { // Keyboard shortcuts container
id: sidebar Item {
Layout.preferredWidth: 220 * scaling Layout.preferredWidth: 0
Layout.fillHeight: true Layout.preferredHeight: 0
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
MouseArea { // Scrolling via keyboard
anchors.fill: parent Shortcut {
acceptedButtons: Qt.NoButton // Don't interfere with clicks sequence: "Down"
property int wheelAccumulator: 0 onActivated: root.scrollDown()
onWheel: wheel => { enabled: root.opened
wheelAccumulator += wheel.angleDelta.y
if (wheelAccumulator >= 120) {
root.selectPreviousTab()
wheelAccumulator = 0
} else if (wheelAccumulator <= -120) {
root.selectNextTab()
wheelAccumulator = 0
}
wheel.accepted = true
}
} }
Column { Shortcut {
anchors.fill: parent sequence: "Up"
anchors.margins: Style.marginS * scaling onActivated: root.scrollUp()
spacing: Style.marginXS * 1.5 * scaling enabled: root.opened
}
Repeater { Shortcut {
id: sections sequence: "Ctrl+J"
model: root.tabsModel onActivated: root.scrollDown()
delegate: Rectangle { enabled: root.opened
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)
Behavior on color { Shortcut {
ColorAnimation { sequence: "Ctrl+K"
duration: Style.animationFast onActivated: root.scrollUp()
} enabled: root.opened
} }
Behavior on tabTextColor { Shortcut {
ColorAnimation { sequence: "PgDown"
duration: Style.animationFast onActivated: root.scrollPageDown()
} enabled: root.opened
} }
RowLayout { Shortcut {
anchors.fill: parent sequence: "PgUp"
anchors.leftMargin: Style.marginS * scaling onActivated: root.scrollPageUp()
anchors.rightMargin: Style.marginS * scaling enabled: root.opened
spacing: Style.marginS * scaling }
// Tab icon on the left side
NIcon { // Changing tab via keyboard
text: modelData.icon Shortcut {
color: tabTextColor sequence: "Tab"
font.pointSize: Style.fontSizeL * scaling onActivated: root.selectNextTab()
} enabled: root.opened
// Tab label on the left side }
NText {
text: modelData.label Shortcut {
color: tabTextColor sequence: "Shift+Tab"
font.pointSize: Style.fontSizeM * scaling onActivated: root.selectPreviousTab()
font.weight: Style.fontWeightBold enabled: root.opened
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
}
}
}
} }
} }
// Content // Main content area
Rectangle { RowLayout {
id: contentPane
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
radius: Style.radiusM * scaling spacing: Style.marginM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
ColumnLayout { // Sidebar
id: contentLayout Rectangle {
anchors.fill: parent id: sidebar
anchors.margins: Style.marginL * scaling Layout.preferredWidth: 220 * scaling
spacing: Style.marginS * 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 { MouseArea {
id: headerRow anchors.fill: parent
Layout.fillWidth: true acceptedButtons: Qt.NoButton // Don't interfere with clicks
spacing: Style.marginS * scaling property int wheelAccumulator: 0
onWheel: wheel => {
// Tab label on the main right side wheelAccumulator += wheel.angleDelta.y
NText { if (wheelAccumulator >= 120) {
text: root.tabsModel[currentTabIndex].label root.selectPreviousTab()
font.pointSize: Style.fontSizeXL * scaling wheelAccumulator = 0
font.weight: Style.fontWeightBold } else if (wheelAccumulator <= -120) {
color: Color.mPrimary root.selectNextTab()
Layout.fillWidth: true wheelAccumulator = 0
} }
NIconButton { wheel.accepted = true
icon: "close" }
tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}
} }
NDivider { ColumnLayout {
Layout.fillWidth: true anchors.fill: parent
} anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Repeater { Repeater {
id: sections
model: root.tabsModel model: root.tabsModel
delegate: Loader { delegate: Rectangle {
anchors.fill: parent id: tabItem
active: index === root.currentTabIndex 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: { Behavior on color {
if (status === Loader.Ready && item) { ColorAnimation {
// Find and store reference to the ScrollView duration: Style.animationFast
const scrollView = item.children[0]
if (scrollView && scrollView.toString().includes("ScrollView")) {
root.activeScrollView = scrollView
}
} }
} }
sourceComponent: ColumnLayout { Behavior on tabTextColor {
ScrollView { ColorAnimation {
id: scrollView 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.fillWidth: true
Layout.fillHeight: true }
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff }
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
Component.onCompleted: { MouseArea {
root.activeScrollView = scrollView 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 { sourceComponent: Flickable {
active: true // Using a Flickable here with a pressDelay to fix conflict between
sourceComponent: root.tabsModel[index].source // ScrollView and NTextInput. This fixes the weird text selection issue.
width: scrollView.availableWidth 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 { Rectangle {
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginS * scaling 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) Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusL * scaling radius: Style.radiusL * scaling
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
@ -85,11 +85,12 @@ ColumnLayout {
} }
RowLayout { RowLayout {
id: updateRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIcon { NIcon {
text: "system_update" text: "download"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
} }

View file

@ -22,9 +22,11 @@ ColumnLayout {
fallbackIcon: "person" fallbackIcon: "person"
borderColor: Color.mPrimary borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling) borderWidth: Math.max(1, Style.borderM * scaling)
Layout.alignment: Qt.AlignTop
} }
NTextInput { NTextInput {
Layout.fillWidth: true
label: `${Quickshell.env("USER") || "user"}'s profile picture` label: `${Quickshell.env("USER") || "user"}'s profile picture`
description: "Your profile picture that appears throughout the interface." description: "Your profile picture that appears throughout the interface."
text: Settings.data.general.avatarImage text: Settings.data.general.avatarImage
@ -75,6 +77,45 @@ ColumnLayout {
onToggled: checked => Settings.data.dock.autoHide = checked 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 { ColumnLayout {
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -5,94 +5,85 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
ScrollView { ColumnLayout {
id: root id: contentColumn
spacing: Style.marginL * scaling
width: root.width
property real scaling: 1.0 // Enable/Disable Toggle
NToggle {
contentWidth: contentColumn.width label: "Enable Hooks"
contentHeight: contentColumn.height description: "Enable or disable all hook commands."
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked
}
ColumnLayout { ColumnLayout {
id: contentColumn visible: Settings.data.hooks.enabled
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
width: root.width Layout.fillWidth: true
// Enable/Disable Toggle NDivider {
NToggle { Layout.fillWidth: true
label: "Enable Hooks"
description: "Enable or disable all hook commands."
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked
} }
// 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 { ColumnLayout {
visible: Settings.data.hooks.enabled spacing: Style.marginM * scaling
spacing: Style.marginL * scaling
Layout.fillWidth: true Layout.fillWidth: true
NDivider { NLabel {
Layout.fillWidth: true 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 NLabel {
NInputAction { label: "Available Parameters"
id: wallpaperHookInput description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
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)"
}
} }
} }
} }

View file

@ -59,6 +59,13 @@ ColumnLayout {
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked 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 { ColumnLayout {
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true

View file

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

View file

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

View file

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

View file

@ -59,7 +59,7 @@ NBox {
} }
NIconButton { NIconButton {
icon: "settings" icon: "settings"
tooltipText: "Open settings" tooltipText: "Open settings."
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.open(screen) settingsPanel.open(screen)
@ -69,7 +69,7 @@ NBox {
NIconButton { NIconButton {
id: powerButton id: powerButton
icon: "power_settings_new" icon: "power_settings_new"
tooltipText: "Power menu" tooltipText: "Power menu."
onClicked: { onClicked: {
powerPanel.open(screen) powerPanel.open(screen)
sidePanel.close() sidePanel.close()
@ -79,7 +79,7 @@ NBox {
NIconButton { NIconButton {
id: closeButton id: closeButton
icon: "close" icon: "close"
tooltipText: "Close side panel" tooltipText: "Close side panel."
onClicked: { onClicked: {
sidePanel.close() sidePanel.close()
} }
@ -104,19 +104,7 @@ NBox {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
var minutes = Math.floor(uptimeSeconds / 60) % 60 uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds)
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"
}
uptimeProcess.running = false uptimeProcess.running = false
} }
} }

View file

@ -11,7 +11,7 @@ NBox {
Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling
implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling
Column { ColumnLayout {
id: content id: content
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -22,11 +22,6 @@ NBox {
anchors.bottomMargin: Style.marginM * scaling anchors.bottomMargin: Style.marginM * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Slight top padding
Item {
height: Style.marginXS * scaling
}
NCircleStat { NCircleStat {
value: SystemStatService.cpuUsage value: SystemStatService.cpuUsage
icon: "speed" icon: "speed"
@ -60,10 +55,5 @@ NBox {
width: 72 * scaling width: 72 * scaling
height: 68 * 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 // Screen Recorder
NIconButton { NIconButton {
icon: "videocam" 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 colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: { onClicked: {
@ -42,7 +42,7 @@ NBox {
// Idle Inhibitor // Idle Inhibitor
NIconButton { NIconButton {
icon: "coffee" 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 colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: { onClicked: {
@ -54,7 +54,7 @@ NBox {
NIconButton { NIconButton {
visible: Settings.data.wallpaper.enabled visible: Settings.data.wallpaper.enabled
icon: "image" 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: { onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel") var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector

View file

@ -14,16 +14,11 @@ NPanel {
panelHeight: 500 * scaling panelHeight: 500 * scaling
panelKeyboardFocus: true panelKeyboardFocus: true
property string passwordPromptSsid: "" property string passwordSsid: ""
property string passwordInput: "" property string passwordInput: ""
property bool showPasswordPrompt: false property string expandedSsid: ""
property string expandedNetwork: "" // Track which network shows options
onOpened: { onOpened: NetworkService.scan()
if (Settings.data.network.wifiEnabled) {
NetworkService.refreshNetworks()
}
}
panelContent: Rectangle { panelContent: Rectangle {
color: Color.transparent color: Color.transparent
@ -39,35 +34,32 @@ NPanel {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NIcon {
text: "wifi" text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
} }
NText { NText {
text: "WiFi" text: "Wi-Fi"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mOnSurface color: Color.mOnSurface
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling
} }
// Connection status indicator NToggle {
Rectangle { id: wifiSwitch
visible: NetworkService.hasActiveConnection checked: Settings.data.network.wifiEnabled
width: 8 * scaling onToggled: checked => NetworkService.setWifiEnabled(checked)
height: 8 * scaling baseSize: Style.baseWidgetSize * 0.65 * scaling
radius: 4 * scaling
color: Color.mPrimary
} }
NIconButton { NIconButton {
icon: "refresh" icon: "refresh"
tooltipText: "Refresh networks" tooltipText: "Refresh"
sizeRatio: 0.8 sizeRatio: 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
onClicked: NetworkService.refreshNetworks() onClicked: NetworkService.scan()
} }
NIconButton { NIconButton {
@ -82,17 +74,18 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
// Error banner // Error message
Rectangle { Rectangle {
visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 visible: NetworkService.lastError.length > 0
Layout.fillWidth: true 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) color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1)
radius: Style.radiusS * scaling radius: Style.radiusS * scaling
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mError border.color: Color.mError
RowLayout { RowLayout {
id: errorRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
@ -104,8 +97,7 @@ NPanel {
} }
NText { NText {
id: errorText text: NetworkService.lastError
text: NetworkService.connectError
color: Color.mError color: Color.mError
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
wrapMode: Text.Wrap wrapMode: Text.Wrap
@ -115,301 +107,364 @@ NPanel {
NIconButton { NIconButton {
icon: "close" icon: "close"
sizeRatio: 0.6 sizeRatio: 0.6
onClicked: { onClicked: NetworkService.lastError = ""
NetworkService.connectStatus = ""
NetworkService.connectError = ""
}
} }
} }
} }
ScrollView { // Main content area
Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff color: Color.transparent
ScrollBar.vertical.policy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// WiFi disabled state
ColumnLayout { ColumnLayout {
width: parent.width visible: !Settings.data.network.wifiEnabled
anchors.fill: parent
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Loading state Item {
ColumnLayout { Layout.fillHeight: true
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
}
} }
// 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 { ColumnLayout {
Layout.fillWidth: true width: parent.width
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: !Settings.data.network.wifiEnabled
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { // Network list
text: "wifi_off" Repeater {
font.pointSize: Style.fontSizeXXXL * scaling model: {
color: Color.mOnSurfaceVariant if (!Settings.data.network.wifiEnabled)
Layout.alignment: Qt.AlignHCenter return []
}
NText { const nets = Object.values(NetworkService.networks)
text: "WiFi is disabled" return nets.sort((a, b) => {
font.pointSize: Style.fontSizeL * scaling if (a.connected !== b.connected)
color: Color.mOnSurfaceVariant return b.connected - a.connected
Layout.alignment: Qt.AlignHCenter return b.signal - a.signal
} })
NButton {
text: "Enable WiFi"
icon: "wifi"
Layout.alignment: Qt.AlignHCenter
onClicked: {
Settings.data.network.wifiEnabled = true
Settings.save()
NetworkService.setWifiEnabled(true)
} }
}
}
// 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 { Rectangle {
id: networkRect Layout.fillWidth: true
width: parent.width implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling 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, color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b,
0.05) : Color.mSurface 0.05) : Color.mSurface
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
border.color: modelData.connected ? Color.mPrimary : Color.mOutline border.color: modelData.connected ? Color.mPrimary : Color.mOutline
clip: true
// Smooth opacity animation
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
ColumnLayout { ColumnLayout {
id: networkContent id: netColumn
width: parent.width - (Style.marginM * scaling * 2) width: parent.width - (Style.marginM * scaling * 2)
x: Style.marginM * scaling x: Style.marginM * scaling
y: Style.marginM * scaling y: Style.marginM * scaling
spacing: Style.marginM * scaling spacing: Style.marginS * scaling
// Main network row // Main row
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Signal icon
NIcon { NIcon {
text: NetworkService.signalIcon(modelData.signal) text: NetworkService.signalIcon(modelData.signal)
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: modelData.connected ? Color.mPrimary : Color.mOnSurface color: modelData.connected ? Color.mPrimary : Color.mOnSurface
} }
// Network info
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter spacing: 2 * scaling
spacing: 0
NText { NText {
text: modelData.ssid || "Unknown Network" text: modelData.ssid
font.pointSize: Style.fontSizeNormal * scaling font.pointSize: Style.fontSizeNormal * scaling
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
elide: Text.ElideRight
color: Color.mOnSurface color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
} }
NText { RowLayout {
text: { spacing: Style.marginXS * scaling
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)
NText { NText {
id: connectedLabel text: `${modelData.signal}%`
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"
font.pointSize: Style.fontSizeXXS * scaling font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
} }
}
// Loading indicator NText {
NBusyIndicator { text: "•"
visible: NetworkService.connectingSsid === modelData.ssid font.pointSize: Style.fontSizeXXS * scaling
running: NetworkService.connectingSsid === modelData.ssid color: Color.mOnSurfaceVariant
color: Color.mPrimary }
size: Style.baseWidgetSize * 0.6 * scaling
}
// Action buttons NText {
RowLayout { text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
spacing: Style.marginXS * scaling font.pointSize: Style.fontSizeXXS * scaling
visible: NetworkService.connectingSsid !== modelData.ssid color: Color.mOnSurfaceVariant
}
NButton { Item {
visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt) Layout.preferredWidth: Style.marginXXS * scaling
outlined: !hovered }
fontSize: Style.fontSizeXS * scaling
text: modelData.existing ? "Connect" : (NetworkService.isSecured( // Update the status badges area (around line 237)
modelData.security) ? "Password" : "Connect") Rectangle {
onClicked: { visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
if (modelData.existing || !NetworkService.isSecured(modelData.security)) { color: Color.mPrimary
NetworkService.connectNetwork(modelData.ssid, modelData.security) radius: height * 0.5
} else { width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
expandedNetwork = modelData.ssid height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2)
passwordPromptSsid = modelData.ssid
showPasswordPrompt = true NText {
passwordInput = "" id: connectedText
Qt.callLater(() => passwordInputField.forceActiveFocus()) anchors.centerIn: parent
} text: "Connected"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
} }
} }
NButton { Rectangle {
visible: modelData.connected visible: NetworkService.disconnectingFrom === modelData.ssid
outlined: !hovered color: Color.mError
fontSize: Style.fontSizeXS * scaling radius: height * 0.5
backgroundColor: Color.mError width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2)
text: "Disconnect" height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2)
onClicked: NetworkService.disconnectNetwork(modelData.ssid)
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 { Rectangle {
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
&& NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: visible ? 50 * scaling : 0 height: passwordRow.implicitHeight + Style.marginS * scaling * 2
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusS * scaling radius: Style.radiusS * scaling
RowLayout { RowLayout {
id: passwordRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling spacing: Style.marginM * scaling
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
radius: Style.radiusS * scaling radius: Style.radiusXS * scaling
color: Color.mSurface 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) border.width: Math.max(1, Style.borderS * scaling)
TextInput { TextInput {
id: passwordInputField id: pwdInput
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginM * scaling anchors.margins: Style.marginS * scaling
anchors.rightMargin: Style.marginM * scaling
height: parent.height
text: passwordInput text: passwordInput
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurface color: Color.mOnSurface
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt
selectByMouse: true
echoMode: TextInput.Password echoMode: TextInput.Password
selectByMouse: true
focus: visible
passwordCharacter: "●" passwordCharacter: "●"
onTextChanged: passwordInput = text onTextChanged: passwordInput = text
onVisibleChanged: if (visible)
forceActiveFocus()
onAccepted: { onAccepted: {
if (passwordInput) { if (text) {
NetworkService.submitPassword(passwordPromptSsid, passwordInput) NetworkService.connect(passwordSsid, text)
showPasswordPrompt = false passwordSsid = ""
expandedNetwork = "" passwordInput = ""
} }
} }
@ -425,56 +480,75 @@ NPanel {
NButton { NButton {
text: "Connect" text: "Connect"
icon: "check" fontSize: Style.fontSizeXXS * scaling
fontSize: Style.fontSizeXS * scaling
enabled: passwordInput.length > 0 enabled: passwordInput.length > 0
outlined: !enabled outlined: true
onClicked: { onClicked: {
if (passwordInput) { NetworkService.connect(passwordSsid, passwordInput)
NetworkService.submitPassword(passwordPromptSsid, passwordInput) passwordSsid = ""
showPasswordPrompt = false passwordInput = ""
expandedNetwork = ""
}
} }
} }
NIconButton { NIconButton {
icon: "close" icon: "close"
tooltipText: "Cancel" sizeRatio: 0.8
sizeRatio: 0.9
onClicked: { onClicked: {
showPasswordPrompt = false passwordSsid = ""
expandedNetwork = ""
passwordInput = "" passwordInput = ""
} }
} }
} }
} }
// Forget network option - appears when saved badge is clicked // Forget network
RowLayout { Rectangle {
visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
&& !showPasswordPrompt && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Style.marginXS * scaling height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
spacing: Style.marginS * scaling color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mOutline
Item { RowLayout {
Layout.fillWidth: true id: forgetRow
} anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginM * scaling
NButton { RowLayout {
id: forgetButton NIcon {
text: "Forget Network" text: "delete_outline"
icon: "delete_outline" font.pointSize: Style.fontSizeL * scaling
fontSize: Style.fontSizeXXS * scaling color: Color.mError
backgroundColor: Color.mError }
textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary
outlined: !forgetButton.hovered NText {
Layout.preferredHeight: 28 * scaling text: "Forget this network?"
onClicked: { font.pointSize: Style.fontSizeS * scaling
NetworkService.forgetNetwork(modelData.ssid) color: Color.mError
expandedNetwork = "" 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 // Empty state when no networks
ColumnLayout { ColumnLayout {
Layout.fillWidth: true visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter NetworkService.networks).length === 0
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys( anchors.fill: parent
NetworkService.networks).length === 0 spacing: Style.marginL * scaling
spacing: Style.marginM * scaling
NIcon { Item {
text: "wifi_find" Layout.fillHeight: true
font.pointSize: Style.fontSizeXXXL * scaling }
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText { NIcon {
text: "No networks found" text: "wifi_find"
font.pointSize: Style.fontSizeL * scaling font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
NButton { NText {
text: "Refresh" text: "No networks found"
icon: "refresh" font.pointSize: Style.fontSizeL * scaling
Layout.alignment: Qt.AlignHCenter color: Color.mOnSurfaceVariant
onClicked: NetworkService.refreshNetworks() 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 Settings Window | `qs -c noctalia-shell ipc call settings toggle` |
| Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen 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 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` | | Change Wallpaper | `qs -c noctalia-shell ipc call wallpaper set $path $monitor` |
| Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` | | Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` |
| Toggle Dark Mode | `qs -c noctalia-shell ipc call darkMode toggle` | | 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: For Niri:
``` ```
debug {
honor-xdg-activation-with-invalid-serial
}
window-rule { window-rule {
geometry-corner-radius 20 geometry-corner-radius 20
clip-to-geometry true clip-to-geometry true
@ -279,6 +284,8 @@ layer-rule {
place-within-backdrop true 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, "PowerToggle": powerToggleComponent,
"ScreenRecorderIndicator": screenRecorderIndicatorComponent, "ScreenRecorderIndicator": screenRecorderIndicatorComponent,
"SidePanelToggle": sidePanelToggleComponent, "SidePanelToggle": sidePanelToggleComponent,
"Spacer": spacerComponent,
"SystemMonitor": systemMonitorComponent, "SystemMonitor": systemMonitorComponent,
"Taskbar": taskbarComponent, "Taskbar": taskbarComponent,
"Tray": trayComponent, "Tray": trayComponent,
@ -43,6 +44,11 @@ Singleton {
"leftClickExec": "", "leftClickExec": "",
"rightClickExec": "", "rightClickExec": "",
"middleClickExec": "" "middleClickExec": ""
},
"Spacer": {
"allowUserSettings": true,
"icon": "space_bar",
"width": 20
} }
}) })
@ -101,6 +107,9 @@ Singleton {
property Component sidePanelToggleComponent: Component { property Component sidePanelToggleComponent: Component {
SidePanelToggle {} SidePanelToggle {}
} }
property Component spacerComponent: Component {
Spacer {}
}
property Component systemMonitorComponent: Component { property Component systemMonitorComponent: Component {
SystemMonitor {} SystemMonitor {}
} }

View file

@ -8,215 +8,239 @@ import qs.Commons
Singleton { Singleton {
id: root id: root
// Core properties // Core state
property var networks: ({}) property var networks: ({})
property string connectingSsid: "" property bool scanning: false
property string connectStatus: "" property bool connecting: false
property string connectStatusSsid: "" property string connectingTo: ""
property string connectError: "" property string lastError: ""
property bool isLoading: false property bool ethernetConnected: false
property bool ethernet: false property string disconnectingFrom: ""
property int retryCount: 0 property string forgettingNetwork: ""
property int maxRetries: 3
// File path for persistent storage // Persistent cache
property string cacheFile: Settings.cacheDir + "network.json" property string cacheFile: Settings.cacheDir + "network.json"
readonly property string cachedLastConnected: cacheAdapter.lastConnected
readonly property var cachedNetworks: cacheAdapter.knownNetworks
// Stable properties for UI // Cache file handling
readonly property alias cache: adapter
readonly property string lastConnectedNetwork: adapter.lastConnected
// File-based persistent storage
FileView { FileView {
id: cacheFileView id: cacheFileView
path: root.cacheFile 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 { JsonAdapter {
id: adapter id: cacheAdapter
property var knownNetworks: ({}) property var knownNetworks: ({})
property string lastConnected: "" 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 { Timer {
id: saveTimer id: saveDebounce
running: false
interval: 1000 interval: 1000
onTriggered: cacheFileView.writeAdapter() onTriggered: cacheFileView.writeAdapter()
} }
Component.onCompleted: { function saveCache() {
Logger.log("Network", "Service started") 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) { if (Settings.data.network.wifiEnabled) {
refreshNetworks() scan()
} }
} }
// Signal strength icon mapping function scan() {
function signalIcon(signal) { if (scanning)
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)
return return
isLoading = true scanning = true
retryCount = 0 lastError = ""
adapter.lastRefresh = Date.now() scanProcess.running = true
performRefresh() Logger.log("Network", "Wi-Fi scan in progress...")
} }
function performRefresh() { function connect(ssid, password = "") {
checkEthernet.running = true if (connecting)
existingNetworkProcess.running = true return
}
// Retry mechanism for failed operations connecting = true
function retryRefresh() { connectingTo = ssid
if (retryCount < maxRetries) { lastError = ""
retryCount++
Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`) // Check if we have a saved connection
retryTimer.start() if (networks[ssid]?.existing || cachedNetworks[ssid]) {
connectProcess.mode = "saved"
connectProcess.ssid = ssid
connectProcess.password = ""
} else { } else {
isLoading = false connectProcess.mode = "new"
connectError = "Failed to refresh networks after multiple attempts" connectProcess.ssid = ssid
connectProcess.password = password
} }
connectProcess.running = true
} }
Timer { function disconnect(ssid) {
id: retryTimer disconnectingFrom = ssid
interval: 1000 * retryCount // Progressive backoff disconnectProcess.ssid = ssid
repeat: false disconnectProcess.running = true
onTriggered: performRefresh()
} }
Timer { function forget(ssid) {
id: autoConnectTimer forgettingNetwork = ssid
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}`)
// Remove from cache // Remove from cache
let known = adapter.knownNetworks let known = cacheAdapter.knownNetworks
delete known[ssid] delete known[ssid]
adapter.knownNetworks = known cacheAdapter.knownNetworks = known
// Clear last connected if it's this network if (cacheAdapter.lastConnected === ssid) {
if (adapter.lastConnected === ssid) { cacheAdapter.lastConnected = ""
adapter.lastConnected = ""
} }
// Save changes saveCache()
saveTimer.restart()
// Remove NetworkManager profile // Remove from system
forgetProcess.ssid = ssid forgetProcess.ssid = ssid
forgetProcess.running = true 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 { Process {
id: forgetProcess id: ethernetStateProcess
property string ssid: ""
running: false running: false
command: ["nmcli", "connection", "delete", "id", ssid] command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`) const connected = text.split("\n").some(line => {
refreshNetworks() const parts = line.split(":")
} return parts[1] === "ethernet" && parts[2] === "connected"
} })
if (root.ethernetConnected !== connected) {
stderr: StdioCollector { root.ethernetConnected = connected
onStreamFinished: { Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
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()
} }
} }
} }
} }
// 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 { 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" property string action: "on"
running: false running: false
command: ["nmcli", "radio", "wifi", action] command: ["nmcli", "radio", "wifi", action]
@ -224,10 +248,12 @@ Singleton {
onRunningChanged: { onRunningChanged: {
if (!running) { if (!running) {
if (action === "on") { if (action === "on") {
wifiEnableTimer.start() // Clear networks immediately and start delayed scan
root.networks = ({})
delayedScanTimer.interval = 8000
delayedScanTimer.restart()
} else { } else {
root.networks = ({}) root.networks = ({})
root.isLoading = false
} }
} }
} }
@ -235,137 +261,177 @@ Singleton {
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim()) { if (text.trim()) {
Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`) Logger.warn("Network", "WiFi toggle error: " + text)
} }
} }
} }
} }
Timer { Process {
id: wifiEnableTimer id: scanProcess
interval: 2000 running: false
repeat: false command: ["sh", "-c", `
onTriggered: { # Get list of saved connection profiles (just the names)
refreshNetworks() profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
if (adapter.lastConnected) {
reconnectTimer.start() # 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 { Process {
id: connectProcess id: connectProcess
property string mode: "new"
property string ssid: "" property string ssid: ""
property string password: "" property string password: ""
property bool isSecured: false
running: false running: false
command: { command: {
const cmd = ["nmcli", "device", "wifi", "connect", ssid] if (mode === "saved") {
if (isSecured && password) { return ["nmcli", "connection", "up", "id", ssid]
cmd.push("password", password) } else {
} const cmd = ["nmcli", "device", "wifi", "connect", ssid]
return cmd if (password) {
} cmd.push("password", password)
stdout: StdioCollector {
onStreamFinished: {
handleConnectionSuccess(connectProcess.ssid)
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
handleConnectionError(connectProcess.ssid, text)
} }
return cmd
} }
} }
}
Process {
id: upConnectionProcess
property string profileName: ""
running: false
command: ["nmcli", "connection", "up", "id", profileName]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { 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 { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
root.connecting = false
root.connectingTo = ""
if (text.trim()) { 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 running: false
command: ["nmcli", "connection", "down", "id", ssid] command: ["nmcli", "connection", "down", "id", ssid]
onRunningChanged: { stdout: StdioCollector {
if (!running) { onStreamFinished: {
connectingSsid = "" Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`)
connectStatus = ""
connectStatusSsid = "" // Immediately update UI on successful disconnect
connectError = "" root.updateNetworkStatus(disconnectProcess.ssid, false)
refreshNetworks() root.disconnectingFrom = ""
// Do a scan to refresh the list
delayedScanTimer.interval = 1000
delayedScanTimer.restart()
} }
} }
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
root.disconnectingFrom = ""
if (text.trim()) { 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 { Process {
id: existingNetworkProcess id: forgetProcess
property string ssid: ""
running: false 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 { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const profiles = {} Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`)
const lines = text.split("\n").filter(l => l.trim()) Logger.log("Network", text.trim().replace(/[\r\n]/g, " "))
for (const line of lines) { // Update both cached and existing status immediately
const parts = line.split(":") let nets = root.networks
const name = parts[0] if (nets[forgetProcess.ssid]) {
const type = parts[1] nets[forgetProcess.ssid].cached = false
if (name && type === "802-11-wireless") { nets[forgetProcess.ssid].existing = false
profiles[name] = { // Trigger property change
"ssid": name, root.networks = ({})
"type": type root.networks = nets
}
}
} }
scanProcess.existingProfiles = profiles root.forgettingNetwork = ""
scanProcess.running = true
// Quick scan to verify the profile is gone
delayedScanTimer.interval = 500
delayedScanTimer.restart()
} }
} }
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim()) { root.forgettingNetwork = ""
Logger.warn("Network", "Error listing connections:", text) if (text.trim() && !text.includes("No profiles found")) {
retryRefresh() 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 // Signal when notification is received
onNotification: function (notification) { onNotification: function (notification) {
// Always add notification to history
root.addToHistory(notification)
// Check if notifications are suppressed // Check if do-not-disturb is enabled
if (Settings.data.notifications && Settings.data.notifications.suppressed) { if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) {
// Still add to history but don't show notification
root.addToHistory(notification)
return return
} }
@ -46,8 +46,6 @@ Singleton {
// Add to our model // Add to our model
root.addNotification(notification) 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 to add notification to model
function addNotification(notification) { function addNotification(notification) {
notificationModel.insert(0, { notificationModel.insert(0, {

View file

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

View file

@ -165,13 +165,21 @@ Singleton {
"timestamp": Date.now() "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 // Add to queue
messageQueue.push(toastData) messageQueue.push(toastData)
// Process queue if not currently showing a toast // Always process immediately for instant display
if (!isShowingToast) { processQueue()
processQueue()
}
} }
// Process the message queue // Process the message queue
@ -181,11 +189,6 @@ Singleton {
return return
} }
if (isShowingToast) {
// Wait for current toast to finish
return
}
var toastData = messageQueue.shift() var toastData = messageQueue.shift()
isShowingToast = true isShowingToast = true

View file

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

View file

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

View file

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

View file

@ -8,39 +8,34 @@ Rectangle {
id: root id: root
property color selectedColor: "#000000" property color selectedColor: "#000000"
property bool expanded: false
signal colorSelected(color color) signal colorSelected(color color)
signal colorCancelled
implicitWidth: expanded ? 320 * scaling : 150 * scaling implicitWidth: 150 * scaling
implicitHeight: expanded ? 300 * scaling : 40 * scaling implicitHeight: 40 * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: Color.mSurface color: Color.mSurface
border.color: Color.mOutline border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) 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"] // Minimized Look
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Style.animationFast
}
}
// Collapsed view - just show current color
MouseArea { MouseArea {
visible: !root.expanded
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor 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 { RowLayout {
anchors.fill: parent 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 string tooltipText
property bool enabled: true property bool enabled: true
property bool hovering: false property bool hovering: false
property real fontPointSize: Style.fontSizeM
property color colorBg: Color.mSurfaceVariant property color colorBg: Color.mSurfaceVariant
property color colorFg: Color.mPrimary property color colorFg: Color.mPrimary
@ -41,7 +40,7 @@ Rectangle {
NIcon { NIcon {
text: root.icon text: root.icon
font.pointSize: root.fontPointSize * scaling font.pointSize: Style.fontSizeM * scaling
color: root.hovering ? colorFgHover : colorFg color: root.hovering ? colorFgHover : colorFg
// Center horizontally // Center horizontally
x: (root.width - width) / 2 x: (root.width - width) / 2

View file

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

View file

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

View file

@ -11,7 +11,6 @@ ColumnLayout {
property string description: "" property string description: ""
property bool readOnly: false property bool readOnly: false
property bool enabled: true property bool enabled: true
property int inputMaxWidth: Math.round(420 * scaling)
property color labelColor: Color.mOnSurface property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant property color descriptionColor: Color.mOnSurfaceVariant
property string fontFamily: Settings.data.ui.fontDefault property string fontFamily: Settings.data.ui.fontDefault
@ -26,7 +25,6 @@ ColumnLayout {
signal editingFinished signal editingFinished
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
implicitHeight: frame.height
NLabel { NLabel {
label: root.label label: root.label
@ -34,6 +32,7 @@ ColumnLayout {
labelColor: root.labelColor labelColor: root.labelColor
descriptionColor: root.descriptionColor descriptionColor: root.descriptionColor
visible: root.label !== "" || root.description !== "" visible: root.label !== "" || root.description !== ""
Layout.fillWidth: true
} }
// Container // Container
@ -42,50 +41,48 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 80 * scaling Layout.minimumWidth: 80 * scaling
Layout.maximumWidth: root.inputMaxWidth
implicitWidth: parent.width
implicitHeight: Style.baseWidgetSize * 1.1 * scaling implicitHeight: Style.baseWidgetSize * 1.1 * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: Color.mSurface color: Color.mSurface
border.color: Color.mOutline border.color: input.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
// Focus ring Behavior on border.color {
Rectangle { ColorAnimation {
anchors.fill: parent duration: Style.animationFast
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
}
} }
} }
RowLayout { TextField {
id: input
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Style.marginM * scaling anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling anchors.rightMargin: Style.marginM * scaling
spacing: Style.marginS * scaling
TextField { verticalAlignment: TextInput.AlignVCenter
id: input
Layout.fillWidth: true echoMode: TextInput.Normal
echoMode: TextInput.Normal readOnly: root.readOnly
readOnly: root.readOnly enabled: root.enabled
enabled: root.enabled color: Color.mOnSurface
color: Color.mOnSurface placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
background: null selectByMouse: true
font.family: fontFamily
font.pointSize: fontSize topPadding: 0
font.weight: fontWeight bottomPadding: 0
onEditingFinished: root.editingFinished() 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. // NToast updates its scaling when showing.
scaling = ScalingService.getScreenScale(screen) 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 visible = true
// Start the show animation
showAnimation.start() showAnimation.start()
if (duration > 0 && !persistent) { if (duration > 0 && !persistent) {
autoHideTimer.start() autoHideTimer.start()
@ -81,7 +90,6 @@ Item {
// Main toast container // Main toast container
Rectangle { Rectangle {
id: container
anchors.fill: parent anchors.fill: parent
radius: Style.radiusL * scaling radius: Style.radiusL * scaling
@ -137,43 +145,41 @@ Item {
} }
// Label and description // Label and description
Column { ColumnLayout {
id: textColumn
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
NText { NText {
id: labelText Layout.fillWidth: true
text: root.label text: root.label
color: Color.mOnSurface color: Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width
visible: text.length > 0 visible: text.length > 0
} }
NText { NText {
id: descriptionText Layout.fillWidth: true
text: root.description text: root.description
color: Color.mOnSurface color: Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width
visible: text.length > 0 visible: text.length > 0
} }
} }
// Close button (only if persistent or manual dismiss needed) // Close button (only if persistent or manual dismiss needed)
NIconButton { NIconButton {
id: closeButton
icon: "close" icon: "close"
visible: root.persistent || root.duration === 0 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 sizeRatio: 0.8
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop