Compare commits

..

No commits in common. "7860c41959de3d6b30db598a5b9ff10bfef507fe" and "1cab45235285e878d28ae7b328780fef48e7ea34" have entirely different histories.

58 changed files with 2159 additions and 2922 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,42 +2,35 @@ 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())
}
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) {
return Icons.iconForAppId(activeToplevel.appId.toLowerCase())
}
}
return "" return ""
return Icons.iconForAppId(focusedWindow.appId)
} }
// A hidden text element to safely measure the full title width // A hidden text element to safely measure the full title width
@ -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,13 +71,12 @@ 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 {
@ -91,7 +92,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 == "wave" active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
&& MediaService.isPlaying && MediaService.isPlaying && MediaService.trackLength > 0
z: 0 z: 0
sourceComponent: WaveSpectrum { sourceComponent: WaveSpectrum {
@ -102,9 +103,10 @@ RowLayout {
opacity: 0.4 opacity: 0.4
} }
} }
}
RowLayout { Row {
id: rowLayout id: row
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,44 +1,46 @@
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
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Row {
id: cpuUsageLayout id: cpuUsageLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NIcon { NIcon {
id: cpuUsageIcon id: cpuUsageIcon
text: "speed" text: "speed"
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
NText { NText {
@ -47,22 +49,21 @@ RowLayout {
font.family: Settings.data.ui.fontFixed font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mPrimary color: Color.mPrimary
} }
} }
// CPU Temperature Component // CPU Temperature Component
RowLayout { Row {
id: cpuTempLayout id: cpuTempLayout
// spacing is thin here to compensate for the vertical thermometer icon // spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.alignment: Qt.AlignVCenter
NIcon { NIcon {
text: "thermometer" text: "thermometer"
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
NText { NText {
@ -70,21 +71,20 @@ RowLayout {
font.family: Settings.data.ui.fontFixed font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mPrimary color: Color.mPrimary
} }
} }
// Memory Usage Component // Memory Usage Component
RowLayout { Row {
id: memoryUsageLayout id: memoryUsageLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NIcon { NIcon {
text: "memory" text: "memory"
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
NText { NText {
@ -92,22 +92,21 @@ RowLayout {
font.family: Settings.data.ui.fontFixed font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mPrimary color: Color.mPrimary
} }
} }
// Network Download Speed Component // Network Download Speed Component
RowLayout { Row {
id: networkDownloadLayout id: networkDownloadLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats visible: Settings.data.bar.showNetworkStats
NIcon { NIcon {
text: "download" text: "download"
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
NText { NText {
@ -115,22 +114,21 @@ RowLayout {
font.family: Settings.data.ui.fontFixed font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mPrimary color: Color.mPrimary
} }
} }
// Network Upload Speed Component // Network Upload Speed Component
RowLayout { Row {
id: networkUploadLayout id: networkUploadLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats visible: Settings.data.bar.showNetworkStats
NIcon { NIcon {
text: "upload" text: "upload"
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
NText { NText {
@ -138,11 +136,12 @@ RowLayout {
font.family: Settings.data.ui.fontFixed font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mPrimary 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,15 +146,14 @@ Rectangle {
function open() { function open() {
visible = true visible = true
PanelService.willOpenPanel(trayPanel) PanelService.willOpenPanel(trayPanel)
} }
function close() { function close() {
visible = false visible = false
if (trayMenu.item) {
trayMenu.item.hideMenu() trayMenu.item.hideMenu()
} }
}
// Clicking outside of the rectangle to close // Clicking outside of the rectangle to close
MouseArea { MouseArea {

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,9 +243,15 @@ NPanel {
anchors.margins: Style.marginL * scaling anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Item {
id: searchInputWrap
Layout.fillWidth: true
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
NTextInput { NTextInput {
id: searchInput id: searchInput
Layout.fillWidth: true anchors.fill: parent
inputMaxWidth: Number.MAX_SAFE_INTEGER
fontSize: Style.fontSizeL * scaling fontSize: Style.fontSizeL * scaling
fontWeight: Style.fontWeightSemiBold fontWeight: Style.fontWeightSemiBold
@ -285,6 +291,7 @@ NPanel {
} }
} }
} }
}
// Results list // Results list
ListView { ListView {

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,6 +375,11 @@ Loader {
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: 50 * scaling anchors.verticalCenterOffset: 50 * scaling
Item {
width: parent.width
height: 280 * scaling
Layout.fillWidth: true
Rectangle { Rectangle {
id: terminalBackground id: terminalBackground
anchors.fill: parent anchors.fill: parent
@ -431,7 +434,7 @@ Loader {
Layout.fillWidth: true Layout.fillWidth: true
} }
RowLayout { Row {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible visible: batteryIndicator.batteryVisible
NIcon { NIcon {
@ -448,7 +451,7 @@ Loader {
} }
} }
RowLayout { Row {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NText { NText {
text: keyboardLayout.currentLayout text: keyboardLayout.currentLayout
@ -641,12 +644,12 @@ Loader {
} }
} }
RowLayout { Row {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling Layout.bottomMargin: -10 * scaling
Rectangle { Rectangle {
Layout.preferredWidth: 120 * scaling width: 120 * scaling
Layout.preferredHeight: 40 * scaling height: 40 * scaling
radius: Style.radiusS * scaling radius: Style.radiusS * scaling
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2) color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
border.color: Color.mPrimary border.color: Color.mPrimary
@ -731,17 +734,18 @@ Loader {
} }
} }
} }
}
// 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,60 +156,65 @@ Variants {
} }
} }
ColumnLayout {
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
spacing: Style.marginM * scaling
// Header section with app name and timestamp
RowLayout { RowLayout {
Layout.fillWidth: true id: contentRow
spacing: Style.marginS * scaling anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginL * scaling
// Right: header on top, then avatar + texts
ColumnLayout {
id: textColumn
spacing: Style.marginS * scaling
Layout.fillWidth: true
RowLayout {
spacing: Style.marginS * scaling
id: appHeaderRow
NText { NText {
text: `${(model.appName || model.desktopEntry) text: `${(model.appName || model.desktopEntry)
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling font.pointSize: Style.fontSizeXS * scaling
} }
Rectangle { Rectangle {
Layout.preferredWidth: 6 * scaling width: 6 * scaling
Layout.preferredHeight: 6 * scaling height: 6 * scaling
radius: Style.radiusXS * scaling radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
// Main content section
RowLayout { RowLayout {
Layout.fillWidth: true id: bodyRow
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Avatar
NImageCircled { NImageCircled {
id: appAvatar id: appAvatar
Layout.preferredWidth: 40 * scaling Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
imagePath: model.image && model.image !== "" ? model.image : "" // Start avatar aligned with body (below the summary)
fallbackIcon: "" 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 borderColor: Color.transparent
borderWidth: 0 borderWidth: 0
visible: (model.image && model.image !== "") visible: (imagePath && imagePath !== "")
} }
// Text content Column {
ColumnLayout { id: textContent
Layout.fillWidth: true
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
Layout.fillWidth: true
// Ensure a concrete width so text wraps
width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0))
NText { NText {
text: model.summary || "No summary" text: model.summary || "No summary"
@ -230,6 +223,7 @@ Variants {
color: Color.mOnSurface color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true Layout.fillWidth: true
width: parent.width
maximumLineCount: 3 maximumLineCount: 3
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -240,67 +234,26 @@ Variants {
color: Color.mOnSurface color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true Layout.fillWidth: true
width: parent.width
maximumLineCount: 5 maximumLineCount: 5
elide: Text.ElideRight elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Notification actions
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
visible: model.rawNotification && model.rawNotification.actions
&& model.rawNotification.actions.length > 0
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
Repeater {
model: parent.notificationActions
delegate: NButton {
text: {
var actionText = modelData.text || "Open"
// If text contains comma, take the part after the comma (the display text)
if (actionText.includes(",")) {
return actionText.split(",")[1] || actionText
}
return actionText
}
fontSize: Style.fontSizeS * scaling
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
hoverColor: Color.mSecondary
pressColor: Color.mTertiary
outlined: false
customHeight: 32 * scaling
Layout.preferredHeight: 32 * scaling
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
} }
} }
} }
} }
// Spacer to push buttons to the left if needed // Actions removed
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: modelData.icon icon: "lock_outline"
title: modelData.title title: "Lock"
subtitle: modelData.subtitle subtitle: "Lock your session"
isShutdown: modelData.isShutdown || false onClicked: startTimer("lock")
isSelected: index === selectedIndex pending: timerActive && pendingAction === "lock"
onClicked: {
selectedIndex = index
startTimer(modelData.action)
} }
pending: timerActive && pendingAction === modelData.action
// Suspend
PowerButton {
Layout.fillWidth: true
icon: "bedtime"
title: "Suspend"
subtitle: "Put the system to sleep"
onClicked: startTimer("suspend")
pending: timerActive && pendingAction === "suspend"
} }
// Reboot
PowerButton {
Layout.fillWidth: true
icon: "refresh"
title: "Reboot"
subtitle: "Restart the system"
onClicked: startTimer("reboot")
pending: timerActive && pendingAction === "reboot"
}
// Logout
PowerButton {
Layout.fillWidth: true
icon: "exit_to_app"
title: "Logout"
subtitle: "End your session"
onClicked: startTimer("logout")
pending: timerActive && pendingAction === "logout"
}
// Shutdown
PowerButton {
Layout.fillWidth: true
icon: "power_settings_new"
title: "Shutdown"
subtitle: "Turn off the system"
onClicked: startTimer("shutdown")
pending: timerActive && pendingAction === "shutdown"
isShutdown: true
} }
} }
} }
@ -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,18 +267,9 @@ NPanel {
} }
panelContent: Rectangle { panelContent: Rectangle {
color: Color.transparent
// Main layout container that fills the panel
ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginL * scaling anchors.margins: Style.marginL * scaling
spacing: 0 color: Color.transparent
// Keyboard shortcuts container
Item {
Layout.preferredWidth: 0
Layout.preferredHeight: 0
// Scrolling via keyboard // Scrolling via keyboard
Shortcut { Shortcut {
@ -329,20 +320,15 @@ NPanel {
onActivated: root.selectPreviousTab() onActivated: root.selectPreviousTab()
enabled: root.opened enabled: root.opened
} }
}
// Main content area
RowLayout { RowLayout {
Layout.fillWidth: true anchors.fill: parent
Layout.fillHeight: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Sidebar
Rectangle { Rectangle {
id: sidebar id: sidebar
Layout.preferredWidth: 220 * scaling Layout.preferredWidth: 220 * scaling
Layout.fillHeight: true Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
border.color: Color.mOutline border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
@ -365,7 +351,7 @@ NPanel {
} }
} }
ColumnLayout { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling spacing: Style.marginXS * 1.5 * scaling
@ -375,8 +361,8 @@ NPanel {
model: root.tabsModel model: root.tabsModel
delegate: Rectangle { delegate: Rectangle {
id: tabItem id: tabItem
Layout.fillWidth: true width: parent.width
Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginS * scaling * 2 height: 32 * scaling
radius: Style.radiusS * scaling radius: Style.radiusS * scaling
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
readonly property bool selected: index === currentTabIndex readonly property bool selected: index === currentTabIndex
@ -396,19 +382,17 @@ NPanel {
} }
RowLayout { RowLayout {
id: tabEntryRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Tab icon on the left side
// Tab icon
NIcon { NIcon {
text: modelData.icon text: modelData.icon
color: tabTextColor color: tabTextColor
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
} }
// Tab label on the left side
// Tab label
NText { NText {
text: modelData.label text: modelData.label
color: tabTextColor color: tabTextColor
@ -417,7 +401,6 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
@ -429,19 +412,14 @@ NPanel {
} }
} }
} }
Item {
Layout.fillHeight: true
}
} }
} }
// Content pane // Content
Rectangle { Rectangle {
id: contentPane id: contentPane
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
border.color: Color.mOutline border.color: Color.mOutline
@ -454,23 +432,19 @@ NPanel {
anchors.margins: Style.marginL * scaling anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Header row
RowLayout { RowLayout {
id: headerRow id: headerRow
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Tab title // Tab label on the main right side
NText { NText {
text: root.tabsModel[currentTabIndex]?.label || "" text: root.tabsModel[currentTabIndex].label
font.pointSize: Style.fontSizeXL * scaling font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mPrimary color: Color.mPrimary
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
} }
// Close button
NIconButton { NIconButton {
icon: "close" icon: "close"
tooltipText: "Close" tooltipText: "Close"
@ -479,16 +453,13 @@ NPanel {
} }
} }
// Divider
NDivider { NDivider {
Layout.fillWidth: true Layout.fillWidth: true
} }
// Tab content area Item {
Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
color: Color.transparent
Repeater { Repeater {
model: root.tabsModel model: root.tabsModel
@ -506,16 +477,11 @@ NPanel {
} }
} }
sourceComponent: Flickable { sourceComponent: ColumnLayout {
// Using a Flickable here with a pressDelay to fix conflict between
// ScrollView and NTextInput. This fixes the weird text selection issue.
id: flickable
anchors.fill: parent
pressDelay: 200
ScrollView { ScrollView {
id: scrollView id: scrollView
anchors.fill: parent Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling padding: Style.marginL * scaling
@ -527,7 +493,7 @@ NPanel {
Loader { Loader {
active: true active: true
sourceComponent: root.tabsModel[index]?.source sourceComponent: root.tabsModel[index].source
width: scrollView.availableWidth width: scrollView.availableWidth
} }
} }
@ -539,5 +505,4 @@ NPanel {
} }
} }
} }
}
} }

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,7 +5,15 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
ColumnLayout { ScrollView {
id: root
property real scaling: 1.0
contentWidth: contentColumn.width
contentHeight: contentColumn.height
ColumnLayout {
id: contentColumn id: contentColumn
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
width: root.width width: root.width
@ -73,7 +81,7 @@ ColumnLayout {
// Info section // Info section
ColumnLayout { ColumnLayout {
spacing: Style.marginM * scaling spacing: Style.marginS * scaling
Layout.fillWidth: true Layout.fillWidth: true
NLabel { NLabel {
@ -87,4 +95,5 @@ ColumnLayout {
} }
} }
} }
}
} }

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,63 +115,33 @@ 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 { ColumnLayout {
visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys( Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys(
NetworkService.networks).length === 0 NetworkService.networks).length === 0
anchors.fill: parent spacing: Style.marginM * scaling
spacing: Style.marginL * scaling
Item {
Layout.fillHeight: true
}
NBusyIndicator { NBusyIndicator {
running: true running: true
@ -173,131 +151,139 @@ NPanel {
} }
NText { NText {
text: "Searching for nearby networks..." text: "Scanning for networks..."
font.pointSize: Style.fontSizeNormal * scaling font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
Item {
Layout.fillHeight: true
}
} }
// Networks list container // WiFi disabled state
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
spacing: Style.marginM * scaling 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 // Network list
Repeater { Repeater {
model: { model: {
if (!Settings.data.network.wifiEnabled) if (!Settings.data.network.wifiEnabled || NetworkService.isLoading)
return [] return []
// Sort networks: connected first, then by signal strength
const nets = Object.values(NetworkService.networks) const nets = Object.values(NetworkService.networks)
return nets.sort((a, b) => { return nets.sort((a, b) => {
if (a.connected !== b.connected) if (a.connected && !b.connected)
return b.connected - a.connected return -1
if (!a.connected && b.connected)
return 1
return b.signal - a.signal return b.signal - a.signal
}) })
} }
Rectangle { Item {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) implicitHeight: networkRect.implicitHeight
Rectangle {
id: networkRect
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
} }
NText {
text: {
const security = modelData.security
&& modelData.security !== "--" ? modelData.security : "Open"
const signal = `${modelData.signal}%`
return `${signal} ${security}`
}
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
}
// Right-aligned items container
RowLayout { RowLayout {
spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
spacing: Style.marginS * scaling
NText { // Connected badge
text: `${modelData.signal}%`
font.pointSize: Style.fontSizeXXS * scaling
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 { Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid visible: modelData.connected
color: Color.mPrimary color: Color.mPrimary
radius: height * 0.5 radius: width * 0.5
width: connectedText.implicitWidth + (Style.marginS * scaling * 2) width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2)
height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2) height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2)
NText { NText {
id: connectedText id: connectedLabel
anchors.centerIn: parent anchors.centerIn: parent
text: "Connected" text: "Connected"
font.pointSize: Style.fontSizeXXS * scaling font.pointSize: Style.fontSizeXXS * scaling
@ -305,166 +291,125 @@ NPanel {
} }
} }
Rectangle { // Saved badge - clickable
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 { Rectangle {
visible: modelData.cached && !modelData.connected visible: modelData.cached && !modelData.connected
&& NetworkService.forgettingNetwork !== modelData.ssid color: Color.mSurfaceVariant
&& NetworkService.disconnectingFrom !== modelData.ssid radius: width * 0.5
color: Color.transparent width: savedLabel.implicitWidth + (Style.marginS * scaling * 2)
height: savedLabel.implicitHeight + (Style.marginXS * scaling * 2)
border.color: Color.mOutline border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * scaling * 2) MouseArea {
height: savedText.implicitHeight + (Style.marginXXS * scaling * 2) 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 { NText {
id: savedText id: savedLabel
anchors.centerIn: parent anchors.centerIn: parent
text: "Saved" text: "Saved"
font.pointSize: Style.fontSizeXXS * scaling font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
} }
} }
}
}
// Action area
RowLayout {
spacing: Style.marginS * scaling
// Loading indicator
NBusyIndicator { NBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid visible: NetworkService.connectingSsid === modelData.ssid
|| NetworkService.disconnectingFrom === modelData.ssid running: NetworkService.connectingSsid === modelData.ssid
|| NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary color: Color.mPrimary
size: Style.baseWidgetSize * 0.5 * scaling size: Style.baseWidgetSize * 0.6 * scaling
} }
NIconButton { // Action buttons
visible: (modelData.existing || modelData.cached) && !modelData.connected RowLayout {
&& NetworkService.connectingTo !== modelData.ssid spacing: Style.marginXS * scaling
&& NetworkService.forgettingNetwork !== modelData.ssid visible: NetworkService.connectingSsid !== modelData.ssid
&& NetworkService.disconnectingFrom !== modelData.ssid
icon: "delete"
tooltipText: "Forget network"
sizeRatio: 0.7
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
}
NButton { NButton {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt)
&& 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 outlined: !hovered
fontSize: Style.fontSizeXS * scaling fontSize: Style.fontSizeXS * scaling
text: modelData.existing ? "Connect" : (NetworkService.isSecured(
modelData.security) ? "Password" : "Connect")
onClicked: { onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { if (modelData.existing || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid) NetworkService.connectNetwork(modelData.ssid, modelData.security)
} else { } else {
passwordSsid = modelData.ssid expandedNetwork = modelData.ssid
passwordPromptSsid = modelData.ssid
showPasswordPrompt = true
passwordInput = "" passwordInput = ""
expandedSsid = "" Qt.callLater(() => passwordInputField.forceActiveFocus())
} }
} }
} }
NButton { NButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid visible: modelData.connected
text: "Disconnect"
outlined: !hovered outlined: !hovered
fontSize: Style.fontSizeXS * scaling fontSize: Style.fontSizeXS * scaling
backgroundColor: Color.mError backgroundColor: Color.mError
onClicked: NetworkService.disconnect(modelData.ssid) 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,76 +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 {
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
&& NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mOutline
RowLayout { RowLayout {
id: forgetRow visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid
anchors.fill: parent && !showPasswordPrompt
anchors.margins: Style.marginS * scaling Layout.fillWidth: true
spacing: Style.marginM * scaling Layout.topMargin: Style.marginXS * scaling
spacing: Style.marginS * scaling
RowLayout {
NIcon { Item {
text: "delete_outline"
font.pointSize: Style.fontSizeL * scaling
color: Color.mError
}
NText {
text: "Forget this network?"
font.pointSize: Style.fontSizeS * scaling
color: Color.mError
Layout.fillWidth: true Layout.fillWidth: true
}
} }
NButton { NButton {
id: forgetButton id: forgetButton
text: "Forget" text: "Forget Network"
icon: "delete_outline"
fontSize: Style.fontSizeXXS * scaling fontSize: Style.fontSizeXXS * scaling
backgroundColor: Color.mError backgroundColor: Color.mError
outlined: forgetButton.hovered ? false : true textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary
outlined: !forgetButton.hovered
Layout.preferredHeight: 28 * scaling
onClicked: { onClicked: {
NetworkService.forget(modelData.ssid) NetworkService.forgetNetwork(modelData.ssid)
expandedSsid = "" expandedNetwork = ""
}
}
NIconButton {
icon: "close"
sizeRatio: 0.8
onClicked: expandedSsid = ""
}
} }
} }
} }
@ -558,20 +483,17 @@ NPanel {
} }
} }
// Empty state when no networks // No networks found
ColumnLayout { ColumnLayout {
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys( Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys(
NetworkService.networks).length === 0 NetworkService.networks).length === 0
anchors.fill: parent spacing: Style.marginM * scaling
spacing: Style.marginL * scaling
Item {
Layout.fillHeight: true
}
NIcon { NIcon {
text: "wifi_find" text: "wifi_find"
font.pointSize: 64 * scaling font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
@ -584,14 +506,11 @@ NPanel {
} }
NButton { NButton {
text: "Scan again" text: "Refresh"
icon: "refresh" icon: "refresh"
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan() onClicked: NetworkService.refreshNetworks()
} }
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()
onLoaded: {
Logger.log("Network", "Loaded network cache from disk")
// Try to auto-connect on startup if WiFi is enabled
if (Settings.data.network.wifiEnabled && adapter.lastConnected) {
autoConnectTimer.start()
}
}
onLoadFailed: function (error) {
Logger.log("Network", "No existing cache found, creating new one")
// Initialize with empty data
adapter.knownNetworks = ({})
adapter.lastConnected = ""
}
JsonAdapter { JsonAdapter {
id: cacheAdapter id: adapter
property var knownNetworks: ({}) property var knownNetworks: ({})
property string lastConnected: "" property string lastConnected: ""
} property int lastRefresh: 0
onLoadFailed: {
cacheAdapter.knownNetworks = ({})
cacheAdapter.lastConnected = ""
} }
} }
Connections { // Save timer to batch writes
target: Settings.data.network
function onWifiEnabledChanged() {
if (Settings.data.network.wifiEnabled) {
ToastService.showNotice("Wi-Fi", "Enabled")
} else {
ToastService.showNotice("Wi-Fi", "Disabled")
}
}
}
Component.onCompleted: {
Logger.log("Network", "Service initialized")
syncWifiState()
refresh()
}
// Save cache with debounce
Timer { Timer {
id: 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
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
}
}
} }
} }
Process { stderr: StdioCollector {
id: wifiStateProcess
running: false
command: ["nmcli", "radio", "wifi"]
stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const enabled = text.trim() === "enabled" if (text.trim()) {
Logger.log("Network", "Wi-Fi enabled:", enabled) if (text.includes("no such connection profile")) {
if (Settings.data.network.wifiEnabled !== enabled) { Logger.log("Network", `Network profile not found: ${forgetProcess.ssid}`)
Settings.data.network.wifiEnabled = enabled } else {
Logger.warn("Network", `Error forgetting network: ${text}`)
}
refreshNetworks()
} }
} }
} }
} }
// WiFi enable/disable functions
function setWifiEnabled(enabled) {
if (enabled) {
isLoading = true
wifiRadioProcess.action = "on"
wifiRadioProcess.running = true
} else {
// Save current connection for later
for (const ssid in networks) {
if (networks[ssid].connected) {
adapter.lastConnected = ssid
saveTimer.restart()
disconnectNetwork(ssid)
break
}
}
wifiRadioProcess.action = "off"
wifiRadioProcess.running = true
}
}
// Unified WiFi radio control
Process { Process {
id: 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") {
return ["nmcli", "connection", "up", "id", ssid]
} else {
const cmd = ["nmcli", "device", "wifi", "connect", ssid] const cmd = ["nmcli", "device", "wifi", "connect", ssid]
if (password) { if (isSecured && password) {
cmd.push("password", 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()
} }
root.forgettingNetwork = "" // 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
}
}
}
// Quick scan to verify the profile is gone root.networks = networksMap
delayedScanTimer.interval = 500 root.isLoading = false
delayedScanTimer.restart() scanProcess.existingProfiles = {}
//Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`)
} }
} }
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,22 +165,14 @@ 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
if (!isShowingToast) {
processQueue() processQueue()
} }
}
// Process the message queue // Process the message queue
function processQueue() { function processQueue() {
@ -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,12 +21,32 @@ RowLayout {
// Internal properties // Internal properties
property real scaling: 1.0 property real scaling: 1.0
// Label
NText {
text: root.label
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
// Description
NText {
text: root.description
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.Wrap
Layout.fillWidth: true
}
// Input and button row
RowLayout {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Layout.fillWidth: true
NTextInput { NTextInput {
id: textInput id: textInput
label: root.label
description: root.description
placeholderText: root.placeholderText placeholderText: root.placeholderText
text: root.text text: root.text
onEditingFinished: { onEditingFinished: {
@ -37,10 +56,11 @@ RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
} }
NButton { Item {
Layout.fillWidth: false Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom }
NButton {
text: root.actionButtonText text: root.actionButtonText
icon: root.actionButtonIcon icon: root.actionButtonIcon
backgroundColor: Color.mSecondary backgroundColor: Color.mSecondary
@ -48,9 +68,10 @@ RowLayout {
hoverColor: Color.mTertiary hoverColor: Color.mTertiary
pressColor: Color.mPrimary pressColor: Color.mPrimary
enabled: root.actionButtonEnabled enabled: root.actionButtonEnabled
Layout.fillWidth: false
onClicked: { onClicked: {
root.actionClicked() 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

@ -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)
// Focus ring
Rectangle {
anchors.fill: parent
radius: frame.radius
color: Color.transparent
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: Style.animationFast 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
Layout.fillWidth: true
echoMode: TextInput.Normal echoMode: TextInput.Normal
readOnly: root.readOnly readOnly: root.readOnly
enabled: root.enabled enabled: root.enabled
color: Color.mOnSurface color: Color.mOnSurface
placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6)
selectByMouse: true
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
background: null background: null
font.family: fontFamily
font.family: root.fontFamily font.pointSize: fontSize
font.pointSize: root.fontSize font.weight: fontWeight
font.weight: root.fontWeight
onEditingFinished: root.editingFinished() 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