Redesign weather popup and auto-refresh forecast
Replace the inline detail flyout with a two-column hero: location chip
and large temperature on the left, big condition icon on the right.
Stats (Feels / Wind / Humidity) move below as caps-labeled columns,
separated from the forecast by a thin divider. Forecast cells now lead
with the day icon and put the day name above hi/lo temps.
Drop the Refresh and wttr.in pill buttons; a Timer refreshes the
forecast every refreshMinutes (default 15). The interval is exposed
through the Settings panel — Bar -> Weather has a gear that opens a
SpinBox-backed form, persisted as "settings": { "refreshMinutes": N }
in shell.json.
This commit is contained in:
@@ -312,7 +312,7 @@ Item {
|
|||||||
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" },
|
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" },
|
||||||
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false },
|
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false },
|
||||||
"systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false },
|
"systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false },
|
||||||
"weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false },
|
"weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false, settingsForm: "weatherSettings" },
|
||||||
"idleInhibitor": { displayName: "Keep awake", description: "Toggle idle inhibitor", category: "System", allowMultiple: false },
|
"idleInhibitor": { displayName: "Keep awake", description: "Toggle idle inhibitor", category: "System", allowMultiple: false },
|
||||||
"microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false },
|
"microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false },
|
||||||
"activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false },
|
"activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false },
|
||||||
|
|||||||
@@ -31,12 +31,24 @@ Item {
|
|||||||
return /^en_US/.test(name) || /^en_LR/.test(name) || /^my/.test(name)
|
return /^en_US/.test(name) || /^en_LR/.test(name) || /^my/.test(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string reportLocation: areaInfo && areaInfo.areaName && areaInfo.areaName[0] ? areaInfo.areaName[0].value : ""
|
// Auto-refresh interval in minutes; clamped to a sane minimum.
|
||||||
|
readonly property int refreshMinutes: Math.max(1, parseInt(setting("refreshMinutes", 15), 10) || 15)
|
||||||
|
|
||||||
|
readonly property string reportLocation: areaInfo && areaInfo.areaName && areaInfo.areaName[0] ? areaInfo.areaName[0].value : ""
|
||||||
readonly property string reportCondition: current && current.weatherDesc && current.weatherDesc[0] ? current.weatherDesc[0].value : ""
|
readonly property string reportCondition: current && current.weatherDesc && current.weatherDesc[0] ? current.weatherDesc[0].value : ""
|
||||||
readonly property string reportTemp: current ? formatTemp(useImperial ? current.temp_F : current.temp_C) : ""
|
readonly property string reportTemp: current ? formatTemp(useImperial ? current.temp_F : current.temp_C) : ""
|
||||||
|
readonly property string reportTempNum: current ? String(useImperial ? current.temp_F : current.temp_C) : ""
|
||||||
|
readonly property string tempUnit: "°" + (useImperial ? "F" : "C")
|
||||||
readonly property string reportFeels: current ? formatTemp(useImperial ? current.FeelsLikeF : current.FeelsLikeC) : ""
|
readonly property string reportFeels: current ? formatTemp(useImperial ? current.FeelsLikeF : current.FeelsLikeC) : ""
|
||||||
readonly property string reportWind: current ? (useImperial ? (current.windspeedMiles + " mph") : (current.windspeedKmph + " km/h")) : ""
|
readonly property string reportWind: current ? (useImperial ? (current.windspeedMiles + " mph") : (current.windspeedKmph + " km/h")) : ""
|
||||||
readonly property string reportHumidity: current ? (current.humidity + "%") : ""
|
readonly property string reportHumidity: current ? (current.humidity + "%") : ""
|
||||||
|
readonly property string conditionLine: {
|
||||||
|
var parts = []
|
||||||
|
if (reportCondition) parts.push(reportCondition)
|
||||||
|
var wd = windDescriptor()
|
||||||
|
if (wd) parts.push(wd)
|
||||||
|
return parts.join(" · ")
|
||||||
|
}
|
||||||
|
|
||||||
visible: label !== ""
|
visible: label !== ""
|
||||||
implicitWidth: button.implicitWidth + 8
|
implicitWidth: button.implicitWidth + 8
|
||||||
@@ -73,6 +85,32 @@ Item {
|
|||||||
return formatTemp(useImperial ? day.mintempF : day.mintempC)
|
return formatTemp(useImperial ? day.mintempF : day.mintempC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beaufort-ish descriptor based on current windspeed in mph.
|
||||||
|
function windDescriptor() {
|
||||||
|
if (!current) return ""
|
||||||
|
var mph = parseInt(String(current.windspeedMiles || "0"), 10)
|
||||||
|
if (isNaN(mph)) return ""
|
||||||
|
if (mph < 1) return "Calm"
|
||||||
|
if (mph < 4) return "Light air"
|
||||||
|
if (mph < 8) return "Light breeze"
|
||||||
|
if (mph < 13) return "Gentle breeze"
|
||||||
|
if (mph < 19) return "Moderate breeze"
|
||||||
|
if (mph < 25) return "Fresh breeze"
|
||||||
|
if (mph < 32) return "Strong breeze"
|
||||||
|
if (mph < 39) return "Near gale"
|
||||||
|
return "Gale"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bare degree value (no unit letter), used in the forecast row.
|
||||||
|
function bareTempForDay(day, kind) {
|
||||||
|
if (!day) return ""
|
||||||
|
var v = useImperial
|
||||||
|
? (kind === "max" ? day.maxtempF : day.mintempF)
|
||||||
|
: (kind === "max" ? day.maxtempC : day.mintempC)
|
||||||
|
if (v === undefined || v === null || v === "") return ""
|
||||||
|
return v + "°"
|
||||||
|
}
|
||||||
|
|
||||||
// Representative icon for a forecast day: the hourly entry nearest noon.
|
// Representative icon for a forecast day: the hourly entry nearest noon.
|
||||||
function dayIcon(day) {
|
function dayIcon(day) {
|
||||||
if (!day || !day.hourly || day.hourly.length === 0) return ""
|
if (!day || !day.hourly || day.hourly.length === 0) return ""
|
||||||
@@ -122,6 +160,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: refreshTimer
|
||||||
|
interval: root.refreshMinutes * 60 * 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
Common.WidgetButton {
|
Common.WidgetButton {
|
||||||
id: button
|
id: button
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -155,61 +202,143 @@ Item {
|
|||||||
bar: root.bar
|
bar: root.bar
|
||||||
open: root.popupOpen
|
open: root.popupOpen
|
||||||
triggerMode: "click"
|
triggerMode: "click"
|
||||||
contentWidth: 340
|
contentWidth: 460
|
||||||
contentHeight: card.implicitHeight + 28
|
contentHeight: card.implicitHeight + 28
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: card
|
id: card
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 14
|
spacing: 20
|
||||||
|
|
||||||
Row {
|
// ---- Hero row: location/temp/condition on the left, big icon on the right.
|
||||||
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 12
|
height: heroLeft.height
|
||||||
|
|
||||||
Text {
|
|
||||||
id: glyph
|
|
||||||
text: root.label || "—"
|
|
||||||
color: root.bar.foreground
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 30
|
|
||||||
anchors.verticalCenter: headerCol.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: headerCol
|
id: heroLeft
|
||||||
width: parent.width - glyph.width - parent.spacing
|
anchors.left: parent.left
|
||||||
spacing: 2
|
anchors.top: parent.top
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
Text {
|
Row {
|
||||||
text: root.reportTemp || "—"
|
visible: root.reportLocation !== ""
|
||||||
color: root.bar.foreground
|
spacing: 6
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 22
|
Text {
|
||||||
font.bold: true
|
text: "" // nf-fa-map_marker
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 11
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: (root.reportLocation || "").toUpperCase()
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.letterSpacing: 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: tempBig
|
||||||
|
text: root.reportTempNum || "—"
|
||||||
|
color: root.bar.foreground
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 56
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.current ? root.tempUnit : ""
|
||||||
|
color: root.bar.foreground
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 22
|
||||||
|
anchors.top: tempBig.top
|
||||||
|
anchors.topMargin: 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
text: root.reportLocation
|
text: root.conditionLine
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.2)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
font.pixelSize: 11
|
font.pixelSize: 12
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: heroIcon
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: heroLeft.verticalCenter
|
||||||
|
text: root.label || "—"
|
||||||
|
color: root.bar.foreground
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 56
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
// ---- Stats row: three caps-labeled columns.
|
||||||
visible: text !== ""
|
Row {
|
||||||
text: root.reportCondition
|
visible: !!root.current
|
||||||
color: Qt.darker(root.bar.foreground, 1.2)
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
font.italic: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
spacing: 36
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
text: "FEELS"
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.letterSpacing: 1
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.reportFeels
|
||||||
|
color: root.bar.foreground
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
text: "WIND"
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.letterSpacing: 1
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.reportWind
|
||||||
|
color: root.bar.foreground
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
text: "HUMIDITY"
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.letterSpacing: 1
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.reportHumidity
|
||||||
|
color: root.bar.foreground
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 13
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -221,118 +350,70 @@ Item {
|
|||||||
font.italic: true
|
font.italic: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Grid {
|
// ---- Divider between current conditions and forecast.
|
||||||
visible: !!root.current
|
Rectangle {
|
||||||
|
visible: root.forecastDays.length > 0
|
||||||
width: parent.width
|
width: parent.width
|
||||||
columns: 2
|
height: 1
|
||||||
columnSpacing: 12
|
color: root.bar.foreground
|
||||||
rowSpacing: 4
|
opacity: 0.12
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Feels like"
|
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: root.reportFeels
|
|
||||||
color: root.bar.foreground
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Wind"
|
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: root.reportWind
|
|
||||||
color: root.bar.foreground
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Humidity"
|
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: root.reportHumidity
|
|
||||||
color: root.bar.foreground
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Forecast row: each cell has the day icon left of a day-name + hi/lo column.
|
||||||
Row {
|
Row {
|
||||||
id: forecastRow
|
id: forecastRow
|
||||||
visible: root.forecastDays.length > 0
|
visible: root.forecastDays.length > 0
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.forecastDays
|
model: root.forecastDays
|
||||||
|
|
||||||
Column {
|
Row {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
width: (forecastRow.width - forecastRow.spacing * Math.max(0, root.forecastDays.length - 1)) / Math.max(1, root.forecastDays.length)
|
width: forecastRow.width / Math.max(1, root.forecastDays.length)
|
||||||
spacing: 4
|
spacing: 10
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.dayName(modelData.date)
|
|
||||||
color: root.bar.foreground
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: root.dayIcon(modelData)
|
text: root.dayIcon(modelData)
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
font.pixelSize: 22
|
font.pixelSize: 24
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Column {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.maxTempForDay(modelData) + " / " + root.minTempForDay(modelData)
|
spacing: 2
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
|
||||||
font.family: root.bar.fontFamily
|
Text {
|
||||||
font.pixelSize: 10
|
text: root.dayName(modelData.date).toUpperCase()
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.letterSpacing: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.bareTempForDay(modelData, "max")
|
||||||
|
color: root.bar.foreground
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.bareTempForDay(modelData, "min")
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Common.PillButton {
|
|
||||||
iconText: ""
|
|
||||||
text: "Refresh"
|
|
||||||
foreground: root.bar.foreground
|
|
||||||
horizontalPadding: 12
|
|
||||||
verticalPadding: 6
|
|
||||||
onClicked: root.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
Common.PillButton {
|
|
||||||
iconText: ""
|
|
||||||
text: "wttr.in"
|
|
||||||
foreground: root.bar.foreground
|
|
||||||
horizontalPadding: 12
|
|
||||||
verticalPadding: 6
|
|
||||||
onClicked: { root.bar.run("xdg-open https://wttr.in"); root.popupOpen = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2497,6 +2497,7 @@ Item {
|
|||||||
switch (meta.settingsForm) {
|
switch (meta.settingsForm) {
|
||||||
case "spacerSettings": return spacerSettingsComponent
|
case "spacerSettings": return spacerSettingsComponent
|
||||||
case "calendarSettings": return calendarSettingsComponent
|
case "calendarSettings": return calendarSettingsComponent
|
||||||
|
case "weatherSettings": return weatherSettingsComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||||
@@ -2644,6 +2645,33 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: weatherSettingsComponent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: weatherForm
|
||||||
|
signal fieldChanged(string key, var value)
|
||||||
|
property var entry: ({})
|
||||||
|
|
||||||
|
spacing: 8
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Auto-refresh interval (minutes)"
|
||||||
|
color: Qt.darker(root.foreground, 1.4)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
SpinBox {
|
||||||
|
from: 1
|
||||||
|
to: 1440
|
||||||
|
value: weatherForm.entry.refreshMinutes !== undefined ? weatherForm.entry.refreshMinutes : 15
|
||||||
|
onValueModified: weatherForm.fieldChanged("refreshMinutes", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== plugins category ==================================
|
// ===================== plugins category ==================================
|
||||||
component PluginManager: Column {
|
component PluginManager: Column {
|
||||||
id: pm
|
id: pm
|
||||||
|
|||||||
Reference in New Issue
Block a user