Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell
This commit is contained in:
commit
835f88d71e
17 changed files with 890 additions and 283 deletions
|
|
@ -60,6 +60,7 @@ Singleton {
|
||||||
property int animationFast: Math.round(150 * Settings.data.general.animationSpeed)
|
property int animationFast: Math.round(150 * Settings.data.general.animationSpeed)
|
||||||
property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed)
|
property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed)
|
||||||
property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed)
|
property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed)
|
||||||
|
property int animationSlowest: Math.round(750 * Settings.data.general.animationSpeed)
|
||||||
|
|
||||||
// Dimensions
|
// Dimensions
|
||||||
property int barHeight: 36
|
property int barHeight: 36
|
||||||
|
|
|
||||||
|
|
@ -65,40 +65,17 @@ Item {
|
||||||
|
|
||||||
// Test mode
|
// Test mode
|
||||||
property bool testMode: false
|
property bool testMode: false
|
||||||
property int testPercent: 20
|
property int testPercent: 50
|
||||||
property bool testCharging: false
|
property bool testCharging: true
|
||||||
property var battery: UPower.displayDevice
|
property var battery: UPower.displayDevice
|
||||||
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||||
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||||
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||||
|
|
||||||
// Choose icon based on charge and charging state
|
|
||||||
function batteryIcon() {
|
|
||||||
if (!isReady || !battery.isLaptopBattery)
|
|
||||||
return "battery_android_alert"
|
|
||||||
if (charging)
|
|
||||||
return "battery_android_bolt"
|
|
||||||
if (percent >= 95)
|
|
||||||
return "battery_android_full"
|
|
||||||
// Hardcoded battery symbols
|
|
||||||
if (percent >= 85)
|
|
||||||
return "battery_android_6"
|
|
||||||
if (percent >= 70)
|
|
||||||
return "battery_android_5"
|
|
||||||
if (percent >= 55)
|
|
||||||
return "battery_android_4"
|
|
||||||
if (percent >= 40)
|
|
||||||
return "battery_android_3"
|
|
||||||
if (percent >= 25)
|
|
||||||
return "battery_android_2"
|
|
||||||
if (percent >= 10)
|
|
||||||
return "battery_android_1"
|
|
||||||
if (percent >= 0)
|
|
||||||
return "battery_android_0"
|
|
||||||
}
|
|
||||||
|
|
||||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||||
icon: batteryIcon()
|
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent,
|
||||||
|
charging, isReady)
|
||||||
|
iconRotation: -90
|
||||||
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
|
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
|
||||||
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
||||||
iconCircleColor: Color.mPrimary
|
iconCircleColor: Color.mPrimary
|
||||||
|
|
@ -109,30 +86,30 @@ Item {
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
let lines = []
|
let lines = []
|
||||||
if (testMode) {
|
if (testMode) {
|
||||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
|
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
if (!isReady || !battery.isLaptopBattery) {
|
if (!isReady || !battery.isLaptopBattery) {
|
||||||
return "No battery detected"
|
return "No battery detected."
|
||||||
}
|
}
|
||||||
if (battery.timeToEmpty > 0) {
|
if (battery.timeToEmpty > 0) {
|
||||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
|
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
|
||||||
}
|
}
|
||||||
if (battery.timeToFull > 0) {
|
if (battery.timeToFull > 0) {
|
||||||
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
|
lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`)
|
||||||
}
|
}
|
||||||
if (battery.changeRate !== undefined) {
|
if (battery.changeRate !== undefined) {
|
||||||
const rate = battery.changeRate
|
const rate = battery.changeRate
|
||||||
if (rate > 0) {
|
if (rate > 0) {
|
||||||
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
|
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(
|
||||||
2) + " W")
|
2) + " W.")
|
||||||
} else if (rate < 0) {
|
} else if (rate < 0) {
|
||||||
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W")
|
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.")
|
||||||
} else {
|
} else {
|
||||||
lines.push("Estimating...")
|
lines.push("Estimating...")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(charging ? "Charging" : "Discharging")
|
lines.push(charging ? "Charging." : "Discharging.")
|
||||||
}
|
}
|
||||||
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
|
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
|
||||||
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
|
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
|
||||||
|
|
|
||||||
|
|
@ -37,28 +37,26 @@ Item {
|
||||||
target: getMonitor()
|
target: getMonitor()
|
||||||
ignoreUnknownSignals: true
|
ignoreUnknownSignals: true
|
||||||
function onBrightnessUpdated() {
|
function onBrightnessUpdated() {
|
||||||
Logger.log("Bar-Brightness", "OnBrightnessUpdated")
|
// Ignore if this is the first time we receive an update.
|
||||||
var monitor = getMonitor()
|
// Most likely service just kicked off.
|
||||||
if (!monitor)
|
|
||||||
return
|
|
||||||
var currentBrightness = monitor.brightness
|
|
||||||
|
|
||||||
// Ignore if this is the first time or if brightness hasn't actually changed
|
|
||||||
if (!firstBrightnessReceived) {
|
if (!firstBrightnessReceived) {
|
||||||
firstBrightnessReceived = true
|
firstBrightnessReceived = true
|
||||||
monitor.lastBrightness = currentBrightness
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show pill if brightness actually changed (not just loaded from settings)
|
pill.show()
|
||||||
if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) {
|
hideTimerAfterChange.restart()
|
||||||
pill.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
monitor.lastBrightness = currentBrightness
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: hideTimerAfterChange
|
||||||
|
interval: 2500
|
||||||
|
running: false
|
||||||
|
repeat: false
|
||||||
|
onTriggered: pill.hide()
|
||||||
|
}
|
||||||
|
|
||||||
NPill {
|
NPill {
|
||||||
id: pill
|
id: pill
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ RowLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: `${SystemStatService.memoryUsageGb}G`
|
text: `${SystemStatService.memGb}G`
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,6 @@ NIconButton {
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -30,7 +22,7 @@ NIconButton {
|
||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
try {
|
try {
|
||||||
if (NetworkService.ethernet) {
|
if (NetworkService.ethernetConnected) {
|
||||||
return "lan"
|
return "lan"
|
||||||
}
|
}
|
||||||
let connected = false
|
let connected = false
|
||||||
|
|
@ -44,10 +36,10 @@ NIconButton {
|
||||||
}
|
}
|
||||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error("WiFi", "Error getting icon:", error)
|
Logger.error("Wi-Fi", "Error getting icon:", error)
|
||||||
return "signal_wifi_bad"
|
return "signal_wifi_bad"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltipText: "Network / Wi-Fi."
|
tooltipText: "Manage Wi-Fi."
|
||||||
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
|
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,24 +34,28 @@ Variants {
|
||||||
|
|
||||||
WlrLayershell.namespace: "noctalia-dock"
|
WlrLayershell.namespace: "noctalia-dock"
|
||||||
|
|
||||||
property bool autoHide: Settings.data.dock.autoHide
|
readonly property bool autoHide: Settings.data.dock.autoHide
|
||||||
property bool hidden: autoHide
|
readonly property int hideDelay: 500
|
||||||
property int hideDelay: 500
|
readonly property int showDelay: 100
|
||||||
property int showDelay: 100
|
readonly property int hideAnimationDuration: Style.animationFast
|
||||||
property int hideAnimationDuration: Style.animationFast
|
readonly property int showAnimationDuration: Style.animationFast
|
||||||
property int showAnimationDuration: Style.animationFast
|
readonly property int peekHeight: 7 * scaling
|
||||||
property int peekHeight: 7 * scaling
|
readonly property int fullHeight: dockContainer.height
|
||||||
property int fullHeight: dockContainer.height
|
readonly property int iconSize: 36 * scaling
|
||||||
property int iconSize: 36
|
readonly property int floatingMargin: 12 * scaling // Margin to make dock float
|
||||||
|
|
||||||
// Bar positioning properties
|
// Bar detection and positioning properties
|
||||||
property bool barAtBottom: Settings.data.bar.position === "bottom"
|
readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name)
|
||||||
property int barHeight: barAtBottom ? (Settings.data.bar.height || 30) * scaling : 0
|
|| (Settings.data.bar.monitors.length === 0)) : false
|
||||||
property int dockSpacing: 4 * scaling // Space between dock and bar
|
readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom"
|
||||||
|
readonly property bool barAtTop: hasBar && Settings.data.bar.position === "top"
|
||||||
|
readonly property int barHeight: (barAtBottom || barAtTop) ? (Settings.data.bar.height || 30) * scaling : 0
|
||||||
|
readonly property int dockSpacing: 8 * scaling // Space between dock and bar/edge
|
||||||
|
|
||||||
// Track hover state
|
// Track hover state
|
||||||
property bool dockHovered: false
|
property bool dockHovered: false
|
||||||
property bool anyAppHovered: false
|
property bool anyAppHovered: false
|
||||||
|
property bool hidden: autoHide
|
||||||
|
|
||||||
// Dock is positioned at the bottom
|
// Dock is positioned at the bottom
|
||||||
anchors.bottom: true
|
anchors.bottom: true
|
||||||
|
|
@ -63,11 +67,11 @@ Variants {
|
||||||
// Make the window transparent
|
// Make the window transparent
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
|
|
||||||
// Set the window size - always include space for peek area when auto-hide is enabled
|
// Set the window size - include extra height only if bar is at bottom
|
||||||
implicitWidth: dockContainer.width
|
implicitWidth: dockContainer.width + (floatingMargin * 2)
|
||||||
implicitHeight: fullHeight + (barAtBottom ? barHeight + dockSpacing : 0)
|
implicitHeight: fullHeight + floatingMargin + (barAtBottom ? barHeight + dockSpacing : 0)
|
||||||
|
|
||||||
// Position the entire window above the bar when bar is at bottom
|
// Position the entire window above the bar only when bar is at bottom
|
||||||
margins.bottom: barAtBottom ? barHeight : 0
|
margins.bottom: barAtBottom ? barHeight : 0
|
||||||
|
|
||||||
// Watch for autoHide setting changes
|
// Watch for autoHide setting changes
|
||||||
|
|
@ -111,7 +115,7 @@ Variants {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
height: peekHeight + dockSpacing
|
height: peekHeight + floatingMargin + (barAtBottom ? dockSpacing : 0)
|
||||||
hoverEnabled: autoHide
|
hoverEnabled: autoHide
|
||||||
visible: autoHide
|
visible: autoHide
|
||||||
|
|
||||||
|
|
@ -130,24 +134,32 @@ Variants {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: dockContainer
|
id: dockContainer
|
||||||
width: dockLayout.implicitWidth + 48 * scaling
|
width: dockLayout.implicitWidth + Style.marginL * scaling * 2
|
||||||
height: iconSize * 1.4 * scaling
|
height: Math.round(iconSize * 1.6)
|
||||||
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.bottomMargin: dockSpacing
|
anchors.bottomMargin: floatingMargin + (barAtBottom ? dockSpacing : 0)
|
||||||
topLeftRadius: Style.radiusL * scaling
|
radius: Style.radiusL * scaling
|
||||||
topRightRadius: Style.radiusL * scaling
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
border.color: Color.mOutline
|
||||||
|
|
||||||
// Animate the dock sliding up and down
|
// Fade and zoom animation properties
|
||||||
transform: Translate {
|
opacity: hidden ? 0 : 1
|
||||||
y: hidden ? (fullHeight - peekHeight) : 0
|
scale: hidden ? 0.85 : 1
|
||||||
|
|
||||||
Behavior on y {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: hidden ? hideAnimationDuration : showAnimationDuration
|
duration: hidden ? hideAnimationDuration : showAnimationDuration
|
||||||
easing.type: Easing.InOutQuad
|
easing.type: Easing.InOutQuad
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: hidden ? hideAnimationDuration : showAnimationDuration
|
||||||
|
easing.type: hidden ? Easing.InQuad : Easing.OutBack
|
||||||
|
easing.overshoot: hidden ? 0 : 1.05
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,15 +191,9 @@ Variants {
|
||||||
Item {
|
Item {
|
||||||
id: dock
|
id: dock
|
||||||
width: dockLayout.implicitWidth
|
width: dockLayout.implicitWidth
|
||||||
height: parent.height - (20 * scaling)
|
height: parent.height - (Style.marginM * 2 * scaling)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
NTooltip {
|
|
||||||
id: appTooltip
|
|
||||||
visible: false
|
|
||||||
positionAbove: true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAppIcon(toplevel: Toplevel): string {
|
function getAppIcon(toplevel: Toplevel): string {
|
||||||
if (!toplevel)
|
if (!toplevel)
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -203,39 +209,48 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ToplevelManager ? ToplevelManager.toplevels : null
|
model: ToplevelManager ? ToplevelManager.toplevels : null
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Item {
|
||||||
id: appButton
|
id: appButton
|
||||||
Layout.preferredWidth: iconSize * scaling
|
Layout.preferredWidth: iconSize
|
||||||
Layout.preferredHeight: iconSize * scaling
|
Layout.preferredHeight: iconSize
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
|
||||||
color: Color.transparent
|
|
||||||
radius: Style.radiusM * scaling
|
|
||||||
|
|
||||||
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
|
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
|
||||||
property bool hovered: appMouseArea.containsMouse
|
property bool hovered: appMouseArea.containsMouse
|
||||||
property string appId: modelData ? modelData.appId : ""
|
property string appId: modelData ? modelData.appId : ""
|
||||||
property string appTitle: modelData ? modelData.title : ""
|
property string appTitle: modelData ? modelData.title : ""
|
||||||
|
|
||||||
// The icon
|
// Individual tooltip for this app
|
||||||
|
NTooltip {
|
||||||
|
id: appTooltip
|
||||||
|
target: appButton
|
||||||
|
positionAbove: true
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The icon with better quality settings
|
||||||
Image {
|
Image {
|
||||||
id: appIcon
|
id: appIcon
|
||||||
width: iconSize * scaling
|
width: iconSize
|
||||||
height: iconSize * scaling
|
height: iconSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
source: dock.getAppIcon(modelData)
|
source: dock.getAppIcon(modelData)
|
||||||
visible: source.toString() !== ""
|
visible: source.toString() !== ""
|
||||||
|
sourceSize.width: iconSize * 2
|
||||||
|
sourceSize.height: iconSize * 2
|
||||||
smooth: true
|
smooth: true
|
||||||
mipmap: false
|
mipmap: true
|
||||||
antialiasing: false
|
antialiasing: true
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
|
cache: true
|
||||||
|
|
||||||
scale: appButton.hovered ? 1.1 : 1.0
|
scale: appButton.hovered ? 1.15 : 1.0
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Style.animationFast
|
duration: Style.animationNormal
|
||||||
easing.type: Easing.OutBack
|
easing.type: Easing.OutBack
|
||||||
|
easing.overshoot: 1.2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,15 +261,15 @@ Variants {
|
||||||
visible: !appIcon.visible
|
visible: !appIcon.visible
|
||||||
text: "question_mark"
|
text: "question_mark"
|
||||||
font.family: "Material Symbols Rounded"
|
font.family: "Material Symbols Rounded"
|
||||||
font.pointSize: iconSize * 0.7 * scaling
|
font.pointSize: iconSize * 0.7
|
||||||
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||||
|
scale: appButton.hovered ? 1.15 : 1.0
|
||||||
scale: appButton.hovered ? 1.1 : 1.0
|
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Style.animationFast
|
duration: Style.animationFast
|
||||||
easing.type: Easing.OutBack
|
easing.type: Easing.OutBack
|
||||||
|
easing.overshoot: 1.2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -270,7 +285,6 @@ Variants {
|
||||||
anyAppHovered = true
|
anyAppHovered = true
|
||||||
const appName = appButton.appTitle || appButton.appId || "Unknown"
|
const appName = appButton.appTitle || appButton.appId || "Unknown"
|
||||||
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
|
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
|
||||||
appTooltip.target = appButton
|
|
||||||
appTooltip.isVisible = true
|
appTooltip.isVisible = true
|
||||||
if (autoHide) {
|
if (autoHide) {
|
||||||
showTimer.stop()
|
showTimer.stop()
|
||||||
|
|
@ -300,15 +314,32 @@ Variants {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Active indicator
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: isActive
|
visible: isActive
|
||||||
width: iconSize * 0.25
|
width: iconSize * 0.2
|
||||||
height: 4 * scaling
|
height: iconSize * 0.1
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
radius: Style.radiusXS
|
radius: Style.radiusXS * scaling
|
||||||
anchors.top: parent.bottom
|
anchors.top: parent.bottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.topMargin: Style.marginXXS * scaling
|
anchors.topMargin: Style.marginXXS * 1.5 * scaling
|
||||||
|
|
||||||
|
// Pulse animation for active indicator
|
||||||
|
SequentialAnimation on opacity {
|
||||||
|
running: isActive
|
||||||
|
loops: Animation.Infinite
|
||||||
|
NumberAnimation {
|
||||||
|
to: 0.6
|
||||||
|
duration: Style.animationSlowest
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
to: 1.0
|
||||||
|
duration: Style.animationSlowest
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,29 +58,6 @@ Loader {
|
||||||
property real percent: isReady ? (battery.percentage * 100) : 0
|
property real percent: isReady ? (battery.percentage * 100) : 0
|
||||||
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
|
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
|
||||||
property bool batteryVisible: isReady && percent > 0
|
property bool batteryVisible: isReady && percent > 0
|
||||||
|
|
||||||
function getIcon() {
|
|
||||||
if (!batteryVisible)
|
|
||||||
return ""
|
|
||||||
if (charging)
|
|
||||||
return "battery_android_bolt"
|
|
||||||
if (percent >= 95)
|
|
||||||
return "battery_android_full"
|
|
||||||
if (percent >= 85)
|
|
||||||
return "battery_android_6"
|
|
||||||
if (percent >= 70)
|
|
||||||
return "battery_android_5"
|
|
||||||
if (percent >= 55)
|
|
||||||
return "battery_android_4"
|
|
||||||
if (percent >= 40)
|
|
||||||
return "battery_android_3"
|
|
||||||
if (percent >= 25)
|
|
||||||
return "battery_android_2"
|
|
||||||
if (percent >= 10)
|
|
||||||
return "battery_android_1"
|
|
||||||
if (percent >= 0)
|
|
||||||
return "battery_android_0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -420,7 +397,7 @@ Loader {
|
||||||
anchors.bottomMargin: Style.marginM * scaling
|
anchors.bottomMargin: Style.marginM * scaling
|
||||||
anchors.leftMargin: Style.marginL * scaling
|
anchors.leftMargin: Style.marginL * scaling
|
||||||
anchors.rightMargin: Style.marginL * scaling
|
anchors.rightMargin: Style.marginL * scaling
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "SECURE TERMINAL"
|
text: "SECURE TERMINAL"
|
||||||
|
|
@ -431,23 +408,6 @@ Loader {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
visible: batteryIndicator.batteryVisible
|
|
||||||
NIcon {
|
|
||||||
text: batteryIndicator.getIcon()
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
|
||||||
}
|
|
||||||
NText {
|
|
||||||
text: Math.round(batteryIndicator.percent) + "%"
|
|
||||||
color: Color.mOnSurface
|
|
||||||
font.family: Settings.data.ui.fontFixed
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
NText {
|
NText {
|
||||||
|
|
@ -463,6 +423,25 @@ Loader {
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
visible: batteryIndicator.batteryVisible
|
||||||
|
NIcon {
|
||||||
|
text: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging,
|
||||||
|
batteryIndicator.isReady)
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
||||||
|
rotation: -90
|
||||||
|
}
|
||||||
|
NText {
|
||||||
|
text: Math.round(batteryIndicator.percent) + "%"
|
||||||
|
color: Color.mOnSurface
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,7 @@ NPanel {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
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 * scaling
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: sections
|
id: sections
|
||||||
|
|
@ -398,7 +398,8 @@ NPanel {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: tabEntryRow
|
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
|
// Tab icon
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,14 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "WiFi Enabled"
|
label: "Enable Wi-Fi"
|
||||||
description: "Enable WiFi connectivity."
|
description: "Enable Wi-Fi connectivity."
|
||||||
checked: Settings.data.network.wifiEnabled
|
checked: Settings.data.network.wifiEnabled
|
||||||
onToggled: checked => {
|
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||||
Settings.data.network.wifiEnabled = checked
|
|
||||||
NetworkService.setWifiEnabled(checked)
|
|
||||||
if (checked) {
|
|
||||||
ToastService.showNotice("WiFi", "Enabled")
|
|
||||||
} else {
|
|
||||||
ToastService.showNotice("WiFi", "Disabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Bluetooth Enabled"
|
label: "Enable Bluetooth"
|
||||||
description: "Enable Bluetooth connectivity."
|
description: "Enable Bluetooth connectivity."
|
||||||
checked: Settings.data.network.bluetoothEnabled
|
checked: Settings.data.network.bluetoothEnabled
|
||||||
onToggled: checked => {
|
onToggled: checked => {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ NBox {
|
||||||
height: 68 * scaling
|
height: 68 * scaling
|
||||||
}
|
}
|
||||||
NCircleStat {
|
NCircleStat {
|
||||||
value: SystemStatService.memoryUsagePer
|
value: SystemStatService.memPercent
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
flat: true
|
flat: true
|
||||||
contentScale: 0.8
|
contentScale: 0.8
|
||||||
|
|
@ -48,7 +48,7 @@ NBox {
|
||||||
height: 68 * scaling
|
height: 68 * scaling
|
||||||
}
|
}
|
||||||
NCircleStat {
|
NCircleStat {
|
||||||
value: SystemStatService.diskUsage
|
value: SystemStatService.diskPercent
|
||||||
icon: "hard_drive"
|
icon: "hard_drive"
|
||||||
flat: true
|
flat: true
|
||||||
contentScale: 0.8
|
contentScale: 0.8
|
||||||
|
|
|
||||||
|
|
@ -11,71 +11,92 @@ NPanel {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
panelWidth: 460 * scaling
|
panelWidth: 460 * scaling
|
||||||
panelHeight: 708 * scaling
|
panelHeight: contentHeight
|
||||||
|
|
||||||
|
// Default height, will be modified via binding when the content is fully loaded
|
||||||
|
property real contentHeight: 720 * scaling
|
||||||
|
|
||||||
panelContent: Item {
|
panelContent: Item {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
property real cardSpacing: Style.marginL * scaling
|
property real cardSpacing: Style.marginL * scaling
|
||||||
|
|
||||||
anchors.left: parent.left
|
width: root.panelWidth
|
||||||
anchors.right: parent.right
|
implicitHeight: layout.implicitHeight + (2 * cardSpacing)
|
||||||
anchors.top: parent.top
|
height: implicitHeight
|
||||||
anchors.margins: content.cardSpacing
|
|
||||||
implicitHeight: layout.implicitHeight
|
|
||||||
|
|
||||||
// Layout content (not vertically anchored so implicitHeight is valid)
|
// Update parent's contentHeight whenever our height changes
|
||||||
|
onHeightChanged: {
|
||||||
|
root.contentHeight = height
|
||||||
|
}
|
||||||
|
|
||||||
|
onImplicitHeightChanged: {
|
||||||
|
if (implicitHeight > 0) {
|
||||||
|
root.contentHeight = implicitHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout content
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
// Use the same spacing value horizontally and vertically
|
x: content.cardSpacing
|
||||||
anchors.left: parent.left
|
y: content.cardSpacing
|
||||||
anchors.right: parent.right
|
width: parent.width - (2 * content.cardSpacing)
|
||||||
anchors.top: parent.top
|
|
||||||
spacing: content.cardSpacing
|
spacing: content.cardSpacing
|
||||||
|
|
||||||
// Cards (consistent inter-card spacing via ColumnLayout spacing)
|
// Cards (consistent inter-card spacing via ColumnLayout spacing)
|
||||||
ProfileCard {// Layout.topMargin: 0
|
ProfileCard {
|
||||||
// Layout.bottomMargin: 0
|
id: profileCard
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
WeatherCard {// Layout.topMargin: 0
|
|
||||||
// Layout.bottomMargin: 0
|
WeatherCard {
|
||||||
|
id: weatherCard
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middle section: media + stats column
|
// Middle section: media + stats column
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: middleRow
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 0
|
Layout.minimumHeight: 280 * scaling
|
||||||
Layout.bottomMargin: 0
|
Layout.preferredHeight: Math.max(280 * scaling, statsCard.implicitHeight)
|
||||||
spacing: content.cardSpacing
|
spacing: content.cardSpacing
|
||||||
|
|
||||||
// Media card
|
// Media card
|
||||||
MediaCard {
|
MediaCard {
|
||||||
id: mediaCard
|
id: mediaCard
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: statsCard.implicitHeight
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// System monitors combined in one card
|
// System monitors combined in one card
|
||||||
SystemMonitorCard {
|
SystemMonitorCard {
|
||||||
id: statsCard
|
id: statsCard
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom actions (two grouped rows of round buttons)
|
// Bottom actions (two grouped rows of round buttons)
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: bottomRow
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 0
|
Layout.minimumHeight: 60 * scaling
|
||||||
Layout.bottomMargin: 0
|
Layout.preferredHeight: Math.max(60 * scaling, powerProfilesCard.implicitHeight, utilitiesCard.implicitHeight)
|
||||||
spacing: content.cardSpacing
|
spacing: content.cardSpacing
|
||||||
|
|
||||||
// Power Profiles switcher
|
// Power Profiles switcher
|
||||||
PowerProfilesCard {
|
PowerProfilesCard {
|
||||||
|
id: powerProfilesCard
|
||||||
spacing: content.cardSpacing
|
spacing: content.cardSpacing
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilities buttons
|
// Utilities buttons
|
||||||
UtilitiesCard {
|
UtilitiesCard {
|
||||||
|
id: utilitiesCard
|
||||||
spacing: content.cardSpacing
|
spacing: content.cardSpacing
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "Scanning for networks..."
|
text: "Searching for nearby 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
|
||||||
|
|
@ -215,11 +215,23 @@ NPanel {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
|
implicitHeight: netColumn.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
|
||||||
|
|
||||||
|
// Smooth opacity animation
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: netColumn
|
id: netColumn
|
||||||
width: parent.width - (Style.marginM * scaling * 2)
|
width: parent.width - (Style.marginM * scaling * 2)
|
||||||
|
|
@ -276,8 +288,9 @@ NPanel {
|
||||||
Layout.preferredWidth: Style.marginXXS * scaling
|
Layout.preferredWidth: Style.marginXXS * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the status badges area (around line 237)
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: modelData.connected
|
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
radius: height * 0.5
|
radius: height * 0.5
|
||||||
width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
|
width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
|
||||||
|
|
@ -292,8 +305,42 @@ NPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: NetworkService.disconnectingFrom === modelData.ssid
|
||||||
|
color: Color.mError
|
||||||
|
radius: height * 0.5
|
||||||
|
width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2)
|
||||||
|
height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||||
|
|
||||||
|
NText {
|
||||||
|
id: disconnectingText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Disconnecting..."
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
|
color: Color.mOnPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: NetworkService.forgettingNetwork === modelData.ssid
|
||||||
|
color: Color.mError
|
||||||
|
radius: height * 0.5
|
||||||
|
width: forgettingText.implicitWidth + (Style.marginS * scaling * 2)
|
||||||
|
height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2)
|
||||||
|
|
||||||
|
NText {
|
||||||
|
id: forgettingText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Forgetting..."
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
|
color: Color.mOnPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: modelData.cached && !modelData.connected
|
visible: modelData.cached && !modelData.connected
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
|
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
border.color: Color.mOutline
|
border.color: Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
@ -318,6 +365,8 @@ NPanel {
|
||||||
|
|
||||||
NBusyIndicator {
|
NBusyIndicator {
|
||||||
visible: NetworkService.connectingTo === modelData.ssid
|
visible: NetworkService.connectingTo === modelData.ssid
|
||||||
|
|| NetworkService.disconnectingFrom === modelData.ssid
|
||||||
|
|| NetworkService.forgettingNetwork === modelData.ssid
|
||||||
running: visible
|
running: visible
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
size: Style.baseWidgetSize * 0.5 * scaling
|
size: Style.baseWidgetSize * 0.5 * scaling
|
||||||
|
|
@ -326,6 +375,8 @@ NPanel {
|
||||||
NIconButton {
|
NIconButton {
|
||||||
visible: (modelData.existing || modelData.cached) && !modelData.connected
|
visible: (modelData.existing || modelData.cached) && !modelData.connected
|
||||||
&& NetworkService.connectingTo !== modelData.ssid
|
&& NetworkService.connectingTo !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
|
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
icon: "delete"
|
icon: "delete"
|
||||||
tooltipText: "Forget network"
|
tooltipText: "Forget network"
|
||||||
sizeRatio: 0.7
|
sizeRatio: 0.7
|
||||||
|
|
@ -335,6 +386,8 @@ NPanel {
|
||||||
NButton {
|
NButton {
|
||||||
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid
|
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid
|
||||||
&& passwordSsid !== modelData.ssid
|
&& passwordSsid !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
|
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
text: {
|
text: {
|
||||||
if (modelData.existing || modelData.cached)
|
if (modelData.existing || modelData.cached)
|
||||||
return "Connect"
|
return "Connect"
|
||||||
|
|
@ -356,7 +409,7 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
NButton {
|
NButton {
|
||||||
visible: modelData.connected
|
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
text: "Disconnect"
|
text: "Disconnect"
|
||||||
outlined: !hovered
|
outlined: !hovered
|
||||||
fontSize: Style.fontSizeXS * scaling
|
fontSize: Style.fontSizeXS * scaling
|
||||||
|
|
@ -368,7 +421,8 @@ NPanel {
|
||||||
|
|
||||||
// Password input
|
// Password input
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: passwordSsid === modelData.ssid
|
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
|
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
@ -449,7 +503,8 @@ NPanel {
|
||||||
|
|
||||||
// Forget network
|
// Forget network
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: expandedSsid === modelData.ssid
|
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
|
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
@ -529,7 +584,7 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
NButton {
|
NButton {
|
||||||
text: "Scan Again"
|
text: "Scan again"
|
||||||
icon: "refresh"
|
icon: "refresh"
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
onClicked: NetworkService.scan()
|
onClicked: NetworkService.scan()
|
||||||
|
|
|
||||||
49
Services/BatteryService.qml
Normal file
49
Services/BatteryService.qml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Choose icon based on charge and charging state
|
||||||
|
function getIcon(percent, charging, isReady) {
|
||||||
|
if (!isReady) {
|
||||||
|
return "battery_error"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charging) {
|
||||||
|
if (percent >= 95)
|
||||||
|
return "battery_full"
|
||||||
|
if (percent >= 85)
|
||||||
|
return "battery_charging_90"
|
||||||
|
if (percent >= 65)
|
||||||
|
return "battery_charging_80"
|
||||||
|
if (percent >= 55)
|
||||||
|
return "battery_charging_60"
|
||||||
|
if (percent >= 45)
|
||||||
|
return "battery_charging_50"
|
||||||
|
if (percent >= 25)
|
||||||
|
return "battery_charging_30"
|
||||||
|
if (percent >= 0)
|
||||||
|
return "battery_charging_20"
|
||||||
|
} else {
|
||||||
|
if (percent >= 95)
|
||||||
|
return "battery_full"
|
||||||
|
if (percent >= 85)
|
||||||
|
return "battery_6_bar"
|
||||||
|
if (percent >= 70)
|
||||||
|
return "battery_5_bar"
|
||||||
|
if (percent >= 55)
|
||||||
|
return "battery_4_bar"
|
||||||
|
if (percent >= 40)
|
||||||
|
return "battery_3_bar"
|
||||||
|
if (percent >= 25)
|
||||||
|
return "battery_2_bar"
|
||||||
|
if (percent >= 10)
|
||||||
|
return "battery_1_bar"
|
||||||
|
if (percent >= 0)
|
||||||
|
return "battery_0_bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -110,9 +110,43 @@ Singleton {
|
||||||
property real lastBrightness: 0
|
property real lastBrightness: 0
|
||||||
property real queuedBrightness: NaN
|
property real queuedBrightness: NaN
|
||||||
|
|
||||||
|
// For internal displays - store the backlight device path
|
||||||
|
property string backlightDevice: ""
|
||||||
|
property string brightnessPath: ""
|
||||||
|
property string maxBrightnessPath: ""
|
||||||
|
property int maxBrightness: 100
|
||||||
|
property bool ignoreNextChange: false
|
||||||
|
|
||||||
// Signal for brightness changes
|
// Signal for brightness changes
|
||||||
signal brightnessUpdated(real newBrightness)
|
signal brightnessUpdated(real newBrightness)
|
||||||
|
|
||||||
|
// FileView to watch for external brightness changes (internal displays only)
|
||||||
|
readonly property FileView brightnessWatcher: FileView {
|
||||||
|
id: brightnessWatcher
|
||||||
|
// Only set path for internal displays with a valid brightness path
|
||||||
|
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
|
||||||
|
watchChanges: path !== ""
|
||||||
|
onFileChanged: {
|
||||||
|
reload()
|
||||||
|
if (monitor.ignoreNextChange) {
|
||||||
|
monitor.ignoreNextChange = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (text() === "")
|
||||||
|
return
|
||||||
|
var current = parseInt(text().trim())
|
||||||
|
if (!isNaN(current) && monitor.maxBrightness > 0) {
|
||||||
|
var newBrightness = current / monitor.maxBrightness
|
||||||
|
// Only update if it's actually different (avoid feedback loops)
|
||||||
|
if (Math.abs(newBrightness - monitor.brightness) > 0.01) {
|
||||||
|
monitor.brightness = newBrightness
|
||||||
|
monitor.brightnessUpdated(monitor.brightness)
|
||||||
|
//Logger.log("Brightness", "External change detected:", monitor.modelData.name, monitor.brightness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize brightness
|
// Initialize brightness
|
||||||
readonly property Process initProc: Process {
|
readonly property Process initProc: Process {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
|
|
@ -121,8 +155,8 @@ Singleton {
|
||||||
if (dataText === "") {
|
if (dataText === "") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
|
|
||||||
|
|
||||||
|
//Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
|
||||||
if (monitor.isAppleDisplay) {
|
if (monitor.isAppleDisplay) {
|
||||||
var val = parseInt(dataText)
|
var val = parseInt(dataText)
|
||||||
if (!isNaN(val)) {
|
if (!isNaN(val)) {
|
||||||
|
|
@ -140,14 +174,20 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Internal backlight
|
// Internal backlight - parse the response which includes device path
|
||||||
var parts = dataText.split(" ")
|
var lines = dataText.split("\n")
|
||||||
if (parts.length >= 2) {
|
if (lines.length >= 3) {
|
||||||
var current = parseInt(parts[0])
|
monitor.backlightDevice = lines[0]
|
||||||
var max = parseInt(parts[1])
|
monitor.brightnessPath = monitor.backlightDevice + "/brightness"
|
||||||
|
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"
|
||||||
|
|
||||||
|
var current = parseInt(lines[1])
|
||||||
|
var max = parseInt(lines[2])
|
||||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||||
|
monitor.maxBrightness = max
|
||||||
monitor.brightness = current / max
|
monitor.brightness = current / max
|
||||||
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
||||||
|
Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +211,7 @@ Singleton {
|
||||||
|
|
||||||
function increaseBrightness(): void {
|
function increaseBrightness(): void {
|
||||||
var stepSize = Settings.data.brightness.brightnessStep / 100.0
|
var stepSize = Settings.data.brightness.brightnessStep / 100.0
|
||||||
setBrightnessDebounced(brightness + stepSize)
|
setBrightnessDebounced(monitor.brightness + stepSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
function decreaseBrightness(): void {
|
function decreaseBrightness(): void {
|
||||||
|
|
@ -183,22 +223,23 @@ Singleton {
|
||||||
value = Math.max(0, Math.min(1, value))
|
value = Math.max(0, Math.min(1, value))
|
||||||
var rounded = Math.round(value * 100)
|
var rounded = Math.round(value * 100)
|
||||||
|
|
||||||
if (Math.round(brightness * 100) === rounded)
|
if (Math.round(monitor.brightness * 100) === rounded)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (isDdc && timer.running) {
|
if (isDdc && timer.running) {
|
||||||
queuedBrightness = value
|
monitor.queuedBrightness = value
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
brightness = value
|
monitor.brightness = value
|
||||||
brightnessUpdated(brightness)
|
brightnessUpdated(monitor.brightness)
|
||||||
|
|
||||||
if (isAppleDisplay) {
|
if (isAppleDisplay) {
|
||||||
Quickshell.execDetached(["asdbctl", "set", rounded])
|
Quickshell.execDetached(["asdbctl", "set", rounded])
|
||||||
} else if (isDdc) {
|
} else if (isDdc) {
|
||||||
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
|
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
|
||||||
} else {
|
} else {
|
||||||
|
monitor.ignoreNextChange = true
|
||||||
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
|
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +249,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBrightnessDebounced(value: real): void {
|
function setBrightnessDebounced(value: real): void {
|
||||||
queuedBrightness = value
|
monitor.queuedBrightness = value
|
||||||
timer.restart()
|
timer.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,8 +259,11 @@ Singleton {
|
||||||
} else if (isDdc) {
|
} else if (isDdc) {
|
||||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
|
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
|
||||||
} else {
|
} else {
|
||||||
// Internal backlight - try to find the first available backlight device
|
// Internal backlight - find the first available backlight device and get its info
|
||||||
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"]
|
// This now returns: device_path, current_brightness, max_brightness (on separate lines)
|
||||||
|
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do "
|
||||||
|
+ " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; "
|
||||||
|
+ " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]
|
||||||
}
|
}
|
||||||
initProc.running = true
|
initProc.running = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ Singleton {
|
||||||
property bool connecting: false
|
property bool connecting: false
|
||||||
property string connectingTo: ""
|
property string connectingTo: ""
|
||||||
property string lastError: ""
|
property string lastError: ""
|
||||||
property bool ethernet: false
|
property bool ethernetConnected: false
|
||||||
|
property string disconnectingFrom: ""
|
||||||
|
property string forgettingNetwork: ""
|
||||||
|
|
||||||
// Persistent cache
|
// Persistent cache
|
||||||
property string cacheFile: Settings.cacheDir + "network.json"
|
property string cacheFile: Settings.cacheDir + "network.json"
|
||||||
|
|
@ -38,10 +40,21 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Settings.data.network
|
||||||
|
function onWifiEnabledChanged() {
|
||||||
|
if (Settings.data.network.wifiEnabled) {
|
||||||
|
ToastService.showNotice("Wi-Fi", "Enabled")
|
||||||
|
} else {
|
||||||
|
ToastService.showNotice("Wi-Fi", "Disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Logger.log("Network", "Service initialized")
|
Logger.log("Network", "Service initialized")
|
||||||
syncWifiState()
|
syncWifiState()
|
||||||
refresh()
|
scan()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save cache with debounce
|
// Save cache with debounce
|
||||||
|
|
@ -55,22 +68,23 @@ Singleton {
|
||||||
saveDebounce.restart()
|
saveDebounce.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single refresh timer for periodic scans
|
// Delayed scan timer
|
||||||
Timer {
|
|
||||||
id: refreshTimer
|
|
||||||
interval: 30000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delayed scan timer for WiFi enable
|
|
||||||
Timer {
|
Timer {
|
||||||
id: delayedScanTimer
|
id: delayedScanTimer
|
||||||
interval: 7000
|
interval: 7000
|
||||||
onTriggered: scan()
|
onTriggered: scan()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ethernet check timer
|
||||||
|
// Always running every 30s
|
||||||
|
Timer {
|
||||||
|
id: ethernetCheckTimer
|
||||||
|
interval: 30000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: ethernetStateProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
// Core functions
|
// Core functions
|
||||||
function syncWifiState() {
|
function syncWifiState() {
|
||||||
wifiStateProcess.running = true
|
wifiStateProcess.running = true
|
||||||
|
|
@ -83,14 +97,6 @@ Singleton {
|
||||||
wifiToggleProcess.running = true
|
wifiToggleProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
ethernetStateProcess.running = true
|
|
||||||
|
|
||||||
if (Settings.data.network.wifiEnabled) {
|
|
||||||
scan()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scan() {
|
function scan() {
|
||||||
if (scanning)
|
if (scanning)
|
||||||
return
|
return
|
||||||
|
|
@ -98,6 +104,7 @@ Singleton {
|
||||||
scanning = true
|
scanning = true
|
||||||
lastError = ""
|
lastError = ""
|
||||||
scanProcess.running = true
|
scanProcess.running = true
|
||||||
|
Logger.log("Network", "Wi-Fi scan in progress...")
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect(ssid, password = "") {
|
function connect(ssid, password = "") {
|
||||||
|
|
@ -123,11 +130,14 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnect(ssid) {
|
function disconnect(ssid) {
|
||||||
|
disconnectingFrom = ssid
|
||||||
disconnectProcess.ssid = ssid
|
disconnectProcess.ssid = ssid
|
||||||
disconnectProcess.running = true
|
disconnectProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function forget(ssid) {
|
function forget(ssid) {
|
||||||
|
forgettingNetwork = ssid
|
||||||
|
|
||||||
// Remove from cache
|
// Remove from cache
|
||||||
let known = cacheAdapter.knownNetworks
|
let known = cacheAdapter.knownNetworks
|
||||||
delete known[ssid]
|
delete known[ssid]
|
||||||
|
|
@ -144,6 +154,40 @@ Singleton {
|
||||||
forgetProcess.running = true
|
forgetProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to immediately update network status
|
||||||
|
function updateNetworkStatus(ssid, connected) {
|
||||||
|
let nets = networks
|
||||||
|
|
||||||
|
// Update all networks connected status
|
||||||
|
for (let key in nets) {
|
||||||
|
if (nets[key].connected && key !== ssid) {
|
||||||
|
nets[key].connected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the target network if it exists
|
||||||
|
if (nets[ssid]) {
|
||||||
|
nets[ssid].connected = connected
|
||||||
|
nets[ssid].existing = true
|
||||||
|
nets[ssid].cached = true
|
||||||
|
} else if (connected) {
|
||||||
|
// Create a temporary entry if network doesn't exist yet
|
||||||
|
nets[ssid] = {
|
||||||
|
"ssid": ssid,
|
||||||
|
"security": "--",
|
||||||
|
"signal": 100,
|
||||||
|
"connected"// Default to good signal until real scan
|
||||||
|
: true,
|
||||||
|
"existing": true,
|
||||||
|
"cached": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger property change notification
|
||||||
|
networks = ({})
|
||||||
|
networks = nets
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function signalIcon(signal) {
|
function signalIcon(signal) {
|
||||||
if (signal >= 80)
|
if (signal >= 80)
|
||||||
|
|
@ -164,17 +208,19 @@ Singleton {
|
||||||
// Processes
|
// Processes
|
||||||
Process {
|
Process {
|
||||||
id: ethernetStateProcess
|
id: ethernetStateProcess
|
||||||
running: false
|
running: true
|
||||||
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
|
const connected = text.split("\n").some(line => {
|
||||||
root.ethernet = text.split("\n").some(line => {
|
const parts = line.split(":")
|
||||||
const parts = line.split(":")
|
return parts[1] === "ethernet" && parts[2] === "connected"
|
||||||
return parts[1] === "ethernet" && parts[2] === "connected"
|
})
|
||||||
})
|
if (root.ethernetConnected !== connected) {
|
||||||
Logger.log("Network", "Ethernet connected:", root.ethernet)
|
root.ethernetConnected = connected
|
||||||
|
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +233,7 @@ Singleton {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
const enabled = text.trim() === "enabled"
|
const enabled = text.trim() === "enabled"
|
||||||
Logger.log("Network", "Wifi enabled:", enabled)
|
Logger.log("Network", "Wi-Fi enabled:", enabled)
|
||||||
if (Settings.data.network.wifiEnabled !== enabled) {
|
if (Settings.data.network.wifiEnabled !== enabled) {
|
||||||
Settings.data.network.wifiEnabled = enabled
|
Settings.data.network.wifiEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
@ -227,11 +273,11 @@ Singleton {
|
||||||
id: scanProcess
|
id: scanProcess
|
||||||
running: false
|
running: false
|
||||||
command: ["sh", "-c", `
|
command: ["sh", "-c", `
|
||||||
# Get existing profiles
|
# Get list of saved connection profiles (just the names)
|
||||||
profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1)
|
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
|
||||||
|
|
||||||
# Get WiFi networks
|
# Get WiFi networks
|
||||||
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list | while read line; do
|
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do
|
||||||
ssid=$(echo "$line" | cut -d: -f1)
|
ssid=$(echo "$line" | cut -d: -f1)
|
||||||
security=$(echo "$line" | cut -d: -f2)
|
security=$(echo "$line" | cut -d: -f2)
|
||||||
signal=$(echo "$line" | cut -d: -f3)
|
signal=$(echo "$line" | cut -d: -f3)
|
||||||
|
|
@ -242,8 +288,10 @@ Singleton {
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if SSID matches any profile name (simple check)
|
||||||
|
# This covers most cases where profile name equals or contains the SSID
|
||||||
existing=false
|
existing=false
|
||||||
if echo "$profiles" | grep -q "^$ssid$"; then
|
if echo "$profiles" | grep -qF "$ssid|"; then
|
||||||
existing=true
|
existing=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -286,9 +334,25 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.networks = nets
|
||||||
root.scanning = false
|
root.scanning = false
|
||||||
Logger.log("Network", "Discovered", Object.keys(root.networks).length, "Wi-Fi networks")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,12 +402,15 @@ Singleton {
|
||||||
cacheAdapter.lastConnected = connectProcess.ssid
|
cacheAdapter.lastConnected = connectProcess.ssid
|
||||||
saveCache()
|
saveCache()
|
||||||
|
|
||||||
|
// Immediately update the UI before scanning
|
||||||
|
root.updateNetworkStatus(connectProcess.ssid, true)
|
||||||
|
|
||||||
root.connecting = false
|
root.connecting = false
|
||||||
root.connectingTo = ""
|
root.connectingTo = ""
|
||||||
Logger.log("Network", "Connected to " + connectProcess.ssid)
|
Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`)
|
||||||
|
|
||||||
// Rescan to update status
|
// Still do a scan to get accurate signal and security info
|
||||||
delayedScanTimer.interval = 1000
|
delayedScanTimer.interval = 5000
|
||||||
delayedScanTimer.restart()
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -378,8 +445,27 @@ Singleton {
|
||||||
running: false
|
running: false
|
||||||
command: ["nmcli", "connection", "down", "id", ssid]
|
command: ["nmcli", "connection", "down", "id", ssid]
|
||||||
|
|
||||||
onRunningChanged: {
|
stdout: StdioCollector {
|
||||||
if (!running) {
|
onStreamFinished: {
|
||||||
|
Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`)
|
||||||
|
|
||||||
|
// Immediately update UI on successful disconnect
|
||||||
|
root.updateNetworkStatus(disconnectProcess.ssid, false)
|
||||||
|
root.disconnectingFrom = ""
|
||||||
|
|
||||||
|
// Do a scan to refresh the list
|
||||||
|
delayedScanTimer.interval = 1000
|
||||||
|
delayedScanTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.disconnectingFrom = ""
|
||||||
|
if (text.trim()) {
|
||||||
|
Logger.warn("Network", "Disconnect error: " + text)
|
||||||
|
}
|
||||||
|
// Still trigger a scan even on error
|
||||||
delayedScanTimer.interval = 1000
|
delayedScanTimer.interval = 1000
|
||||||
delayedScanTimer.restart()
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
@ -390,10 +476,67 @@ Singleton {
|
||||||
id: forgetProcess
|
id: forgetProcess
|
||||||
property string ssid: ""
|
property string ssid: ""
|
||||||
running: false
|
running: false
|
||||||
command: ["nmcli", "connection", "delete", "id", ssid]
|
|
||||||
|
|
||||||
onRunningChanged: {
|
// Try multiple common profile name patterns
|
||||||
if (!running) {
|
command: ["sh", "-c", `
|
||||||
|
ssid="$1"
|
||||||
|
deleted=false
|
||||||
|
|
||||||
|
# Try exact SSID match first
|
||||||
|
if nmcli connection delete id "$ssid" 2>/dev/null; then
|
||||||
|
echo "Deleted profile: $ssid"
|
||||||
|
deleted=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try "Auto <SSID>" pattern
|
||||||
|
if nmcli connection delete id "Auto $ssid" 2>/dev/null; then
|
||||||
|
echo "Deleted profile: Auto $ssid"
|
||||||
|
deleted=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try "<SSID> 1", "<SSID> 2", etc. patterns
|
||||||
|
for i in 1 2 3; do
|
||||||
|
if nmcli connection delete id "$ssid $i" 2>/dev/null; then
|
||||||
|
echo "Deleted profile: $ssid $i"
|
||||||
|
deleted=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$deleted" = "false" ]; then
|
||||||
|
echo "No profiles found for SSID: $ssid"
|
||||||
|
fi
|
||||||
|
`, "--", ssid]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`)
|
||||||
|
Logger.log("Network", text.trim().replace(/[\r\n]/g, " "))
|
||||||
|
|
||||||
|
// Update both cached and existing status immediately
|
||||||
|
let nets = root.networks
|
||||||
|
if (nets[forgetProcess.ssid]) {
|
||||||
|
nets[forgetProcess.ssid].cached = false
|
||||||
|
nets[forgetProcess.ssid].existing = false
|
||||||
|
// Trigger property change
|
||||||
|
root.networks = ({})
|
||||||
|
root.networks = nets
|
||||||
|
}
|
||||||
|
|
||||||
|
root.forgettingNetwork = ""
|
||||||
|
|
||||||
|
// Scan to verify the profile is gone
|
||||||
|
delayedScanTimer.interval = 5000
|
||||||
|
delayedScanTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.forgettingNetwork = ""
|
||||||
|
if (text.trim() && !text.includes("No profiles found")) {
|
||||||
|
Logger.warn("Network", "Forget error: " + text)
|
||||||
|
}
|
||||||
|
// Still Trigger a scan even on error
|
||||||
delayedScanTimer.interval = 1000
|
delayedScanTimer.interval = 1000
|
||||||
delayedScanTimer.restart()
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import QtQuick
|
||||||
import Qt.labs.folderlistmodel
|
import Qt.labs.folderlistmodel
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -11,12 +12,313 @@ Singleton {
|
||||||
// Public values
|
// Public values
|
||||||
property real cpuUsage: 0
|
property real cpuUsage: 0
|
||||||
property real cpuTemp: 0
|
property real cpuTemp: 0
|
||||||
property real memoryUsageGb: 0
|
property real memGb: 0
|
||||||
property real memoryUsagePer: 0
|
property real memPercent: 0
|
||||||
property real diskUsage: 0
|
property real diskPercent: 0
|
||||||
property real rxSpeed: 0
|
property real rxSpeed: 0
|
||||||
property real txSpeed: 0
|
property real txSpeed: 0
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
property int sleepDuration: 3000
|
||||||
|
|
||||||
|
// Internal state for CPU calculation
|
||||||
|
property var prevCpuStats: null
|
||||||
|
|
||||||
|
// Internal state for network speed calculation
|
||||||
|
// Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
|
||||||
|
// since the computer started, so their value will easily overlfow a 32bit int.
|
||||||
|
property real prevRxBytes: 0
|
||||||
|
property real prevTxBytes: 0
|
||||||
|
property real prevTime: 0
|
||||||
|
|
||||||
|
// Cpu temperature is the most complex
|
||||||
|
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
|
||||||
|
property string cpuTempSensorName: ""
|
||||||
|
property string cpuTempHwmonPath: ""
|
||||||
|
// For Intel coretemp averaging of all cores/sensors
|
||||||
|
property var intelTempValues: []
|
||||||
|
property int intelTempFilesChecked: 0
|
||||||
|
property int intelTempMaxFiles: 20 // Will test up to temp20_input
|
||||||
|
|
||||||
|
// --------------------------------------------
|
||||||
|
Component.onCompleted: {
|
||||||
|
Logger.log("SystemStat", "Service started with interval:", root.sleepDuration, "ms")
|
||||||
|
|
||||||
|
// Kickoff the cpu name detection for temperature
|
||||||
|
cpuTempNameReader.checkNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------
|
||||||
|
// Timer for periodic updates
|
||||||
|
Timer {
|
||||||
|
id: updateTimer
|
||||||
|
interval: root.sleepDuration
|
||||||
|
repeat: true
|
||||||
|
running: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: {
|
||||||
|
// Trigger all direct system files reads
|
||||||
|
memInfoFile.reload()
|
||||||
|
cpuStatFile.reload()
|
||||||
|
netDevFile.reload()
|
||||||
|
|
||||||
|
// Run df (disk free) one time
|
||||||
|
dfProcess.running = true
|
||||||
|
|
||||||
|
updateCpuTemperature()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------
|
||||||
|
// FileView components for reading system files
|
||||||
|
FileView {
|
||||||
|
id: memInfoFile
|
||||||
|
path: "/proc/meminfo"
|
||||||
|
onLoaded: parseMemoryInfo(text())
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: cpuStatFile
|
||||||
|
path: "/proc/stat"
|
||||||
|
onLoaded: calculateCpuUsage(text())
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: netDevFile
|
||||||
|
path: "/proc/net/dev"
|
||||||
|
onLoaded: calculateNetworkSpeed(text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------
|
||||||
|
// Process to fetch disk usage in percent
|
||||||
|
// Uses 'df' aka 'disk free'
|
||||||
|
Process {
|
||||||
|
id: dfProcess
|
||||||
|
command: ["df", "--output=pcent", "/"]
|
||||||
|
running: false
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const lines = text.trim().split('\n')
|
||||||
|
if (lines.length >= 2) {
|
||||||
|
const percent = lines[1].replace(/[^0-9]/g, '')
|
||||||
|
root.diskPercent = parseInt(percent) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------
|
||||||
|
// CPU Temperature
|
||||||
|
// It's more complex.
|
||||||
|
// ----
|
||||||
|
// #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
|
||||||
|
FileView {
|
||||||
|
id: cpuTempNameReader
|
||||||
|
property int currentIndex: 0
|
||||||
|
|
||||||
|
function checkNext() {
|
||||||
|
if (currentIndex >= 10) {
|
||||||
|
// Check up to hwmon10
|
||||||
|
Logger.warn("No supported temperature sensor found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Logger.log("SystemStat", "---- Probing: hwmon", currentIndex)
|
||||||
|
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`
|
||||||
|
cpuTempNameReader.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
const name = text().trim()
|
||||||
|
if (root.supportedTempCpuSensorNames.includes(name)) {
|
||||||
|
root.cpuTempSensorName = name
|
||||||
|
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`
|
||||||
|
Logger.log("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`)
|
||||||
|
} else {
|
||||||
|
currentIndex++
|
||||||
|
Qt.callLater(() => {
|
||||||
|
// Qt.callLater is mandatory
|
||||||
|
checkNext()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadFailed: function (error) {
|
||||||
|
currentIndex++
|
||||||
|
Qt.callLater(() => {
|
||||||
|
// Qt.callLater is mandatory
|
||||||
|
checkNext()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
// #2 - Read sensor value
|
||||||
|
FileView {
|
||||||
|
id: cpuTempReader
|
||||||
|
printErrors: false
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
const data = text().trim()
|
||||||
|
if (root.cpuTempSensorName === "coretemp") {
|
||||||
|
// For Intel, collect all temperature values
|
||||||
|
const temp = parseInt(data) / 1000.0
|
||||||
|
//console.log(temp, cpuTempReader.path)
|
||||||
|
root.intelTempValues.push(temp)
|
||||||
|
Qt.callLater(() => {
|
||||||
|
// Qt.callLater is mandatory
|
||||||
|
checkNextIntelTemp()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// For AMD sensors (k10temp and zenpower), directly set the temperature
|
||||||
|
root.cpuTemp = Math.round(parseInt(data) / 1000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onLoadFailed: function (error) {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
// Qt.callLater is mandatory
|
||||||
|
checkNextIntelTemp()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// Parse memory info from /proc/meminfo
|
||||||
|
function parseMemoryInfo(text) {
|
||||||
|
if (!text)
|
||||||
|
return
|
||||||
|
|
||||||
|
const lines = text.split('\n')
|
||||||
|
let memTotal = 0
|
||||||
|
let memAvailable = 0
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('MemTotal:')) {
|
||||||
|
memTotal = parseInt(line.split(/\s+/)[1]) || 0
|
||||||
|
} else if (line.startsWith('MemAvailable:')) {
|
||||||
|
memAvailable = parseInt(line.split(/\s+/)[1]) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memTotal > 0) {
|
||||||
|
const usageKb = memTotal - memAvailable
|
||||||
|
root.memGb = (usageKb / 1000000).toFixed(1)
|
||||||
|
root.memPercent = Math.round((usageKb / memTotal) * 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// Calculate CPU usage from /proc/stat
|
||||||
|
function calculateCpuUsage(text) {
|
||||||
|
if (!text)
|
||||||
|
return
|
||||||
|
|
||||||
|
const lines = text.split('\n')
|
||||||
|
const cpuLine = lines[0]
|
||||||
|
|
||||||
|
// First line is total CPU
|
||||||
|
if (!cpuLine.startsWith('cpu '))
|
||||||
|
return
|
||||||
|
|
||||||
|
const parts = cpuLine.split(/\s+/)
|
||||||
|
const stats = {
|
||||||
|
"user": parseInt(parts[1]) || 0,
|
||||||
|
"nice": parseInt(parts[2]) || 0,
|
||||||
|
"system": parseInt(parts[3]) || 0,
|
||||||
|
"idle": parseInt(parts[4]) || 0,
|
||||||
|
"iowait": parseInt(parts[5]) || 0,
|
||||||
|
"irq": parseInt(parts[6]) || 0,
|
||||||
|
"softirq": parseInt(parts[7]) || 0,
|
||||||
|
"steal": parseInt(parts[8]) || 0,
|
||||||
|
"guest": parseInt(parts[9]) || 0,
|
||||||
|
"guestNice": parseInt(parts[10]) || 0
|
||||||
|
}
|
||||||
|
const totalIdle = stats.idle + stats.iowait
|
||||||
|
const total = Object.values(stats).reduce((sum, val) => sum + val, 0)
|
||||||
|
|
||||||
|
if (root.prevCpuStats) {
|
||||||
|
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait
|
||||||
|
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => sum + val, 0)
|
||||||
|
|
||||||
|
const diffTotal = total - prevTotal
|
||||||
|
const diffIdle = totalIdle - prevTotalIdle
|
||||||
|
|
||||||
|
if (diffTotal > 0) {
|
||||||
|
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.prevCpuStats = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// Calculate RX and TX speed from /proc/net/dev
|
||||||
|
// Average speed of all interfaces excepted 'lo'
|
||||||
|
function calculateNetworkSpeed(text) {
|
||||||
|
if (!text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = Date.now() / 1000
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
let totalRx = 0
|
||||||
|
let totalTx = 0
|
||||||
|
|
||||||
|
for (var i = 2; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (!line) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const colonIndex = line.indexOf(':')
|
||||||
|
if (colonIndex === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const iface = line.substring(0, colonIndex).trim()
|
||||||
|
if (iface === 'lo') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsLine = line.substring(colonIndex + 1).trim()
|
||||||
|
const stats = statsLine.split(/\s+/)
|
||||||
|
|
||||||
|
const rxBytes = parseInt(stats[0], 10) || 0
|
||||||
|
const txBytes = parseInt(stats[8], 10) || 0
|
||||||
|
|
||||||
|
totalRx += rxBytes
|
||||||
|
totalTx += txBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute only if we have a previous run to compare to.
|
||||||
|
if (root.prevTime > 0) {
|
||||||
|
const timeDiff = currentTime - root.prevTime
|
||||||
|
|
||||||
|
// Avoid division by zero if time hasn't passed.
|
||||||
|
if (timeDiff > 0) {
|
||||||
|
let rxDiff = totalRx - root.prevRxBytes
|
||||||
|
let txDiff = totalTx - root.prevTxBytes
|
||||||
|
|
||||||
|
// Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
|
||||||
|
if (rxDiff < 0) {
|
||||||
|
rxDiff = 0
|
||||||
|
}
|
||||||
|
if (txDiff < 0) {
|
||||||
|
txDiff = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
root.rxSpeed = Math.round(rxDiff / timeDiff) // Speed in Bytes/s
|
||||||
|
root.txSpeed = Math.round(txDiff / timeDiff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.prevRxBytes = totalRx
|
||||||
|
root.prevTxBytes = totalTx
|
||||||
|
root.prevTime = currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
// Helper function to format network speeds
|
// Helper function to format network speeds
|
||||||
function formatSpeed(bytesPerSecond) {
|
function formatSpeed(bytesPerSecond) {
|
||||||
if (bytesPerSecond < 1024) {
|
if (bytesPerSecond < 1024) {
|
||||||
|
|
@ -30,27 +332,44 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background process emitting one JSON line per sample
|
// -------------------------------------------------------
|
||||||
Process {
|
// Function to start fetching and computing the cpu temperature
|
||||||
id: reader
|
function updateCpuTemperature() {
|
||||||
running: true
|
// For AMD sensors (k10temp and zenpower), only use Tctl sensor
|
||||||
command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"]
|
// temp1_input corresponds to Tctl (Temperature Control) on these sensors
|
||||||
stdout: SplitParser {
|
if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
|
||||||
onRead: function (line) {
|
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`
|
||||||
try {
|
cpuTempReader.reload()
|
||||||
const data = JSON.parse(line)
|
} // For Intel coretemp, start averaging all available sensors/cores
|
||||||
root.cpuUsage = data.cpu
|
else if (root.cpuTempSensorName === "coretemp") {
|
||||||
root.cpuTemp = data.cputemp
|
root.intelTempValues = []
|
||||||
root.memoryUsageGb = data.memgb
|
root.intelTempFilesChecked = 0
|
||||||
root.memoryUsagePer = data.memper
|
checkNextIntelTemp()
|
||||||
root.diskUsage = data.diskper
|
|
||||||
root.rxSpeed = parseFloat(data.rx_speed) || 0
|
|
||||||
root.txSpeed = parseFloat(data.tx_speed) || 0
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
// ignore malformed lines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// Function to check next Intel temperature sensor
|
||||||
|
function checkNextIntelTemp() {
|
||||||
|
if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
|
||||||
|
// Calculate average of all found temperatures
|
||||||
|
if (root.intelTempValues.length > 0) {
|
||||||
|
let sum = 0
|
||||||
|
for (var i = 0; i < root.intelTempValues.length; i++) {
|
||||||
|
sum += root.intelTempValues[i]
|
||||||
|
}
|
||||||
|
root.cpuTemp = Math.round(sum / root.intelTempValues.length)
|
||||||
|
//Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`)
|
||||||
|
} else {
|
||||||
|
Logger.warn("SystemStat", "No temperature sensors found for coretemp")
|
||||||
|
root.cpuTemp = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check next temperature file
|
||||||
|
root.intelTempFilesChecked++
|
||||||
|
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`
|
||||||
|
cpuTempReader.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ Item {
|
||||||
property color iconCircleColor: Color.mPrimary
|
property color iconCircleColor: Color.mPrimary
|
||||||
property color iconTextColor: Color.mSurface
|
property color iconTextColor: Color.mSurface
|
||||||
property color collapsedIconColor: Color.mOnSurface
|
property color collapsedIconColor: Color.mOnSurface
|
||||||
|
|
||||||
|
property real iconRotation: 0
|
||||||
property real sizeRatio: 0.8
|
property real sizeRatio: 0.8
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
property bool forceOpen: false
|
property bool forceOpen: false
|
||||||
|
|
@ -97,6 +99,8 @@ Item {
|
||||||
// When forced shown, match pill background; otherwise use accent when hovered
|
// When forced shown, match pill background; otherwise use accent when hovered
|
||||||
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
border.color: forceOpen ? Qt.alpha(Color.mOutline, 0.5) : Color.transparent
|
||||||
|
|
||||||
anchors.left: rightOpen ? parent.left : undefined
|
anchors.left: rightOpen ? parent.left : undefined
|
||||||
anchors.right: rightOpen ? undefined : parent.right
|
anchors.right: rightOpen ? undefined : parent.right
|
||||||
|
|
@ -110,6 +114,7 @@ Item {
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: root.icon
|
text: root.icon
|
||||||
|
rotation: root.iconRotation
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
// When forced shown, use pill text color; otherwise accent color when hovered
|
// When forced shown, use pill text color; otherwise accent color when hovered
|
||||||
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue