1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 16:02:51 -05:00

power: use native upower APIs

This commit is contained in:
bbedward
2025-07-16 12:35:46 -04:00
parent 21bc4ece52
commit 2a84a6423b
5 changed files with 291 additions and 191 deletions

View File

@@ -8,206 +8,205 @@ pragma ComponentBehavior: Bound
Singleton { Singleton {
id: root id: root
// Debug mode for testing (disabled for now) property bool batteryAvailable: UPower.displayDevice?.isLaptopBattery ?? false
property bool debugMode: false property int batteryLevel: batteryAvailable ? Math.round(UPower.displayDevice.percentage * 100) : 0
// Battery properties - using shell command method (native UPower API commented out due to issues)
property bool batteryAvailable: systemBatteryPercentage > 0
property int batteryLevel: systemBatteryPercentage
property string batteryStatus: { property string batteryStatus: {
return systemBatteryState === "charging" ? "Charging" : if (!batteryAvailable) return "No Battery"
systemBatteryState === "discharging" ? "Discharging" : if (UPower.displayDevice.state === UPowerDeviceState.Charging) return "Charging"
systemBatteryState === "fully-charged" ? "Full" : if (UPower.displayDevice.state === UPowerDeviceState.Discharging) return "Discharging"
systemBatteryState === "empty" ? "Empty" : "Unknown" if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) return "Full"
if (UPower.displayDevice.state === UPowerDeviceState.Empty) return "Empty"
if (UPower.displayDevice.state === UPowerDeviceState.PendingCharge) return "Pending Charge"
if (UPower.displayDevice.state === UPowerDeviceState.PendingDischarge) return "Pending Discharge"
return "Unknown"
} }
property int timeRemaining: 0 // Not implemented for shell fallback property int timeRemaining: {
property bool isCharging: systemBatteryState === "charging" if (!batteryAvailable) return 0
property bool isLowBattery: systemBatteryPercentage <= 20 return UPower.onBattery ? (UPower.displayDevice.timeToEmpty || 0) : (UPower.displayDevice.timeToFull || 0)
}
property bool isCharging: batteryAvailable && (UPower.displayDevice.state === UPowerDeviceState.Charging || (!UPower.onBattery && batteryLevel < 100))
property bool isLowBattery: batteryAvailable && batteryLevel <= 20
/* Native UPower API (commented out - not working correctly, returns 1% instead of actual values) property int batteryHealth: batteryAvailable && UPower.displayDevice.healthSupported ? Math.round(UPower.displayDevice.healthPercentage * 100) : 100
property bool batteryAvailable: (UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.percentage > 0) || systemBatteryPercentage > 0 property string batteryTechnology: batteryAvailable ? "Li-ion" : "N/A"
property int batteryLevel: { property int batteryCapacity: batteryAvailable ? Math.round(UPower.displayDevice.energyCapacity * 1000) : 0
if (UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.percentage > 0) {
return Math.round(UPower.displayDevice.percentage)
}
return systemBatteryPercentage
}
property string batteryStatus: {
if (UPower.displayDevice && UPower.displayDevice.ready) {
switch(UPower.displayDevice.state) {
case UPowerDeviceState.Charging: return "Charging"
case UPowerDeviceState.Discharging: return "Discharging"
case UPowerDeviceState.FullyCharged: return "Full"
case UPowerDeviceState.Empty: return "Empty"
case UPowerDeviceState.PendingCharge: return "Pending Charge"
case UPowerDeviceState.PendingDischarge: return "Pending Discharge"
case UPowerDeviceState.Unknown:
default: return "Unknown"
}
}
return systemBatteryState === "charging" ? "Charging" :
systemBatteryState === "discharging" ? "Discharging" :
systemBatteryState === "fully-charged" ? "Full" :
systemBatteryState === "empty" ? "Empty" : "Unknown"
}
property int timeRemaining: (UPower.displayDevice && UPower.displayDevice.ready) ? (UPower.displayDevice.timeToEmpty || UPower.displayDevice.timeToFull || 0) : 0
property bool isCharging: {
if (UPower.displayDevice && UPower.displayDevice.ready) {
return UPower.displayDevice.state === UPowerDeviceState.Charging
}
return systemBatteryState === "charging"
}
property bool isLowBattery: {
if (UPower.displayDevice && UPower.displayDevice.ready) {
return UPower.displayDevice.percentage <= 20
}
return systemBatteryPercentage <= 20
}
*/
property int batteryHealth: 100 // Default fallback
property string batteryTechnology: "Li-ion" // Default fallback
property int cycleCount: 0 // Not implemented for shell fallback
property int batteryCapacity: 45000 // Default fallback
property var powerProfiles: availableProfiles
property string activePowerProfile: "balanced" // Default fallback
// System battery info from shell command (primary source) property var powerProfiles: {
property int systemBatteryPercentage: 100 // Default value, will be updated by shell command if (!powerProfilesAvailable || typeof PowerProfiles === "undefined") {
property string systemBatteryState: "charging" // Default value, will be updated by shell command return ["power-saver", "balanced", "performance"]
}
// Shell command fallback for battery info
Process {
id: batteryProcess
running: false
command: ["upower", "-i", "/org/freedesktop/UPower/devices/battery_BAT1"]
stdout: StdioCollector { let profiles = [
onStreamFinished: { PowerProfile.PowerSaver,
if (text.trim()) { PowerProfile.Balanced,
let output = text.trim() PowerProfile.Performance
let percentageMatch = output.match(/percentage:\s*(\d+)%/) ].filter(profile => {
let stateMatch = output.match(/state:\s*(\w+)/) if (profile === PowerProfile.Performance && !PowerProfiles.hasPerformanceProfile) {
return false
if (percentageMatch) { }
root.systemBatteryPercentage = parseInt(percentageMatch[1]) return true
console.log("Battery percentage updated to:", root.systemBatteryPercentage) })
}
if (stateMatch) { return profiles.map(profile => {
root.systemBatteryState = stateMatch[1] switch(profile) {
console.log("Battery state updated to:", root.systemBatteryState) case PowerProfile.PowerSaver: return "power-saver"
} case PowerProfile.Performance: return "performance"
case PowerProfile.Balanced:
default: return "balanced"
}
})
}
property string activePowerProfile: {
if (powerProfilesAvailable && typeof PowerProfiles !== "undefined") {
try {
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "power-saver"
case PowerProfile.Performance: return "performance"
default: return "balanced"
} }
} catch (error) {
return "balanced"
} }
} }
return "balanced"
}
property bool powerProfilesAvailable: false
property string powerProfilesError: powerProfilesAvailable ? "" : "Power profiles daemon not available. Install and enable power-profiles-daemon."
property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && activePowerProfile !== "power-saver"
Process {
id: checkPowerProfilesDaemon
command: ["bash", "-c", "systemctl is-active power-profiles-daemon || pgrep -x power-profiles-daemon > /dev/null"]
running: false
onExited: (exitCode) => { onExited: (exitCode) => {
if (exitCode !== 0) { powerProfilesAvailable = (exitCode === 0)
console.warn("Battery process failed with exit code:", exitCode) }
} }
Connections {
target: UPower
function onOnBatteryChanged() {
batteryAvailableChanged()
isChargingChanged()
} }
} }
Connections {
// Timer to periodically check battery status target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
Timer { function onProfileChanged() {
interval: 5000 // Check every 5 seconds activePowerProfileChanged()
running: true
repeat: true
onTriggered: {
batteryProcess.running = true
} }
} }
Connections {
target: UPower.displayDevice
function onPercentageChanged() {
batteryLevelChanged()
isLowBatteryChanged()
}
function onStateChanged() {
batteryStatusChanged()
isChargingChanged()
}
function onTimeToEmptyChanged() {
timeRemainingChanged()
}
function onTimeToFullChanged() {
timeRemainingChanged()
}
function onReadyChanged() {
batteryAvailableChanged()
}
function onIsLaptopBatteryChanged() {
batteryAvailableChanged()
}
function onEnergyChanged() {
batteryCapacityChanged()
}
function onEnergyCapacityChanged() {
batteryCapacityChanged()
}
function onHealthPercentageChanged() {
batteryHealthChanged()
}
}
Component.onCompleted: { Component.onCompleted: {
// Initial battery check checkPowerProfilesDaemon.running = true
batteryProcess.running = true
// Get current power profile
getCurrentProfile()
console.log("BatteryService initialized with shell command approach")
} }
property var availableProfiles: { signal showErrorMessage(string message)
// Try to use power-profiles-daemon via shell command
return ["power-saver", "balanced", "performance"]
}
function setBatteryProfile(profileName) { function setBatteryProfile(profileName) {
console.log("Setting power profile to:", profileName) console.log("Setting power profile to:", profileName)
powerProfileProcess.command = ["powerprofilesctl", "set", profileName]
powerProfileProcess.running = true
}
// Process to set power profile
Process {
id: powerProfileProcess
running: false
onExited: (exitCode) => { if (!powerProfilesAvailable) {
if (exitCode === 0) { console.warn("Power profiles daemon not available")
console.log("Power profile set successfully") showErrorMessage("power-profiles-daemon not available")
// Update current profile return
getCurrentProfile()
} else {
console.warn("Failed to set power profile, exit code:", exitCode)
}
}
}
// Process to get current power profile
Process {
id: getCurrentProfileProcess
running: false
command: ["powerprofilesctl", "get"]
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.activePowerProfile = text.trim()
console.log("Current power profile:", root.activePowerProfile)
}
}
} }
onExited: (exitCode) => { try {
if (exitCode !== 0) { switch(profileName) {
console.warn("Failed to get current power profile, exit code:", exitCode) case "power-saver":
PowerProfiles.profile = PowerProfile.PowerSaver
break
case "balanced":
PowerProfiles.profile = PowerProfile.Balanced
break
case "performance":
PowerProfiles.profile = PowerProfile.Performance
break
default:
console.warn("Unknown profile:", profileName)
return
} }
console.log("Power profile set successfully to:", PowerProfiles.profile)
} catch (error) {
console.error("Failed to set power profile:", error)
showErrorMessage("power-profiles-daemon not available")
} }
} }
function getCurrentProfile() {
getCurrentProfileProcess.running = true
}
function getBatteryIcon() { function getBatteryIcon() {
if (!root.batteryAvailable) return "power" if (!batteryAvailable) {
switch(activePowerProfile) {
case "power-saver": return "energy_savings_leaf"
case "performance": return "rocket_launch"
default: return "balance"
}
}
let level = root.batteryLevel const level = batteryLevel
let charging = root.isCharging const charging = isCharging
if (charging) { if (charging) {
if (level >= 90) return "battery_charging_full" if (level >= 90) return "battery_charging_full"
if (level >= 60) return "battery_charging_90" if (level >= 80) return "battery_charging_90"
if (level >= 30) return "battery_charging_60" if (level >= 60) return "battery_charging_80"
if (level >= 50) return "battery_charging_60"
if (level >= 30) return "battery_charging_50"
if (level >= 20) return "battery_charging_30" if (level >= 20) return "battery_charging_30"
return "battery_charging_20" return "battery_charging_20"
} else { } else {
if (level >= 90) return "battery_full" if (level >= 95) return "battery_full"
if (level >= 60) return "battery_6_bar" if (level >= 85) return "battery_6_bar"
if (level >= 50) return "battery_5_bar" if (level >= 70) return "battery_5_bar"
if (level >= 40) return "battery_4_bar" if (level >= 55) return "battery_4_bar"
if (level >= 30) return "battery_3_bar" if (level >= 40) return "battery_3_bar"
if (level >= 20) return "battery_2_bar" if (level >= 25) return "battery_2_bar"
if (level >= 10) return "battery_1_bar" if (level >= 10) return "battery_1_bar"
return "battery_alert" return "battery_alert"
} }
} }
function formatTimeRemaining() { function formatTimeRemaining() {
if (root.timeRemaining <= 0) return "Unknown" if (!batteryAvailable || timeRemaining <= 0) return "Unknown"
let hours = Math.floor(root.timeRemaining / 3600) const hours = Math.floor(timeRemaining / 3600)
let minutes = Math.floor((root.timeRemaining % 3600) / 60) const minutes = Math.floor((timeRemaining % 3600) / 60)
if (hours > 0) { if (hours > 0) {
return hours + "h " + minutes + "m" return hours + "h " + minutes + "m"

View File

@@ -29,6 +29,7 @@ Singleton {
property int retryDelay: 30000 // 30 seconds property int retryDelay: 30000 // 30 seconds
property int lastFetchTime: 0 property int lastFetchTime: 0
property int minFetchInterval: 30000 // 30 seconds minimum between fetches property int minFetchInterval: 30000 // 30 seconds minimum between fetches
property int persistentRetryCount: 0 // Track persistent retry attempts for backoff
// Weather icon mapping (based on wttr.in weather codes) // Weather icon mapping (based on wttr.in weather codes)
property var weatherIcons: ({ property var weatherIcons: ({
@@ -121,6 +122,11 @@ Singleton {
function handleWeatherSuccess() { function handleWeatherSuccess() {
root.retryAttempts = 0 root.retryAttempts = 0
root.persistentRetryCount = 0 // Reset persistent retry count on success
// Stop any persistent retry timer if running
if (persistentRetryTimer.running) {
persistentRetryTimer.stop()
}
// Don't restart the timer - let it continue its normal interval // Don't restart the timer - let it continue its normal interval
if (updateTimer.interval !== root.updateInterval) { if (updateTimer.interval !== root.updateInterval) {
updateTimer.interval = root.updateInterval updateTimer.interval = root.updateInterval
@@ -133,12 +139,17 @@ Singleton {
console.log(`Weather fetch failed, retrying in ${root.retryDelay/1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`) console.log(`Weather fetch failed, retrying in ${root.retryDelay/1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
retryTimer.start() retryTimer.start()
} else { } else {
console.warn("Weather fetch failed after maximum retry attempts") console.warn("Weather fetch failed after maximum retry attempts, will keep trying...")
root.weather.available = false root.weather.available = false
root.weather.loading = false root.weather.loading = false
// Reset retry count but keep trying with exponential backoff
root.retryAttempts = 0 root.retryAttempts = 0
// Set longer interval for next automatic retry // Use exponential backoff: 1min, 2min, 4min, then cap at 5min
updateTimer.interval = root.updateInterval * 2 const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
persistentRetryCount++
console.log(`Scheduling persistent retry in ${backoffDelay/1000}s`)
persistentRetryTimer.interval = backoffDelay
persistentRetryTimer.start()
} }
} }
@@ -223,6 +234,17 @@ Singleton {
} }
} }
Timer {
id: persistentRetryTimer
interval: 60000 // Will be dynamically set
running: false
repeat: false
onTriggered: {
console.log("Persistent retry attempt...")
root.fetchWeather()
}
}
Component.onCompleted: { Component.onCompleted: {
// Watch for preference changes to refetch weather // Watch for preference changes to refetch weather
Prefs.weatherLocationOverrideChanged.connect(() => { Prefs.weatherLocationOverrideChanged.connect(() => {

View File

@@ -9,7 +9,7 @@ import "../Services"
PanelWindow { PanelWindow {
id: batteryControlPopup id: batteryControlPopup
visible: root.batteryPopupVisible && BatteryService.batteryAvailable visible: root.batteryPopupVisible
implicitWidth: 400 implicitWidth: 400
implicitHeight: 300 implicitHeight: 300
@@ -84,7 +84,7 @@ PanelWindow {
width: parent.width width: parent.width
Text { Text {
text: "Battery Information" text: BatteryService.batteryAvailable ? "Battery Information" : "Power Management"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
@@ -119,7 +119,6 @@ PanelWindow {
} }
} }
// Battery status card
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 120 height: 120
@@ -127,13 +126,13 @@ PanelWindow {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)) border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12))
border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1 border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1
visible: BatteryService.batteryAvailable
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
spacing: Theme.spacingL spacing: Theme.spacingL
// Large battery icon
Text { Text {
text: BatteryService.getBatteryIcon() text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont font.family: Theme.iconFont
@@ -188,10 +187,53 @@ PanelWindow {
} }
} }
// No battery info card
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
visible: !BatteryService.batteryAvailable
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
Text {
text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont
font.pixelSize: 36
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: "No Battery Detected"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: "Power profile management is available"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
}
// Battery details // Battery details
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: BatteryService.batteryAvailable
Text { Text {
text: "Battery Details" text: "Battery Details"
@@ -224,23 +266,6 @@ PanelWindow {
} }
} }
// Cycle count
Column {
spacing: 2
Text {
text: "Cycle Count"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Medium
}
Text {
text: BatteryService.cycleCount > 0 ? BatteryService.cycleCount.toString() : "Unknown"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
// Health // Health
Column { Column {
@@ -280,11 +305,11 @@ PanelWindow {
} }
} }
// Power profiles (if available) // Power profiles
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: BatteryService.powerProfiles.length > 0 visible: true
Text { Text {
text: "Power Profile" text: "Power Profile"
@@ -380,4 +405,44 @@ PanelWindow {
} }
} }
} }
// Error toast
Rectangle {
id: errorToast
width: Math.min(300, parent.width - Theme.spacingL * 2)
height: 50
radius: Theme.cornerRadius
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingL
visible: false
z: 1000
Text {
anchors.centerIn: parent
text: "power-profiles-daemon not available"
color: "white"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
Timer {
id: hideTimer
interval: 3000
onTriggered: errorToast.visible = false
}
function show() {
visible = true
hideTimer.restart()
}
}
Connections {
target: BatteryService
function onShowErrorMessage(message) {
errorToast.show()
}
}
} }

View File

@@ -9,19 +9,18 @@ Rectangle {
signal toggleBatteryPopup() signal toggleBatteryPopup()
width: 70 // Increased width to accommodate percentage text width: BatteryService.batteryAvailable ? 70 : 40
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: batteryArea.containsMouse || batteryPopupVisible ? color: batteryArea.containsMouse || batteryPopupVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08) Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
visible: BatteryService.batteryAvailable visible: true
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 4 spacing: 4
// Battery icon - Material Design icons already show level visually
Text { Text {
text: BatteryService.getBatteryIcon() text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont font.family: Theme.iconFont
@@ -34,7 +33,6 @@ Rectangle {
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// Subtle pulse animation for charging
SequentialAnimation on opacity { SequentialAnimation on opacity {
running: BatteryService.isCharging running: BatteryService.isCharging
loops: Animation.Infinite loops: Animation.Infinite
@@ -43,7 +41,6 @@ Rectangle {
} }
} }
// Battery percentage text
Text { Text {
text: BatteryService.batteryLevel + "%" text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -55,6 +52,7 @@ Rectangle {
return Theme.surfaceText return Theme.surfaceText
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
} }
} }
@@ -78,7 +76,7 @@ Rectangle {
color: Theme.surfaceContainer color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1 border.width: 1
visible: batteryArea.containsMouse && !batteryPopupVisible && BatteryService.batteryAvailable visible: batteryArea.containsMouse && !batteryPopupVisible
anchors.bottom: parent.top anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
@@ -100,7 +98,15 @@ Rectangle {
Text { Text {
id: tooltipText id: tooltipText
text: { text: {
if (!BatteryService.batteryAvailable) return "No battery" if (!BatteryService.batteryAvailable) {
let profile = BatteryService.activePowerProfile
switch(profile) {
case "power-saver": return "Power Profile: Power Saver"
case "balanced": return "Power Profile: Balanced"
case "performance": return "Power Profile: Performance"
default: return "Power Profile: " + profile
}
}
let status = BatteryService.batteryStatus let status = BatteryService.batteryStatus
let level = BatteryService.batteryLevel + "%" let level = BatteryService.batteryLevel + "%"

View File

@@ -34,15 +34,23 @@ Rectangle {
visible: !weather || !weather.available || weather.temp === 0 visible: !weather || !weather.available || weather.temp === 0
Text { Text {
text: "cloud_off" text: weather && weather.loading ? "cloud_sync" : "cloud_off"
font.family: theme.iconFont font.family: theme.iconFont
font.pixelSize: theme.iconSize + 8 font.pixelSize: theme.iconSize + 8
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5) color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
RotationAnimation on rotation {
from: 0
to: 360
duration: 2000
running: weather && weather.loading
loops: Animation.Infinite
}
} }
Text { Text {
text: "No Weather Data" text: weather && weather.loading ? "Loading Weather..." : "No Weather Data"
font.pixelSize: theme.fontSizeMedium font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7) color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter