Compare commits

..

No commits in common. "13111922357f13ef86e43d5b3867ba520d1c594e" and "a4a19f942c04c9d474233604b49d9fdbcc357c4f" have entirely different histories.

59 changed files with 2160 additions and 2923 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: "send a message" !important; content: "Message #general" !important;
color: {{colors.on_surface_variant.default.hex}} !important; color: {{colors.on_surface_variant.default.hex}} !important;
} }

View file

@ -278,14 +278,12 @@ 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: []
} }
@ -297,7 +295,6 @@ 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,7 +29,6 @@ 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,34 +78,23 @@ 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, computer uptime, etc.. // Used to display the time remaining on the Battery widget
function formatVagueHumanReadableDuration(totalSeconds) { function formatVagueHumanReadableDuration(totalSeconds) {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) { const hours = Math.floor(totalSeconds / 3600)
return '0s' const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
var str = ""
if (hours) {
str += hours.toString() + "h"
}
if (minutes) {
str += minutes.toString() + "m"
} }
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
// Only show seconds if no hours and no minutes
if (!hours && !minutes) { if (!hours && !minutes) {
parts.push(`${seconds}s`) str += seconds.toString() + "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,45 +2,38 @@ 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
RowLayout { Row {
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 Icons.iconForAppId(focusedWindow.appId.toLowerCase()) return ""
}
// Fallback to ToplevelManager return Icons.iconForAppId(focusedWindow.appId)
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
@ -50,13 +43,15 @@ RowLayout {
} }
Rectangle { Rectangle {
id: windowTitleRect // Let the Rectangle size itself based on its content (the Row)
visible: root.visible visible: root.visible
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling width: row.width + Style.marginM * 2 * scaling
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) 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
Item { Item {
id: mainContainer id: mainContainer
anchors.fill: parent anchors.fill: parent
@ -64,16 +59,16 @@ RowLayout {
anchors.rightMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling
clip: true clip: true
RowLayout { Row {
id: contentLayout id: row
anchors.centerIn: parent anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Window icon // Window icon
Item { Item {
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 width: Style.fontSizeL * scaling * 1.2
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 height: Style.fontSizeL * scaling * 1.2
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
IconImage { IconImage {
@ -88,24 +83,26 @@ RowLayout {
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 Layout.preferredWidth { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Style.animationSlow duration: Style.animationSlow
easing.type: Easing.InOutCubic easing.type: Easing.InOutCubic
@ -123,14 +120,4 @@ RowLayout {
} }
} }
} }
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,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Io import Quickshell.Io
@ -7,7 +6,7 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
Item { Row {
id: root id: root
property ShellScreen screen property ShellScreen screen
@ -19,13 +18,12 @@ Item {
// Use the shared service for keyboard layout // Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout property string currentLayout: KeyboardLayoutService.currentLayout
implicitWidth: pill.width width: pill.width
implicitHeight: pill.height height: 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
RowLayout { Row {
id: root id: root
property ShellScreen screen property ShellScreen screen
@ -15,10 +15,10 @@ RowLayout {
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: MediaService.currentPlayer !== null && MediaService.canPlay visible: MediaService.currentPlayer !== null && MediaService.canPlay
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 width: 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,13 +35,15 @@ RowLayout {
Rectangle { Rectangle {
id: mediaMini id: mediaMini
Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling // Let the Rectangle size itself based on its content (the Row)
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) width: row.width + Style.marginM * 2 * 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
@ -59,7 +61,7 @@ RowLayout {
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.isPlaying && MediaService.trackLength > 0
z: 0 z: 0
sourceComponent: LinearSpectrum { sourceComponent: LinearSpectrum {
@ -69,42 +71,42 @@ RowLayout {
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.isPlaying && MediaService.trackLength > 0
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
}
} }
} }
Loader { Row {
anchors.verticalCenter: parent.verticalCenter id: row
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
@ -114,18 +116,17 @@ RowLayout {
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
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
} }
ColumnLayout { Column {
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
visible: Settings.data.audio.showMiniplayerAlbumArt visible: Settings.data.audio.showMiniplayerAlbumArt
spacing: 0
Item { Item {
Layout.preferredWidth: Math.round(18 * scaling) width: Math.round(18 * scaling)
Layout.preferredHeight: Math.round(18 * scaling) height: Math.round(18 * scaling)
NImageCircled { NImageCircled {
id: trackArt id: trackArt
@ -141,23 +142,23 @@ RowLayout {
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
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 Layout.preferredWidth { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Style.animationSlow duration: Style.animationSlow
easing.type: Easing.InOutCubic easing.type: Easing.InOutCubic
@ -204,10 +205,10 @@ RowLayout {
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." str += "Middle click for previous\n"
} }
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,14 +14,11 @@ NIconButton {
property real scaling: 1.0 property real scaling: 1.0
sizeRatio: 0.8 sizeRatio: 0.8
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" icon: "notifications"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." tooltipText: "Notification history"
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface colorFg: 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

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

View file

@ -2,7 +2,6 @@ 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
@ -18,14 +17,15 @@ 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: taskbarLayout.implicitWidth + Style.marginM * scaling * 2 implicitWidth: taskbarRow.width + 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
RowLayout { Row {
id: taskbarLayout id: taskbarRow
anchors.centerIn: parent anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.marginXXS * root.scaling spacing: Style.marginXXS * root.scaling
Repeater { Repeater {
@ -35,10 +35,8 @@ 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
Layout.preferredWidth: root.itemSize height: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
Rectangle { Rectangle {
id: iconBackground id: iconBackground
@ -91,7 +89,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: trayLayout.implicitWidth + Style.marginM * scaling * 2 implicitWidth: tray.width + 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
RowLayout { Row {
id: trayLayout id: tray
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 {
Layout.preferredWidth: itemSize width: itemSize
Layout.preferredHeight: itemSize height: itemSize
Layout.alignment: Qt.AlignCenter
visible: modelData visible: modelData
IconImage { IconImage {
@ -146,14 +146,13 @@ Rectangle {
function open() { function open() {
visible = true visible = true
PanelService.willOpenPanel(trayPanel) PanelService.willOpenPanel(trayPanel)
} }
function close() { function close() {
visible = false visible = false
if (trayMenu.item) { trayMenu.item.hideMenu()
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(AudioService.volume * 100) tooltipText: "Volume: " + Math.round(
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." AudioService.volume * 100) + "%\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,8 +13,18 @@ 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
@ -22,7 +32,7 @@ NIconButton {
icon: { icon: {
try { try {
if (NetworkService.ethernetConnected) { if (NetworkService.ethernet) {
return "lan" return "lan"
} }
let connected = false let connected = false
@ -36,10 +46,10 @@ NIconButton {
} }
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find" return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
} catch (error) { } catch (error) {
Logger.error("Wi-Fi", "Error getting icon:", error) Logger.error("WiFi", "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 property ShellScreen screen: null
property real scaling: 1.0 property real scaling: 1.0
property bool isDestroying: false property bool isDestroying: false

View file

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

View file

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

View file

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

View file

@ -82,11 +82,7 @@ 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
ColumnLayout { Column {
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -168,7 +168,6 @@ 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
@ -193,23 +192,22 @@ Loader {
font.weight: Font.Light font.weight: Font.Light
color: Color.mOnSurface color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter width: timeText.width
Layout.preferredWidth: timeText.implicitWidth
} }
} }
ColumnLayout { Column {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Rectangle { Rectangle {
Layout.preferredWidth: 108 * scaling width: 108 * scaling
Layout.preferredHeight: 108 * scaling height: 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 {
@ -377,371 +375,377 @@ Loader {
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: 50 * scaling anchors.verticalCenterOffset: 50 * scaling
Rectangle { Item {
id: terminalBackground width: parent.width
anchors.fill: parent height: 280 * scaling
radius: Style.radiusM * scaling Layout.fillWidth: true
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary Rectangle {
border.width: Math.max(1, Style.borderM * scaling) id: terminalBackground
anchors.fill: parent
radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
Repeater {
model: 20
Rectangle {
width: parent.width
height: 1
color: Qt.alpha(Color.mPrimary, 0.1)
y: index * 10 * scaling
opacity: Style.opacityMedium
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000 + Math.random() * 1000
}
NumberAnimation {
to: 0.1
duration: 2000 + Math.random() * 1000
}
}
}
}
Repeater {
model: 20
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 40 * scaling
color: Qt.alpha(Color.mPrimary, 0.1) color: Qt.alpha(Color.mPrimary, 0.2)
y: index * 10 * scaling topLeftRadius: Style.radiusS * scaling
opacity: Style.opacityMedium topRightRadius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.topMargin: Style.marginM * scaling
anchors.bottomMargin: Style.marginM * scaling
anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling
spacing: Style.marginM * scaling
NText {
text: "SECURE TERMINAL"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
Row {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: batteryIndicator.getIcon()
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
Row {
spacing: Style.marginS * scaling
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
NIcon {
text: "keyboard_alt"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
}
}
}
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Style.marginL * scaling
anchors.topMargin: 70 * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
id: welcomeText
text: ""
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (parent.currentIndex < parent.fullText.length) {
parent.text = parent.fullText.substring(0, parent.currentIndex + 1)
parent.currentIndex++
} else {
running = false
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
text: "sudo unlock-session"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
}
TextInput {
id: passwordInput
width: 0
height: 0
visible: false
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
echoMode: TextInput.Password
passwordCharacter: "*"
passwordMaskDelay: 0
text: lockContext.currentText
onTextChanged: {
lockContext.currentText = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lockContext.tryUnlock()
}
}
Component.onCompleted: {
forceActiveFocus()
}
}
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
}
}
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
Layout.leftMargin: -Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
}
}
}
NText {
text: {
if (lockContext.unlockInProgress)
return "Authenticating..."
if (lockContext.showFailure && lockContext.errorMessage)
return lockContext.errorMessage
if (lockContext.showFailure)
return "Authentication failed."
return ""
}
color: {
if (lockContext.unlockInProgress)
return Color.mPrimary
if (lockContext.showFailure)
return Color.mError
return Color.transparent
}
font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeL * scaling
Layout.fillWidth: true
SequentialAnimation on opacity {
running: lockContext.unlockInProgress
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 800
}
NumberAnimation {
to: 0.5
duration: 800
}
}
}
Row {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Rectangle {
width: 120 * scaling
height: 40 * scaling
radius: Style.radiusS * scaling
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
enabled: !lockContext.unlockInProgress
NText {
anchors.centerIn: parent
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
MouseArea {
id: executeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
lockContext.tryUnlock()
}
SequentialAnimation on scale {
running: executeButtonArea.containsMouse
NumberAnimation {
to: 1.05
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
SequentialAnimation on scale {
running: !executeButtonArea.containsMouse
NumberAnimation {
to: 1.0
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: lockContext.unlockInProgress
NumberAnimation {
to: 1.02
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
}
}
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: Qt.alpha(Color.mPrimary, 0.3)
border.width: Math.max(1, Style.borderS * scaling)
z: -1
SequentialAnimation on opacity { SequentialAnimation on opacity {
loops: Animation.Infinite loops: Animation.Infinite
NumberAnimation { NumberAnimation {
to: 0.6 to: 0.6
duration: 2000 + Math.random() * 1000 duration: 2000
easing.type: Easing.InOutQuad
} }
NumberAnimation { NumberAnimation {
to: 0.1 to: 0.2
duration: 2000 + Math.random() * 1000 duration: 2000
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 right // Power buttons at bottom
RowLayout { Row {
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 {
Layout.preferredWidth: 60 * scaling width: 60 * scaling
Layout.preferredHeight: 60 * scaling height: 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
@ -765,8 +769,8 @@ Loader {
} }
Rectangle { Rectangle {
Layout.preferredWidth: 60 * scaling width: 60 * scaling
Layout.preferredHeight: 60 * scaling height: 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
@ -790,8 +794,8 @@ Loader {
} }
Rectangle { Rectangle {
Layout.preferredWidth: 60 * scaling width: 60 * scaling
Layout.preferredHeight: 60 * scaling height: 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
ColumnLayout { Column {
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,9 +92,8 @@ Variants {
Repeater { Repeater {
model: notificationModel model: notificationModel
delegate: Rectangle { delegate: Rectangle {
Layout.preferredWidth: 360 * scaling width: 360 * scaling
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling) height: Math.max(80 * scaling, contentRow.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
@ -106,17 +105,6 @@ 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
@ -168,139 +156,104 @@ Variants {
} }
} }
ColumnLayout { RowLayout {
id: notificationLayout id: contentRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginL * scaling
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button spacing: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header section with app name and timestamp // Right: header on top, then avatar + texts
RowLayout { ColumnLayout {
Layout.fillWidth: true id: textColumn
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NText {
text: `${(model.appName || model.desktopEntry)
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Rectangle {
Layout.preferredWidth: 6 * scaling
Layout.preferredHeight: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
}
// Main content section
RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling
// Avatar RowLayout {
NImageCircled {
id: appAvatar
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
imagePath: model.image && model.image !== "" ? model.image : ""
fallbackIcon: ""
borderColor: Color.transparent
borderWidth: 0
visible: (model.image && model.image !== "")
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
id: appHeaderRow
NText { NText {
text: model.summary || "No summary" text: `${(model.appName || model.desktopEntry)
font.pointSize: Style.fontSizeL * scaling || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
font.weight: Style.fontWeightMedium color: Color.mSecondary
color: Color.mOnSurface font.pointSize: Style.fontSizeXS * scaling
wrapMode: Text.WrapAtWordBoundaryOrAnywhere }
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 Layout.fillWidth: true
maximumLineCount: 3 }
elide: Text.ElideRight }
RowLayout {
id: bodyRow
spacing: Style.marginM * scaling
NImageCircled {
id: appAvatar
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
// Start avatar aligned with body (below the summary)
anchors.topMargin: textContent.childrenRect.y
// Prefer notification-provided image (e.g., user avatar) then fall back to app icon
imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName(
model.appIcon, "application-x-executable")
fallbackIcon: "apps"
borderColor: Color.transparent
borderWidth: 0
visible: (imagePath && imagePath !== "")
} }
NText { Column {
text: model.body || "" id: textContent
font.pointSize: Style.fontSizeM * scaling spacing: Style.marginS * scaling
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true Layout.fillWidth: true
maximumLineCount: 5 // Ensure a concrete width so text wraps
elide: Text.ElideRight width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0))
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
}
} }
} }
} }
// Notification actions // Actions removed
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"
sizeRatio: 0.6 // Compact target (~24dp) and glyph (~16dp)
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.rightMargin: Style.marginM * scaling anchors.margins: Style.marginS * scaling
onClicked: { onClicked: {
animateOut() animateOut()

View file

@ -25,7 +25,6 @@ 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
@ -44,13 +43,6 @@ 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"
@ -73,44 +65,38 @@ NPanel {
} }
// Empty state when no notifications // Empty state when no notifications
ColumnLayout { Item {
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
Item { ColumnLayout {
Layout.fillHeight: true anchors.centerIn: parent
} spacing: Style.marginM * scaling
NIcon { NIcon {
text: "notifications_off" text: "notifications_off"
font.pointSize: 64 * scaling font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurface
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.mOnSurfaceVariant color: Color.mOnSurface
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.fontSizeS * scaling font.pointSize: Style.fontSizeNormal * 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
@ -122,21 +108,21 @@ NPanel {
visible: NotificationService.historyModel.count > 0 visible: NotificationService.historyModel.count > 0
delegate: Rectangle { delegate: Rectangle {
width: notificationList.width width: notificationList ? notificationList.width : 380 * scaling
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2) height: Math.max(80, notificationContent.height + 30)
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 {
id: notificationLayout anchors {
anchors.fill: parent fill: parent
anchors.margins: Style.marginM * scaling margins: Style.marginM * scaling
}
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Notification content column // Notification content
ColumnLayout { Column {
id: notificationContent
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
@ -147,8 +133,7 @@ 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
Layout.fillWidth: true width: parent.width - 60
Layout.maximumWidth: parent.width
maximumLineCount: 2 maximumLineCount: 2
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -158,27 +143,23 @@ 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
Layout.fillWidth: true width: parent.width - 60
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
} }
} }
// Delete button // Trash icon 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)
@ -191,7 +172,7 @@ NPanel {
MouseArea { MouseArea {
id: notificationMouseArea id: notificationMouseArea
anchors.fill: parent anchors.fill: parent
anchors.rightMargin: Style.marginXL * scaling anchors.rightMargin: Style.marginL * 3 * scaling
hoverEnabled: true hoverEnabled: true
} }
} }

View file

@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
@ -17,7 +16,6 @@ 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
@ -25,44 +23,9 @@ NPanel {
property bool timerActive: false property bool timerActive: false
property int timeRemaining: 0 property int timeRemaining: 0
// Navigation properties // Cancel timer when panel is closing
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
@ -116,38 +79,6 @@ 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
@ -162,92 +93,8 @@ 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
@ -297,21 +144,55 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Repeater { // Lock Screen
model: powerOptions PowerButton {
delegate: PowerButton { Layout.fillWidth: true
Layout.fillWidth: true icon: "lock_outline"
icon: modelData.icon title: "Lock"
title: modelData.title subtitle: "Lock your session"
subtitle: modelData.subtitle onClicked: startTimer("lock")
isShutdown: modelData.isShutdown || false pending: timerActive && pendingAction === "lock"
isSelected: index === selectedIndex }
onClicked: {
selectedIndex = index // Suspend
startTimer(modelData.action) PowerButton {
} Layout.fillWidth: true
pending: timerActive && pendingAction === modelData.action icon: "bedtime"
} title: "Suspend"
subtitle: "Put the system to sleep"
onClicked: startTimer("suspend")
pending: timerActive && pendingAction === "suspend"
}
// Reboot
PowerButton {
Layout.fillWidth: true
icon: "refresh"
title: "Reboot"
subtitle: "Restart the system"
onClicked: startTimer("reboot")
pending: timerActive && pendingAction === "reboot"
}
// Logout
PowerButton {
Layout.fillWidth: true
icon: "exit_to_app"
title: "Logout"
subtitle: "End your session"
onClicked: startTimer("logout")
pending: timerActive && pendingAction === "logout"
}
// Shutdown
PowerButton {
Layout.fillWidth: true
icon: "power_settings_new"
title: "Shutdown"
subtitle: "Turn off the system"
onClicked: startTimer("shutdown")
pending: timerActive && pendingAction === "shutdown"
isShutdown: true
} }
} }
} }
@ -326,7 +207,6 @@ 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
@ -336,7 +216,7 @@ NPanel {
if (pending) { if (pending) {
return Qt.alpha(Color.mPrimary, 0.08) return Qt.alpha(Color.mPrimary, 0.08)
} }
if (isSelected || mouseArea.containsMouse) { if (mouseArea.containsMouse) {
return Color.mSecondary return Color.mSecondary
} }
return Color.transparent return Color.transparent
@ -362,12 +242,13 @@ 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 && !buttonRoot.isSelected && !mouseArea.containsMouse) if (buttonRoot.isShutdown && !mouseArea.containsMouse)
return Color.mError return Color.mError
if (buttonRoot.isSelected || mouseArea.containsMouse) if (mouseArea.containsMouse)
return Color.mOnSecondary return Color.mOnTertiary
return Color.mOnSurface return Color.mOnSurface
} }
font.pointSize: Style.fontSizeXXXL * scaling font.pointSize: Style.fontSizeXXXL * scaling
@ -383,7 +264,7 @@ NPanel {
} }
// Text content in the middle // Text content in the middle
ColumnLayout { Column {
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
@ -398,10 +279,10 @@ NPanel {
color: { color: {
if (buttonRoot.pending) if (buttonRoot.pending)
return Color.mPrimary return Color.mPrimary
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) if (buttonRoot.isShutdown && !mouseArea.containsMouse)
return Color.mError return Color.mError
if (buttonRoot.isSelected || mouseArea.containsMouse) if (mouseArea.containsMouse)
return Color.mOnSecondary return Color.mOnTertiary
return Color.mOnSurface return Color.mOnSurface
} }
@ -423,10 +304,10 @@ NPanel {
color: { color: {
if (buttonRoot.pending) if (buttonRoot.pending)
return Color.mPrimary return Color.mPrimary
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) if (buttonRoot.isShutdown && !mouseArea.containsMouse)
return Color.mError return Color.mError
if (buttonRoot.isSelected || mouseArea.containsMouse) if (mouseArea.containsMouse)
return Color.mOnSecondary return Color.mOnTertiary
return Color.mOnSurfaceVariant return Color.mOnSurfaceVariant
} }
opacity: Style.opacityHeavy opacity: Style.opacityHeavy

View file

@ -68,8 +68,6 @@ 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
@ -159,28 +157,4 @@ 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,269 +267,234 @@ NPanel {
} }
panelContent: Rectangle { panelContent: Rectangle {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
color: Color.transparent color: Color.transparent
// Main layout container that fills the panel // Scrolling via keyboard
ColumnLayout { Shortcut {
sequence: "Down"
onActivated: root.scrollDown()
enabled: root.opened
}
Shortcut {
sequence: "Up"
onActivated: root.scrollUp()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
onActivated: root.scrollDown()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+K"
onActivated: root.scrollUp()
enabled: root.opened
}
Shortcut {
sequence: "PgDown"
onActivated: root.scrollPageDown()
enabled: root.opened
}
Shortcut {
sequence: "PgUp"
onActivated: root.scrollPageUp()
enabled: root.opened
}
// Changing tab via keyboard
Shortcut {
sequence: "Tab"
onActivated: root.selectNextTab()
enabled: root.opened
}
Shortcut {
sequence: "Shift+Tab"
onActivated: root.selectPreviousTab()
enabled: root.opened
}
RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling
spacing: 0
// Keyboard shortcuts container Rectangle {
Item { id: sidebar
Layout.preferredWidth: 0 Layout.preferredWidth: 220 * scaling
Layout.preferredHeight: 0 Layout.fillHeight: true
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
// Scrolling via keyboard MouseArea {
Shortcut { anchors.fill: parent
sequence: "Down" acceptedButtons: Qt.NoButton // Don't interfere with clicks
onActivated: root.scrollDown() property int wheelAccumulator: 0
enabled: root.opened onWheel: wheel => {
wheelAccumulator += wheel.angleDelta.y
if (wheelAccumulator >= 120) {
root.selectPreviousTab()
wheelAccumulator = 0
} else if (wheelAccumulator <= -120) {
root.selectNextTab()
wheelAccumulator = 0
}
wheel.accepted = true
}
} }
Shortcut { Column {
sequence: "Up" anchors.fill: parent
onActivated: root.scrollUp() anchors.margins: Style.marginS * scaling
enabled: root.opened spacing: Style.marginXS * 1.5 * scaling
}
Shortcut { Repeater {
sequence: "Ctrl+J" id: sections
onActivated: root.scrollDown() model: root.tabsModel
enabled: root.opened delegate: Rectangle {
} id: tabItem
width: parent.width
height: 32 * scaling
radius: Style.radiusS * scaling
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
Shortcut { Behavior on color {
sequence: "Ctrl+K" ColorAnimation {
onActivated: root.scrollUp() duration: Style.animationFast
enabled: root.opened }
} }
Shortcut { Behavior on tabTextColor {
sequence: "PgDown" ColorAnimation {
onActivated: root.scrollPageDown() duration: Style.animationFast
enabled: root.opened }
} }
Shortcut { RowLayout {
sequence: "PgUp" anchors.fill: parent
onActivated: root.scrollPageUp() anchors.leftMargin: Style.marginS * scaling
enabled: root.opened anchors.rightMargin: Style.marginS * scaling
} spacing: Style.marginS * scaling
// Tab icon on the left side
// Changing tab via keyboard NIcon {
Shortcut { text: modelData.icon
sequence: "Tab" color: tabTextColor
onActivated: root.selectNextTab() font.pointSize: Style.fontSizeL * scaling
enabled: root.opened }
} // Tab label on the left side
NText {
Shortcut { text: modelData.label
sequence: "Shift+Tab" color: tabTextColor
onActivated: root.selectPreviousTab() font.pointSize: Style.fontSizeM * scaling
enabled: root.opened font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
}
}
} }
} }
// Main content area // Content
RowLayout { Rectangle {
id: contentPane
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
spacing: Style.marginM * scaling radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
// Sidebar ColumnLayout {
Rectangle { id: contentLayout
id: sidebar anchors.fill: parent
Layout.preferredWidth: 220 * scaling anchors.margins: Style.marginL * scaling
Layout.fillHeight: true spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignTop
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
MouseArea { RowLayout {
anchors.fill: parent id: headerRow
acceptedButtons: Qt.NoButton // Don't interfere with clicks Layout.fillWidth: true
property int wheelAccumulator: 0
onWheel: wheel => {
wheelAccumulator += wheel.angleDelta.y
if (wheelAccumulator >= 120) {
root.selectPreviousTab()
wheelAccumulator = 0
} else if (wheelAccumulator <= -120) {
root.selectNextTab()
wheelAccumulator = 0
}
wheel.accepted = true
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling
Repeater {
id: sections
model: root.tabsModel
delegate: Rectangle {
id: tabItem
Layout.fillWidth: true
Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginS * scaling * 2
radius: Style.radiusS * scaling
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
id: tabEntryRow
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
// Tab icon
NIcon {
text: modelData.icon
color: tabTextColor
font.pointSize: Style.fontSizeL * scaling
}
// Tab label
NText {
text: modelData.label
color: tabTextColor
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
}
}
Item {
Layout.fillHeight: true
}
}
}
// Content pane
Rectangle {
id: contentPane
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Header row // Tab label on the main right side
RowLayout { NText {
id: headerRow text: root.tabsModel[currentTabIndex].label
Layout.fillWidth: true font.pointSize: Style.fontSizeXL * scaling
spacing: Style.marginS * scaling font.weight: Style.fontWeightBold
color: Color.mPrimary
// 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 Layout.fillWidth: true
} }
NIconButton {
icon: "close"
tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}
}
// Tab content area NDivider {
Rectangle { Layout.fillWidth: true
Layout.fillWidth: true }
Layout.fillHeight: true
color: Color.transparent
Repeater { Item {
model: root.tabsModel Layout.fillWidth: true
delegate: Loader { Layout.fillHeight: true
anchors.fill: parent
active: index === root.currentTabIndex
onStatusChanged: { Repeater {
if (status === Loader.Ready && item) { model: root.tabsModel
// Find and store reference to the ScrollView delegate: Loader {
const scrollView = item.children[0] anchors.fill: parent
if (scrollView && scrollView.toString().includes("ScrollView")) { active: index === root.currentTabIndex
root.activeScrollView = scrollView
} 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
} }
} }
}
sourceComponent: Flickable { sourceComponent: ColumnLayout {
// Using a Flickable here with a pressDelay to fix conflict between ScrollView {
// ScrollView and NTextInput. This fixes the weird text selection issue. id: scrollView
id: flickable Layout.fillWidth: true
anchors.fill: parent Layout.fillHeight: true
pressDelay: 200 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
ScrollView { Component.onCompleted: {
id: scrollView root.activeScrollView = scrollView
anchors.fill: parent }
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
Component.onCompleted: { Loader {
root.activeScrollView = scrollView active: true
} sourceComponent: root.tabsModel[index].source
width: scrollView.availableWidth
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: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2)) Layout.preferredWidth: updateText.implicitWidth + 46 * scaling
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,12 +85,11 @@ ColumnLayout {
} }
RowLayout { RowLayout {
id: updateRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIcon { NIcon {
text: "download" text: "system_update"
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,11 +22,9 @@ 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
@ -77,45 +75,6 @@ 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,85 +5,94 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
ColumnLayout { ScrollView {
id: contentColumn id: root
spacing: Style.marginL * scaling
width: root.width
// Enable/Disable Toggle property real scaling: 1.0
NToggle {
label: "Enable Hooks" contentWidth: contentColumn.width
description: "Enable or disable all hook commands." contentHeight: contentColumn.height
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked
}
ColumnLayout { ColumnLayout {
visible: Settings.data.hooks.enabled id: contentColumn
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
Layout.fillWidth: true width: root.width
NDivider { // Enable/Disable Toggle
Layout.fillWidth: true NToggle {
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 {
spacing: Style.marginM * scaling visible: Settings.data.hooks.enabled
spacing: Style.marginL * scaling
Layout.fillWidth: true Layout.fillWidth: true
NLabel { NDivider {
label: "Hook Command Information" Layout.fillWidth: true
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
} }
NLabel { // Wallpaper Hook Section
label: "Available Parameters" NInputAction {
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)" id: wallpaperHookInput
label: "Wallpaper Change Hook"
description: "Command to be executed when wallpaper changes."
placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\""
text: Settings.data.hooks.wallpaperChange
onEditingFinished: {
Settings.data.hooks.wallpaperChange = wallpaperHookInput.text
}
onActionClicked: {
if (wallpaperHookInput.text) {
HooksService.executeWallpaperHook("test", "test-screen")
}
}
Layout.fillWidth: true
}
NDivider {
Layout.fillWidth: true
}
// Dark Mode Hook Section
NInputAction {
id: darkModeHookInput
label: "Theme Toggle Hook"
description: "Command to be executed when theme toggles between dark and light mode."
placeholderText: "e.g., notify-send \"Theme\" \"Toggled\""
text: Settings.data.hooks.darkModeChange
onEditingFinished: {
Settings.data.hooks.darkModeChange = darkModeHookInput.text
}
onActionClicked: {
if (darkModeHookInput.text) {
HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode)
}
}
Layout.fillWidth: true
}
NDivider {
Layout.fillWidth: true
}
// Info section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NLabel {
label: "Hook Command Information"
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
}
NLabel {
label: "Available Parameters"
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
}
} }
} }
} }

View file

@ -59,13 +59,6 @@ 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,14 +12,22 @@ ColumnLayout {
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
NToggle { NToggle {
label: "Enable Wi-Fi" label: "WiFi Enabled"
description: "Enable Wi-Fi connectivity." description: "Enable WiFi connectivity."
checked: Settings.data.network.wifiEnabled checked: Settings.data.network.wifiEnabled
onToggled: checked => NetworkService.setWifiEnabled(checked) onToggled: checked => {
Settings.data.network.wifiEnabled = checked
NetworkService.setWifiEnabled(checked)
if (checked) {
ToastService.showNotice("WiFi", "Enabled")
} else {
ToastService.showNotice("WiFi", "Disabled")
}
}
} }
NToggle { NToggle {
label: "Enable Bluetooth" label: "Bluetooth Enabled"
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,6 +115,7 @@ 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
} }
} }
@ -277,6 +278,7 @@ 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,7 +104,19 @@ NBox {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds) var minutes = Math.floor(uptimeSeconds / 60) % 60
var hours = Math.floor(uptimeSeconds / 3600) % 24
var days = Math.floor(uptimeSeconds / 86400)
// Format the output
if (days > 0) {
uptimeText = days + "d " + hours + "h"
} else if (hours > 0) {
uptimeText = hours + "h" + minutes + "m"
} else {
uptimeText = minutes + "m"
}
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
ColumnLayout { Column {
id: content id: content
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -22,6 +22,11 @@ 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"
@ -55,5 +60,10 @@ 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,11 +14,16 @@ NPanel {
panelHeight: 500 * scaling panelHeight: 500 * scaling
panelKeyboardFocus: true panelKeyboardFocus: true
property string passwordSsid: "" property string passwordPromptSsid: ""
property string passwordInput: "" property string passwordInput: ""
property string expandedSsid: "" property bool showPasswordPrompt: false
property string expandedNetwork: "" // Track which network shows options
onOpened: NetworkService.scan() onOpened: {
if (Settings.data.network.wifiEnabled) {
NetworkService.refreshNetworks()
}
}
panelContent: Rectangle { panelContent: Rectangle {
color: Color.transparent color: Color.transparent
@ -34,32 +39,35 @@ NPanel {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NIcon {
text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off" text: "wifi"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant color: Color.mPrimary
} }
NText { NText {
text: "Wi-Fi" text: "WiFi"
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
} }
NToggle { // Connection status indicator
id: wifiSwitch Rectangle {
checked: Settings.data.network.wifiEnabled visible: NetworkService.hasActiveConnection
onToggled: checked => NetworkService.setWifiEnabled(checked) width: 8 * scaling
baseSize: Style.baseWidgetSize * 0.65 * scaling height: 8 * scaling
radius: 4 * scaling
color: Color.mPrimary
} }
NIconButton { NIconButton {
icon: "refresh" icon: "refresh"
tooltipText: "Refresh" tooltipText: "Refresh networks"
sizeRatio: 0.8 sizeRatio: 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading
onClicked: NetworkService.scan() onClicked: NetworkService.refreshNetworks()
} }
NIconButton { NIconButton {
@ -74,18 +82,17 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
// Error message // Error banner
Rectangle { Rectangle {
visible: NetworkService.lastError.length > 0 visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * scaling * 2) Layout.preferredHeight: errorText.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
@ -97,7 +104,8 @@ NPanel {
} }
NText { NText {
text: NetworkService.lastError id: errorText
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
@ -107,364 +115,301 @@ NPanel {
NIconButton { NIconButton {
icon: "close" icon: "close"
sizeRatio: 0.6 sizeRatio: 0.6
onClicked: NetworkService.lastError = "" onClicked: {
NetworkService.connectStatus = ""
NetworkService.connectError = ""
}
} }
} }
} }
// Main content area ScrollView {
Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
color: Color.transparent ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// WiFi disabled state
ColumnLayout { ColumnLayout {
visible: !Settings.data.network.wifiEnabled width: parent.width
anchors.fill: parent
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Item { // Loading state
Layout.fillHeight: true
}
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 {
width: parent.width 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 spacing: Style.marginM * scaling
// Network list NBusyIndicator {
Repeater { running: true
model: { color: Color.mPrimary
if (!Settings.data.network.wifiEnabled) size: Style.baseWidgetSize * scaling
return [] Layout.alignment: Qt.AlignHCenter
}
const nets = Object.values(NetworkService.networks) NText {
return nets.sort((a, b) => { text: "Scanning for networks..."
if (a.connected !== b.connected) font.pointSize: Style.fontSizeNormal * scaling
return b.connected - a.connected color: Color.mOnSurfaceVariant
return b.signal - a.signal Layout.alignment: Qt.AlignHCenter
}) }
}
// WiFi disabled state
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: !Settings.data.network.wifiEnabled
spacing: Style.marginM * scaling
NIcon {
text: "wifi_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "WiFi is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: "Enable WiFi"
icon: "wifi"
Layout.alignment: Qt.AlignHCenter
onClicked: {
Settings.data.network.wifiEnabled = true
Settings.save()
NetworkService.setWifiEnabled(true)
} }
}
}
// 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 {
Layout.fillWidth: true id: networkRect
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) width: parent.width
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: netColumn id: networkContent
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.marginS * scaling spacing: Style.marginM * scaling
// Main row // Main network 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
spacing: 2 * scaling Layout.alignment: Qt.AlignVCenter
spacing: 0
NText { NText {
text: modelData.ssid text: modelData.ssid || "Unknown Network"
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
color: Color.mOnSurface
elide: Text.ElideRight elide: Text.ElideRight
color: Color.mOnSurface
Layout.fillWidth: true Layout.fillWidth: true
} }
RowLayout { NText {
spacing: Style.marginXS * scaling text: {
const security = modelData.security
NText { && modelData.security !== "--" ? modelData.security : "Open"
text: `${modelData.signal}%` const signal = `${modelData.signal}%`
font.pointSize: Style.fontSizeXXS * scaling return `${signal} ${security}`
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
Item {
Layout.preferredWidth: Style.marginXXS * scaling
}
// Update the status badges area (around line 237)
Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.mPrimary
radius: height * 0.5
width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: connectedText
anchors.centerIn: parent
text: "Connected"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Color.mError
radius: height * 0.5
width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2)
height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: disconnectingText
anchors.centerIn: parent
text: "Disconnecting..."
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Color.mError
radius: height * 0.5
width: forgettingText.implicitWidth + (Style.marginS * scaling * 2)
height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: forgettingText
anchors.centerIn: parent
text: "Forgetting..."
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
Rectangle {
visible: modelData.cached && !modelData.connected
&& NetworkService.forgettingNetwork !== modelData.ssid
&& NetworkService.disconnectingFrom !== modelData.ssid
color: Color.transparent
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * scaling * 2)
height: savedText.implicitHeight + (Style.marginXXS * scaling * 2)
NText {
id: savedText
anchors.centerIn: parent
text: "Saved"
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
} }
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
} }
} }
// Action area // Right-aligned items container
RowLayout { RowLayout {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NBusyIndicator { // Connected badge
visible: NetworkService.connectingTo === modelData.ssid Rectangle {
|| NetworkService.disconnectingFrom === modelData.ssid visible: modelData.connected
|| NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary color: Color.mPrimary
size: Style.baseWidgetSize * 0.5 * scaling radius: width * 0.5
} width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2)
height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2)
NIconButton { NText {
visible: (modelData.existing || modelData.cached) && !modelData.connected id: connectedLabel
&& NetworkService.connectingTo !== modelData.ssid anchors.centerIn: parent
&& NetworkService.forgettingNetwork !== modelData.ssid text: "Connected"
&& NetworkService.disconnectingFrom !== modelData.ssid font.pointSize: Style.fontSizeXXS * scaling
icon: "delete" color: Color.mOnPrimary
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: { // Saved badge - clickable
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { Rectangle {
NetworkService.connect(modelData.ssid) visible: modelData.cached && !modelData.connected
} else { color: Color.mSurfaceVariant
passwordSsid = modelData.ssid radius: width * 0.5
passwordInput = "" width: savedLabel.implicitWidth + (Style.marginS * scaling * 2)
expandedSsid = "" 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
color: Color.mOnSurfaceVariant
}
} }
NButton { // Loading indicator
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid NBusyIndicator {
text: "Disconnect" visible: NetworkService.connectingSsid === modelData.ssid
outlined: !hovered running: NetworkService.connectingSsid === modelData.ssid
fontSize: Style.fontSizeXS * scaling color: Color.mPrimary
backgroundColor: Color.mError size: Style.baseWidgetSize * 0.6 * scaling
onClicked: NetworkService.disconnect(modelData.ssid) }
// Action buttons
RowLayout {
spacing: Style.marginXS * scaling
visible: NetworkService.connectingSsid !== modelData.ssid
NButton {
visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt)
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
text: modelData.existing ? "Connect" : (NetworkService.isSecured(
modelData.security) ? "Password" : "Connect")
onClicked: {
if (modelData.existing || !NetworkService.isSecured(modelData.security)) {
NetworkService.connectNetwork(modelData.ssid, modelData.security)
} else {
expandedNetwork = modelData.ssid
passwordPromptSsid = modelData.ssid
showPasswordPrompt = true
passwordInput = ""
Qt.callLater(() => passwordInputField.forceActiveFocus())
}
}
}
NButton {
visible: modelData.connected
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
backgroundColor: Color.mError
text: "Disconnect"
onClicked: NetworkService.disconnectNetwork(modelData.ssid)
}
} }
} }
} }
// Password input // Password input section
Rectangle { Rectangle {
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
&& NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true Layout.fillWidth: true
height: passwordRow.implicitHeight + Style.marginS * scaling * 2 implicitHeight: visible ? 50 * scaling : 0
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.marginM * scaling spacing: Style.marginS * scaling
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
radius: Style.radiusXS * scaling radius: Style.radiusS * scaling
color: Color.mSurface color: Color.mSurface
border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline border.color: passwordInputField.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
TextInput { TextInput {
id: pwdInput id: passwordInputField
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.margins: Style.marginS * scaling anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
height: parent.height
text: passwordInput text: passwordInput
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface color: Color.mOnSurface
echoMode: TextInput.Password verticalAlignment: TextInput.AlignVCenter
clip: true
focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt
selectByMouse: true selectByMouse: true
focus: visible echoMode: TextInput.Password
passwordCharacter: "●" passwordCharacter: "●"
onTextChanged: passwordInput = text onTextChanged: passwordInput = text
onVisibleChanged: if (visible)
forceActiveFocus()
onAccepted: { onAccepted: {
if (text) { if (passwordInput) {
NetworkService.connect(passwordSsid, text) NetworkService.submitPassword(passwordPromptSsid, passwordInput)
passwordSsid = "" showPasswordPrompt = false
passwordInput = "" expandedNetwork = ""
} }
} }
@ -480,75 +425,56 @@ NPanel {
NButton { NButton {
text: "Connect" text: "Connect"
fontSize: Style.fontSizeXXS * scaling icon: "check"
fontSize: Style.fontSizeXS * scaling
enabled: passwordInput.length > 0 enabled: passwordInput.length > 0
outlined: true outlined: !enabled
onClicked: { onClicked: {
NetworkService.connect(passwordSsid, passwordInput) if (passwordInput) {
passwordSsid = "" NetworkService.submitPassword(passwordPromptSsid, passwordInput)
passwordInput = "" showPasswordPrompt = false
expandedNetwork = ""
}
} }
} }
NIconButton { NIconButton {
icon: "close" icon: "close"
sizeRatio: 0.8 tooltipText: "Cancel"
sizeRatio: 0.9
onClicked: { onClicked: {
passwordSsid = "" showPasswordPrompt = false
expandedNetwork = ""
passwordInput = "" passwordInput = ""
} }
} }
} }
} }
// Forget network // Forget network option - appears when saved badge is clicked
Rectangle { RowLayout {
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid
&& NetworkService.forgettingNetwork !== modelData.ssid && !showPasswordPrompt
Layout.fillWidth: true Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling Layout.topMargin: Style.marginXS * scaling
color: Color.mSurfaceVariant spacing: Style.marginS * scaling
radius: Style.radiusS * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mOutline
RowLayout { Item {
id: forgetRow Layout.fillWidth: true
anchors.fill: parent }
anchors.margins: Style.marginS * scaling
spacing: Style.marginM * scaling
RowLayout { NButton {
NIcon { id: forgetButton
text: "delete_outline" text: "Forget Network"
font.pointSize: Style.fontSizeL * scaling icon: "delete_outline"
color: Color.mError fontSize: Style.fontSizeXXS * scaling
} backgroundColor: Color.mError
textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary
NText { outlined: !forgetButton.hovered
text: "Forget this network?" Layout.preferredHeight: 28 * scaling
font.pointSize: Style.fontSizeS * scaling onClicked: {
color: Color.mError NetworkService.forgetNetwork(modelData.ssid)
Layout.fillWidth: true expandedNetwork = ""
}
}
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 = ""
} }
} }
} }
@ -556,42 +482,35 @@ NPanel {
} }
} }
} }
}
// Empty state when no networks // No networks found
ColumnLayout { ColumnLayout {
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys( Layout.fillWidth: true
NetworkService.networks).length === 0 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
anchors.fill: parent visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys(
spacing: Style.marginL * scaling NetworkService.networks).length === 0
spacing: Style.marginM * scaling
Item { NIcon {
Layout.fillHeight: true text: "wifi_find"
} font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NIcon { NText {
text: "wifi_find" text: "No networks found"
font.pointSize: 64 * scaling font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
NText { NButton {
text: "No networks found" text: "Refresh"
font.pointSize: Style.fontSizeL * scaling icon: "refresh"
color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignHCenter onClicked: NetworkService.refreshNetworks()
} }
NButton {
text: "Scan again"
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan()
}
Item {
Layout.fillHeight: true
} }
} }
} }

View file

@ -215,7 +215,6 @@ 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` |
@ -266,10 +265,6 @@ 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
@ -284,8 +279,6 @@ 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,7 +28,6 @@ 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,
@ -44,11 +43,6 @@ Singleton {
"leftClickExec": "", "leftClickExec": "",
"rightClickExec": "", "rightClickExec": "",
"middleClickExec": "" "middleClickExec": ""
},
"Spacer": {
"allowUserSettings": true,
"icon": "space_bar",
"width": 20
} }
}) })
@ -107,9 +101,6 @@ 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,239 +8,215 @@ import qs.Commons
Singleton { Singleton {
id: root id: root
// Core state // Core properties
property var networks: ({}) property var networks: ({})
property bool scanning: false property string connectingSsid: ""
property bool connecting: false property string connectStatus: ""
property string connectingTo: "" property string connectStatusSsid: ""
property string lastError: "" property string connectError: ""
property bool ethernetConnected: false property bool isLoading: false
property string disconnectingFrom: "" property bool ethernet: false
property string forgettingNetwork: "" property int retryCount: 0
property int maxRetries: 3
// Persistent cache // File path for persistent storage
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
// Cache file handling // Stable properties for UI
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()
JsonAdapter { onLoaded: {
id: cacheAdapter Logger.log("Network", "Loaded network cache from disk")
property var knownNetworks: ({}) // Try to auto-connect on startup if WiFi is enabled
property string lastConnected: "" if (Settings.data.network.wifiEnabled && adapter.lastConnected) {
} autoConnectTimer.start()
onLoadFailed: {
cacheAdapter.knownNetworks = ({})
cacheAdapter.lastConnected = ""
}
}
Connections {
target: Settings.data.network
function onWifiEnabledChanged() {
if (Settings.data.network.wifiEnabled) {
ToastService.showNotice("Wi-Fi", "Enabled")
} else {
ToastService.showNotice("Wi-Fi", "Disabled")
} }
} }
onLoadFailed: function (error) {
Logger.log("Network", "No existing cache found, creating new one")
// Initialize with empty data
adapter.knownNetworks = ({})
adapter.lastConnected = ""
}
JsonAdapter {
id: adapter
property var knownNetworks: ({})
property string lastConnected: ""
property int lastRefresh: 0
}
} }
Component.onCompleted: { // Save timer to batch writes
Logger.log("Network", "Service initialized")
syncWifiState()
refresh()
}
// Save cache with debounce
Timer { Timer {
id: saveDebounce id: saveTimer
running: false
interval: 1000 interval: 1000
onTriggered: cacheFileView.writeAdapter() onTriggered: cacheFileView.writeAdapter()
} }
function saveCache() { Component.onCompleted: {
saveDebounce.restart() Logger.log("Network", "Service started")
}
// 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) {
scan() refreshNetworks()
} }
} }
function scan() { // Signal strength icon mapping
if (scanning)
return
scanning = true
lastError = ""
scanProcess.running = true
Logger.log("Network", "Wi-Fi scan in progress...")
}
function connect(ssid, password = "") {
if (connecting)
return
connecting = true
connectingTo = ssid
lastError = ""
// Check if we have a saved connection
if (networks[ssid]?.existing || cachedNetworks[ssid]) {
connectProcess.mode = "saved"
connectProcess.ssid = ssid
connectProcess.password = ""
} else {
connectProcess.mode = "new"
connectProcess.ssid = ssid
connectProcess.password = password
}
connectProcess.running = true
}
function disconnect(ssid) {
disconnectingFrom = ssid
disconnectProcess.ssid = ssid
disconnectProcess.running = true
}
function forget(ssid) {
forgettingNetwork = ssid
// Remove from cache
let known = cacheAdapter.knownNetworks
delete known[ssid]
cacheAdapter.knownNetworks = known
if (cacheAdapter.lastConnected === ssid) {
cacheAdapter.lastConnected = ""
}
saveCache()
// Remove from system
forgetProcess.ssid = ssid
forgetProcess.running = true
}
// Helper function to immediately update network status
function updateNetworkStatus(ssid, connected) {
let nets = networks
// Update all networks connected status
for (let key in nets) {
if (nets[key].connected && key !== ssid) {
nets[key].connected = false
}
}
// Update the target network if it exists
if (nets[ssid]) {
nets[ssid].connected = connected
nets[ssid].existing = true
nets[ssid].cached = true
} else if (connected) {
// Create a temporary entry if network doesn't exist yet
nets[ssid] = {
"ssid": ssid,
"security": "--",
"signal": 100,
"connected"// Default to good signal until real scan
: true,
"existing": true,
"cached": true
}
}
// Trigger property change notification
networks = ({})
networks = nets
}
// Helper functions
function signalIcon(signal) { function signalIcon(signal) {
if (signal >= 80) const levels = [{
return "network_wifi" "threshold": 80,
if (signal >= 60) "icon": "network_wifi"
return "network_wifi_3_bar" }, {
if (signal >= 40) "threshold": 60,
return "network_wifi_2_bar" "icon": "network_wifi_3_bar"
if (signal >= 20) }, {
return "network_wifi_1_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" return "signal_wifi_0_bar"
} }
function isSecured(security) { function isSecured(security) {
return security && security !== "--" && security.trim() !== "" return security && security.trim() !== "" && security.trim() !== "--"
}
// Enhanced refresh with retry logic
function refreshNetworks() {
if (isLoading)
return
isLoading = true
retryCount = 0
adapter.lastRefresh = Date.now()
performRefresh()
}
function performRefresh() {
checkEthernet.running = true
existingNetworkProcess.running = true
}
// Retry mechanism for failed operations
function retryRefresh() {
if (retryCount < maxRetries) {
retryCount++
Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`)
retryTimer.start()
} else {
isLoading = false
connectError = "Failed to refresh networks after multiple attempts"
}
}
Timer {
id: retryTimer
interval: 1000 * retryCount // Progressive backoff
repeat: false
onTriggered: performRefresh()
}
Timer {
id: autoConnectTimer
interval: 3000
repeat: false
onTriggered: {
if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) {
Logger.log("Network", `Auto-connecting to ${adapter.lastConnected}`)
connectToExisting(adapter.lastConnected)
}
}
}
// Forget network function
function forgetNetwork(ssid) {
Logger.log("Network", `Forgetting network: ${ssid}`)
// Remove from cache
let known = adapter.knownNetworks
delete known[ssid]
adapter.knownNetworks = known
// Clear last connected if it's this network
if (adapter.lastConnected === ssid) {
adapter.lastConnected = ""
}
// Save changes
saveTimer.restart()
// Remove NetworkManager profile
forgetProcess.ssid = ssid
forgetProcess.running = true
} }
// Processes
Process { Process {
id: ethernetStateProcess id: forgetProcess
property string ssid: ""
running: false running: false
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] command: ["nmcli", "connection", "delete", "id", ssid]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const connected = text.split("\n").some(line => { Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`)
const parts = line.split(":") refreshNetworks()
return parts[1] === "ethernet" && parts[2] === "connected" }
}) }
if (root.ethernetConnected !== connected) {
root.ethernetConnected = connected stderr: StdioCollector {
Logger.log("Network", "Ethernet connected:", root.ethernetConnected) onStreamFinished: {
if (text.trim()) {
if (text.includes("no such connection profile")) {
Logger.log("Network", `Network profile not found: ${forgetProcess.ssid}`)
} else {
Logger.warn("Network", `Error forgetting network: ${text}`)
}
refreshNetworks()
} }
} }
} }
} }
Process { // WiFi enable/disable functions
id: wifiStateProcess function setWifiEnabled(enabled) {
running: false if (enabled) {
command: ["nmcli", "radio", "wifi"] isLoading = true
wifiRadioProcess.action = "on"
stdout: StdioCollector { wifiRadioProcess.running = true
onStreamFinished: { } else {
const enabled = text.trim() === "enabled" // Save current connection for later
Logger.log("Network", "Wi-Fi enabled:", enabled) for (const ssid in networks) {
if (Settings.data.network.wifiEnabled !== enabled) { if (networks[ssid].connected) {
Settings.data.network.wifiEnabled = enabled adapter.lastConnected = ssid
saveTimer.restart()
disconnectNetwork(ssid)
break
} }
} }
wifiRadioProcess.action = "off"
wifiRadioProcess.running = true
} }
} }
// Unified WiFi radio control
Process { Process {
id: wifiToggleProcess id: wifiRadioProcess
property string action: "on" property string action: "on"
running: false running: false
command: ["nmcli", "radio", "wifi", action] command: ["nmcli", "radio", "wifi", action]
@ -248,12 +224,10 @@ Singleton {
onRunningChanged: { onRunningChanged: {
if (!running) { if (!running) {
if (action === "on") { if (action === "on") {
// Clear networks immediately and start delayed scan wifiEnableTimer.start()
root.networks = ({})
delayedScanTimer.interval = 8000
delayedScanTimer.restart()
} else { } else {
root.networks = ({}) root.networks = ({})
root.isLoading = false
} }
} }
} }
@ -261,177 +235,137 @@ Singleton {
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim()) { if (text.trim()) {
Logger.warn("Network", "WiFi toggle error: " + text) Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`)
} }
} }
} }
} }
Process { Timer {
id: scanProcess id: wifiEnableTimer
running: false interval: 2000
command: ["sh", "-c", ` repeat: false
# Get list of saved connection profiles (just the names) onTriggered: {
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|') refreshNetworks()
if (adapter.lastConnected) {
# Get WiFi networks reconnectTimer.start()
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: {
if (mode === "saved") { const cmd = ["nmcli", "device", "wifi", "connect", ssid]
return ["nmcli", "connection", "up", "id", ssid] if (isSecured && password) {
} else { cmd.push("password", password)
const cmd = ["nmcli", "device", "wifi", "connect", ssid]
if (password) {
cmd.push("password", password)
}
return cmd
} }
return cmd
} }
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
// Success - update cache handleConnectionSuccess(connectProcess.ssid)
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()) {
// Parse common errors handleConnectionError(connectProcess.ssid, text)
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) Process {
id: upConnectionProcess
property string profileName: ""
running: false
command: ["nmcli", "connection", "up", "id", profileName]
stdout: StdioCollector {
onStreamFinished: {
handleConnectionSuccess(upConnectionProcess.profileName)
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
handleConnectionError(upConnectionProcess.profileName, text)
} }
} }
} }
@ -443,101 +377,221 @@ Singleton {
running: false running: false
command: ["nmcli", "connection", "down", "id", ssid] command: ["nmcli", "connection", "down", "id", ssid]
stdout: StdioCollector { onRunningChanged: {
onStreamFinished: { if (!running) {
Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`) connectingSsid = ""
connectStatus = ""
// Immediately update UI on successful disconnect connectStatusSsid = ""
root.updateNetworkStatus(disconnectProcess.ssid, false) connectError = ""
root.disconnectingFrom = "" refreshNetworks()
// 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 error: " + text) Logger.warn("Network", `Disconnect warning: ${text}`)
}
}
}
}
// Connection result handlers
function handleConnectionSuccess(ssid) {
connectingSsid = ""
connectStatus = "success"
connectStatusSsid = ssid
connectError = ""
// Update cache
let known = adapter.knownNetworks
known[ssid] = {
"profileName": ssid,
"lastConnected": Date.now(),
"autoConnect": true
}
adapter.knownNetworks = known
adapter.lastConnected = ssid
saveTimer.restart()
Logger.log("Network", `Successfully connected to ${ssid}`)
refreshNetworks()
}
function handleConnectionError(ssid, error) {
connectingSsid = ""
connectStatus = "error"
connectStatusSsid = ssid
connectError = parseError(error)
Logger.warn("Network", `Failed to connect to ${ssid}: ${error}`)
}
function parseError(error) {
// Simplify common error messages
if (error.includes("Secrets were required") || error.includes("no secrets provided")) {
return "Incorrect password"
}
if (error.includes("No network with SSID")) {
return "Network not found"
}
if (error.includes("Connection activation failed")) {
return "Connection failed. Please try again."
}
if (error.includes("Timeout")) {
return "Connection timeout. Network may be out of range."
}
// Return first line only
return error.split("\n")[0].trim()
}
// Network scanning processes
Process {
id: existingNetworkProcess
running: false
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
stdout: StdioCollector {
onStreamFinished: {
const profiles = {}
const lines = text.split("\n").filter(l => l.trim())
for (const line of lines) {
const parts = line.split(":")
const name = parts[0]
const type = parts[1]
if (name && type === "802-11-wireless") {
profiles[name] = {
"ssid": name,
"type": type
}
}
}
scanProcess.existingProfiles = profiles
scanProcess.running = true
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
Logger.warn("Network", "Error listing connections:", text)
retryRefresh()
} }
// Still trigger a scan even on error
delayedScanTimer.interval = 1000
delayedScanTimer.restart()
} }
} }
} }
Process { Process {
id: forgetProcess id: scanProcess
property string ssid: "" property var existingProfiles: ({})
running: false running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
// 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: {
Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`) const networksMap = {}
Logger.log("Network", text.trim().replace(/[\r\n]/g, " ")) const lines = text.split("\n").filter(l => l.trim())
// Update both cached and existing status immediately for (const line of lines) {
let nets = root.networks const parts = line.split(":")
if (nets[forgetProcess.ssid]) { if (parts.length < 4)
nets[forgetProcess.ssid].cached = false continue
nets[forgetProcess.ssid].existing = false
// Trigger property change const ssid = parts[0]
root.networks = ({}) const security = parts[1]
root.networks = nets 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.forgettingNetwork = "" root.networks = networksMap
root.isLoading = false
scanProcess.existingProfiles = {}
// Quick scan to verify the profile is gone //Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`)
delayedScanTimer.interval = 500
delayedScanTimer.restart()
} }
} }
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
root.forgettingNetwork = "" if (text.trim()) {
if (text.trim() && !text.includes("No profiles found")) { Logger.warn("Network", "Error scanning networks:", text)
Logger.warn("Network", "Forget error: " + text) retryRefresh()
} }
// Still Trigger a scan even on error
delayedScanTimer.interval = 500
delayedScanTimer.restart()
} }
} }
} }
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 do-not-disturb is enabled // Check if notifications are suppressed
if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) { if (Settings.data.notifications && Settings.data.notifications.suppressed) {
// Still add to history but don't show notification
root.addToHistory(notification)
return return
} }
@ -46,6 +46,8 @@ Singleton {
// Add to our model // Add to our model
root.addNotification(notification) root.addNotification(notification)
// Also add to history
root.addToHistory(notification)
} }
} }
@ -107,15 +109,6 @@ 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(0) + "KB/s" return (bytesPerSecond / 1024).toFixed(1) + "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,21 +165,13 @@ 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)
// Always process immediately for instant display // Process queue if not currently showing a toast
processQueue() if (!isShowingToast) {
processQueue()
}
} }
// Process the message queue // Process the message queue
@ -189,6 +181,11 @@ 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.6.0" property string baseVersion: "2.5.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,10 +1,9 @@
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 using Layout management // Compact circular statistic display used in the SidePanel
Rectangle { Rectangle {
id: root id: root
@ -29,20 +28,20 @@ Rectangle {
// Repaint gauge when the bound value changes // Repaint gauge when the bound value changes
onValueChanged: gauge.requestPaint() onValueChanged: gauge.requestPaint()
ColumnLayout { Row {
id: mainLayout id: innerRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling * contentScale anchors.margins: Style.marginS * scaling * contentScale
spacing: 0 spacing: Style.marginS * scaling * contentScale
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
// Main gauge container // Gauge with percentage label placed inside the open gap (right side)
Item { Item {
id: gaugeContainer id: gaugeWrap
Layout.fillWidth: true anchors.verticalCenter: innerRow.verticalCenter
Layout.fillHeight: true width: 68 * scaling * contentScale
Layout.alignment: Qt.AlignCenter height: 68 * scaling * contentScale
Layout.preferredWidth: 68 * scaling * contentScale
Layout.preferredHeight: 68 * scaling * contentScale
Canvas { Canvas {
id: gauge id: gauge
@ -85,13 +84,15 @@ Rectangle {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
// Tiny circular badge for the icon, positioned using anchors within the gauge // Tiny circular badge for the icon, inside the right-side gap
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,7 +18,6 @@ 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,34 +8,39 @@ 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: 150 * scaling implicitWidth: expanded ? 320 * scaling : 150 * scaling
implicitHeight: 40 * scaling implicitHeight: expanded ? 300 * scaling : 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)
// Minimized Look property var presetColors: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000", "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E"]
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Style.animationFast
}
}
// Collapsed view - just show current color
MouseArea { MouseArea {
visible: !root.expanded
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: root.expanded = true
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
@ -63,4 +68,119 @@ 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

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

View file

@ -231,7 +231,8 @@ 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

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

View file

@ -11,6 +11,7 @@ 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
@ -25,6 +26,7 @@ ColumnLayout {
signal editingFinished signal editingFinished
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
implicitHeight: frame.height
NLabel { NLabel {
label: root.label label: root.label
@ -32,7 +34,6 @@ 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
@ -41,48 +42,50 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 80 * scaling Layout.minimumWidth: 80 * scaling
implicitHeight: Style.baseWidgetSize * 1.1 * scaling Layout.maximumWidth: root.inputMaxWidth
implicitWidth: parent.width
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: Color.mSurface color: Color.mSurface
border.color: input.activeFocus ? Color.mSecondary : Color.mOutline border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
Behavior on border.color { // Focus ring
ColorAnimation { Rectangle {
duration: Style.animationFast anchors.fill: parent
radius: frame.radius
color: Color.transparent
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
} }
} }
TextField { RowLayout {
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
verticalAlignment: TextInput.AlignVCenter TextField {
id: input
echoMode: TextInput.Normal Layout.fillWidth: true
readOnly: root.readOnly echoMode: TextInput.Normal
enabled: root.enabled readOnly: root.readOnly
color: Color.mOnSurface enabled: root.enabled
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) color: Color.mOnSurface
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
selectByMouse: true background: null
font.family: fontFamily
topPadding: 0 font.pointSize: fontSize
bottomPadding: 0 font.weight: fontWeight
leftPadding: 0 onEditingFinished: root.editingFinished()
rightPadding: 0 }
background: null
font.family: root.fontFamily
font.pointSize: root.fontSize
font.weight: root.fontWeight
onEditingFinished: root.editingFinished()
} }
} }
} }

View file

@ -37,16 +37,7 @@ 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()
@ -90,6 +81,7 @@ 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
@ -145,41 +137,43 @@ Item {
} }
// Label and description // Label and description
ColumnLayout { Column {
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 {
Layout.fillWidth: true id: labelText
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 {
Layout.fillWidth: true id: descriptionText
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
colorBg: Color.mSurfaceVariant color: Color.mOnSurface
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