Claiming the shared suppression flag before showing meant the outgoing panel's close cleared it again, leaving the incoming panel open with the indicators still revealed. Guarding the clear instead only moved the problem: handing off to a panel that does not manage the flag left it stuck on, and the center indicators stopped revealing on hover for good. Claim it after the handoff instead. The panel taking over always wins, and a handoff to a panel that knows nothing about the flag still leaves it cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
840 lines
29 KiB
QML
840 lines
29 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "Model.js" as Model
|
|
|
|
Panel {
|
|
id: root
|
|
moduleName: "omarchy.weather"
|
|
ipcTarget: "omarchy.weather"
|
|
manageIpc: false
|
|
|
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
|
property var anchorItem: null
|
|
property bool openedFromHotkey: false
|
|
|
|
// The bar tracks the widget mounted in its slot — BarWidget.qml — not this
|
|
// nested panel. Everything the bar identifies a panel by has to be that
|
|
// widget: the popout coordinator (and with it the open-panel dot under the
|
|
// pill) compares against `slot.activeItem`, and switchPanelFrom looks the
|
|
// slot up the same way.
|
|
property var hostWidget: null
|
|
readonly property var barIdentity: hostWidget || root
|
|
|
|
function open() {
|
|
openedFromHotkey = false
|
|
setCenterHoverRevealSuppressed(false)
|
|
root.controller.show()
|
|
locationFile.reload()
|
|
root.refresh()
|
|
}
|
|
|
|
function openFromHotkey() {
|
|
openedFromHotkey = true
|
|
root.controller.show()
|
|
locationFile.reload()
|
|
root.refresh()
|
|
// Set after showing, not before: showing hands the popout coordinator
|
|
// over, which closes whichever panel was open, and that close clears the
|
|
// shared flag. Deferring means the panel taking over always wins, while
|
|
// a handoff to a panel that does not manage the flag still leaves it
|
|
// cleared rather than stuck on.
|
|
Qt.callLater(function() {
|
|
if (root.opened) setCenterHoverRevealSuppressed(true)
|
|
})
|
|
}
|
|
|
|
function close() {
|
|
setCenterHoverRevealSuppressed(false)
|
|
if (root.editingLocation) root.cancelEditingLocation()
|
|
root.controller.hide()
|
|
}
|
|
|
|
function toggle() {
|
|
if (root.opened) root.close()
|
|
else root.openFromHotkey()
|
|
}
|
|
|
|
function switchPanel(direction) {
|
|
if (root.bar && typeof root.bar.switchPanelFrom === "function")
|
|
return root.bar.switchPanelFrom(root.barIdentity, direction)
|
|
return false
|
|
}
|
|
|
|
function setCenterHoverRevealSuppressed(value) {
|
|
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
|
root.bar.centerHoverRevealSuppressed = value
|
|
}
|
|
|
|
// Parsed wttr.in j1 response. Kept on failure so stale data stays visible.
|
|
property var report: null
|
|
property var dailyForecastReport: null
|
|
property string wttrLocation: ""
|
|
|
|
// Configured location, read from the weather.json state file (owned by
|
|
// omarchy-weather-location). The query is the wttr.in path segment
|
|
// (coordinates when stored, else the encoded name); empty means IP
|
|
// auto-detect. The watch makes hand edits take effect live.
|
|
property var configuredLocationState: ({ name: "", latitude: null, longitude: null })
|
|
readonly property string configuredLocation: configuredLocationState.name
|
|
readonly property string locationQuery: Model.wttrLocationQuery(configuredLocationState.name, configuredLocationState.latitude, configuredLocationState.longitude)
|
|
|
|
// Keep the previous report visible while the new location loads. The
|
|
// editor remains open with a spinner, so stale data is never presented
|
|
// under the newly configured location label.
|
|
onLocationQueryChanged: {
|
|
if (savingLocation) savingLocationQueryStarted = true
|
|
forecastRetries = 0
|
|
forecastProc.running = false
|
|
dailyForecastProc.running = false
|
|
Qt.callLater(refresh)
|
|
}
|
|
|
|
property FileView locationFile: FileView {
|
|
path: Quickshell.env("HOME") + "/.local/state/omarchy/settings/weather.json"
|
|
watchChanges: true
|
|
printErrors: false
|
|
onFileChanged: reload()
|
|
onLoaded: root.configuredLocationState = Model.parseLocationFile(text())
|
|
onLoadFailed: root.configuredLocationState = Model.parseLocationFile("")
|
|
}
|
|
|
|
// The first read can race shell startup (observed sporadically), leaving a
|
|
// stored location unhonored until the next file write. One delayed reload
|
|
// self-corrects; if the first read was fine it's a no-op, since identical
|
|
// state doesn't change locationQuery and so triggers no refetch.
|
|
Timer {
|
|
interval: 1500
|
|
running: true
|
|
onTriggered: locationFile.reload()
|
|
}
|
|
|
|
property int forecastRetries: 0
|
|
|
|
// Click-to-edit state for the location label.
|
|
property bool editingLocation: false
|
|
property bool savingLocation: false
|
|
property bool savingLocationQueryStarted: false
|
|
property var locationSuggestions: []
|
|
property int suggestionIndex: 0
|
|
property string geocodePendingQuery: ""
|
|
property string geocodeActiveQuery: ""
|
|
|
|
// Shared hero/bar icon state, updated with each successful weather response.
|
|
property string label: ""
|
|
property string klass: ""
|
|
|
|
// wttr's current conditions when available; open-meteo's (bundled with the
|
|
// much faster daily forecast fetch) fill the hero while wttr is in flight.
|
|
readonly property bool hasConfiguredCoordinates: !isNaN(parseFloat(String(configuredLocationState.latitude))) && !isNaN(parseFloat(String(configuredLocationState.longitude)))
|
|
readonly property var openMeteoCurrent: Model.openMeteoCurrentCondition(dailyForecastReport)
|
|
readonly property var current: (hasConfiguredCoordinates && openMeteoCurrent) ? openMeteoCurrent : ((report && report.current_condition && report.current_condition[0]) ? report.current_condition[0] : openMeteoCurrent)
|
|
readonly property var areaInfo: report && report.nearest_area && report.nearest_area[0] ? report.nearest_area[0] : null
|
|
readonly property var forecastDays: buildForecastDays()
|
|
readonly property string reportCountry: areaInfo && areaInfo.country && areaInfo.country[0] ? areaInfo.country[0].value : ""
|
|
|
|
readonly property bool useImperial: Model.shouldUseImperial(setting("unit", ""), Qt.locale().name, reportCountry)
|
|
|
|
// 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: configuredLocation || wttrLocation || (areaInfo && areaInfo.areaName && areaInfo.areaName[0] ? areaInfo.areaName[0].value : "")
|
|
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 reportWind: current ? (useImperial ? (current.windspeedMiles + " mph") : (current.windspeedKmph + " km/h")) : ""
|
|
readonly property string reportHumidity: current ? (current.humidity + "%") : ""
|
|
|
|
function refresh() {
|
|
if (!forecastProc.running) forecastProc.running = true
|
|
if (root.locationQuery === "" && !locationProc.running) locationProc.running = true
|
|
// With stored coordinates this fetches open-meteo right away — no need
|
|
// to wait for the slow wttr response. Without them it's a no-op until
|
|
// wttr reports the detected area.
|
|
refreshDailyForecast(null)
|
|
}
|
|
|
|
function refreshDailyForecast(sourceReport) {
|
|
if (dailyForecastProc.running) return
|
|
|
|
var lat = parseFloat(String(root.configuredLocationState.latitude))
|
|
var lon = parseFloat(String(root.configuredLocationState.longitude))
|
|
if (isNaN(lat) || isNaN(lon)) {
|
|
var area = sourceReport && sourceReport.nearest_area && sourceReport.nearest_area[0] ? sourceReport.nearest_area[0] : root.areaInfo
|
|
if (!area) return
|
|
lat = parseFloat(String(area.latitude || ""))
|
|
lon = parseFloat(String(area.longitude || ""))
|
|
}
|
|
if (isNaN(lat) || isNaN(lon)) return
|
|
|
|
var url = "https://api.open-meteo.com/v1/forecast"
|
|
+ "?latitude=" + encodeURIComponent(String(lat))
|
|
+ "&longitude=" + encodeURIComponent(String(lon))
|
|
+ "&daily=weather_code,temperature_2m_max,temperature_2m_min"
|
|
+ "¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code,is_day"
|
|
+ "&forecast_days=4"
|
|
+ "&timezone=auto"
|
|
dailyForecastProc.command = ["curl", "-fsS", "--max-time", "5", url]
|
|
dailyForecastProc.running = true
|
|
}
|
|
|
|
// ---- Location editing. Clicking the location label swaps it for a search
|
|
// field; picking a geocoded suggestion persists name + coordinates to
|
|
// the module's shell.json entry. An empty commit returns to auto.
|
|
function startEditingLocation() {
|
|
editingLocation = true
|
|
savingLocation = false
|
|
savingLocationQueryStarted = false
|
|
locationSuggestions = []
|
|
suggestionIndex = 0
|
|
Qt.callLater(function() {
|
|
locationField.text = root.configuredLocation
|
|
locationField.selectAll()
|
|
locationField.forceActiveFocus()
|
|
})
|
|
}
|
|
|
|
function cancelEditingLocation() {
|
|
editingLocation = false
|
|
savingLocation = false
|
|
savingLocationQueryStarted = false
|
|
locationSuggestions = []
|
|
geocodeDebounce.stop()
|
|
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
|
}
|
|
|
|
function commitLocation() {
|
|
var location = Model.locationCommit(locationField.text, locationSuggestions, suggestionIndex)
|
|
if (location.name === "") {
|
|
clearLocation()
|
|
return
|
|
}
|
|
savingLocation = true
|
|
savingLocationQueryStarted = false
|
|
configuredLocationState = {
|
|
name: location.name,
|
|
latitude: location.latitude,
|
|
longitude: location.longitude
|
|
}
|
|
persistLocation(location.name, location.latitude, location.longitude)
|
|
}
|
|
|
|
function clearLocation() {
|
|
persistLocation("", null, null)
|
|
wttrLocation = ""
|
|
cancelEditingLocation()
|
|
}
|
|
|
|
function pickSuggestion(suggestion) {
|
|
if (!suggestion) return
|
|
savingLocation = true
|
|
savingLocationQueryStarted = false
|
|
configuredLocationState = {
|
|
name: suggestion.name,
|
|
latitude: suggestion.latitude,
|
|
longitude: suggestion.longitude
|
|
}
|
|
persistLocation(suggestion.name, suggestion.latitude, suggestion.longitude)
|
|
}
|
|
|
|
function finishSavingLocation() {
|
|
if (savingLocation && savingLocationQueryStarted) cancelEditingLocation()
|
|
}
|
|
|
|
function persistLocation(name, latitude, longitude) {
|
|
if (name && latitude !== null && longitude !== null)
|
|
locationSaveProc.command = ["omarchy-weather-location", "--set", name, latitude + "," + longitude]
|
|
else if (name)
|
|
locationSaveProc.command = ["omarchy-weather-location", "--set", name]
|
|
else
|
|
locationSaveProc.command = ["omarchy-weather-location", "--clear"]
|
|
locationSaveProc.running = true
|
|
}
|
|
|
|
// Debounced geocoding. Only one curl runs at a time; if the query moved on
|
|
// while a fetch was in flight, the latest query is fetched right after.
|
|
function requestGeocode() {
|
|
var query = locationField.text.trim()
|
|
if (query.length < 2) {
|
|
locationSuggestions = []
|
|
return
|
|
}
|
|
geocodePendingQuery = query
|
|
if (!geocodeProc.running) startGeocode()
|
|
}
|
|
|
|
function startGeocode() {
|
|
geocodeActiveQuery = geocodePendingQuery
|
|
geocodeProc.command = ["curl", "-fsS", "--max-time", "5",
|
|
"https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(geocodeActiveQuery) + "&count=5&language=en&format=json"]
|
|
geocodeProc.running = true
|
|
}
|
|
|
|
function buildForecastDays() {
|
|
return Model.buildForecastDays(report, dailyForecastReport, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
|
}
|
|
|
|
function openMeteoForecastDays() {
|
|
return Model.openMeteoForecastDays(dailyForecastReport, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
|
}
|
|
|
|
function wttrNextForecastDays() {
|
|
return Model.wttrNextForecastDays(report, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
|
}
|
|
|
|
function isFutureForecastDate(dateString) {
|
|
return Model.isFutureForecastDate(dateString, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
|
}
|
|
|
|
function roundedTemp(value) {
|
|
return Model.roundedTemp(value)
|
|
}
|
|
|
|
function celsiusToFahrenheit(value) {
|
|
return Model.celsiusToFahrenheit(value)
|
|
}
|
|
|
|
function formatTemp(value) {
|
|
return Model.formatTemp(value, useImperial)
|
|
}
|
|
|
|
function dayName(dateString) {
|
|
return Model.dayName(dateString, function(date) { return Qt.formatDate(date, "dddd") })
|
|
}
|
|
|
|
// Bare degree value (no unit letter), used in the forecast row.
|
|
function bareTempForDay(day, kind) {
|
|
return Model.bareTempForDay(day, kind, useImperial)
|
|
}
|
|
|
|
// Representative icon for a forecast day: the hourly entry nearest noon.
|
|
function dayIcon(day) {
|
|
return Model.dayIcon(day)
|
|
}
|
|
|
|
function iconForOpenMeteoCode(code) {
|
|
return Model.iconForOpenMeteoCode(code)
|
|
}
|
|
|
|
// Mirrors omarchy-weather-icon's wttr.in code → nerd-font glyph mapping.
|
|
function iconForCode(code, night) {
|
|
return Model.iconForCode(code, night)
|
|
}
|
|
|
|
Process {
|
|
id: forecastProc
|
|
command: ["curl", "-fsS", "--max-time", "10", "https://wttr.in/" + root.locationQuery + "?format=j1"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: {
|
|
var raw = String(text || "").trim()
|
|
if (!raw) {
|
|
root.scheduleForecastRetry()
|
|
return
|
|
}
|
|
try {
|
|
var parsed = JSON.parse(raw)
|
|
root.report = parsed
|
|
if (!root.hasConfiguredCoordinates)
|
|
root.label = Model.provisionalCurrentIcon(parsed.current_condition && parsed.current_condition[0], root.label)
|
|
root.forecastRetries = 0
|
|
if (Model.weatherResponseCompletesSave(root.hasConfiguredCoordinates, "wttr"))
|
|
root.finishSavingLocation()
|
|
// Stored coordinates already drove the fast open-meteo fetch from
|
|
// refresh(); only auto-detect needs the area wttr reported.
|
|
if (isNaN(parseFloat(String(root.configuredLocationState.latitude))))
|
|
root.refreshDailyForecast(parsed)
|
|
} catch (e) {
|
|
// Keep last-good report visible, but try again shortly.
|
|
root.scheduleForecastRetry()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// wttr.in can be slow or flaky, especially for a location it hasn't
|
|
// cached yet. Retry a few times before leaving it to the refresh timer.
|
|
function scheduleForecastRetry() {
|
|
if (forecastRetries >= 3) return
|
|
forecastRetries++
|
|
forecastRetryTimer.restart()
|
|
}
|
|
|
|
Timer {
|
|
id: forecastRetryTimer
|
|
interval: 2500
|
|
onTriggered: if (!forecastProc.running) forecastProc.running = true
|
|
}
|
|
|
|
Process {
|
|
id: dailyForecastProc
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: {
|
|
var raw = String(text || "").trim()
|
|
if (!raw) return
|
|
try {
|
|
var parsed = JSON.parse(raw)
|
|
var parsedCurrent = Model.openMeteoCurrentCondition(parsed)
|
|
root.dailyForecastReport = parsed
|
|
root.label = Model.currentIcon(parsedCurrent, root.label)
|
|
if (Model.weatherResponseCompletesSave(root.hasConfiguredCoordinates, "open-meteo"))
|
|
root.finishSavingLocation()
|
|
} catch (e) {
|
|
// Keep last-good daily forecast on parse failure.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: geocodeProc
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: {
|
|
root.locationSuggestions = root.editingLocation ? Model.parseGeocodingResults(text) : []
|
|
root.suggestionIndex = 0
|
|
if (root.geocodePendingQuery !== root.geocodeActiveQuery) Qt.callLater(root.startGeocode)
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: geocodeDebounce
|
|
interval: 300
|
|
onTriggered: root.requestGeocode()
|
|
}
|
|
|
|
Process {
|
|
id: locationSaveProc
|
|
onExited: function(exitCode) {
|
|
if (exitCode !== 0 || !root.savingLocation) return
|
|
|
|
// FileView handles changed locations. Explicitly refresh here too so
|
|
// saving the already-active location cannot strand the spinner.
|
|
locationFile.reload()
|
|
if (!root.savingLocationQueryStarted) {
|
|
root.savingLocationQueryStarted = true
|
|
root.forecastRetries = 0
|
|
forecastProc.running = false
|
|
dailyForecastProc.running = false
|
|
Qt.callLater(root.refresh)
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: locationProc
|
|
command: ["curl", "-fsS", "--max-time", "4", "https://wttr.in/?format=%l"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: {
|
|
var raw = String(text || "").trim()
|
|
if (!raw) return
|
|
root.wttrLocation = raw.split(",")[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: refreshTimer
|
|
interval: root.refreshMinutes * 60 * 1000
|
|
running: true
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
IpcHandler {
|
|
target: root.ipcTarget
|
|
|
|
function open(): void { root.openFromHotkey() }
|
|
function close(): void { root.close() }
|
|
function show(): void { root.openFromHotkey() }
|
|
function hide(): void { root.close() }
|
|
function toggle(): void { root.toggle() }
|
|
function edit(): void { root.openFromHotkey(); root.startEditingLocation() }
|
|
}
|
|
|
|
KeyboardPanel {
|
|
id: panel
|
|
anchorItem: root.anchorItem
|
|
owner: root.barIdentity
|
|
bar: root.bar
|
|
open: root.opened
|
|
centerOnBar: true
|
|
focusTarget: keyCatcher
|
|
contentWidth: panel.fittedContentWidth(Style.space(480))
|
|
contentHeight: panel.fittedContentHeight(weatherColumn.implicitHeight)
|
|
|
|
PanelKeyCatcher {
|
|
id: keyCatcher
|
|
anchors.fill: parent
|
|
blocked: root.editingLocation
|
|
onReturnRequested: root.startEditingLocation()
|
|
onCloseRequested: root.close()
|
|
onTabRequested: function(direction) { root.switchPanel(direction) }
|
|
|
|
Flickable {
|
|
id: weatherScroll
|
|
anchors.fill: parent
|
|
contentWidth: width
|
|
contentHeight: weatherColumn.implicitHeight
|
|
clip: true
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
interactive: contentHeight > height
|
|
|
|
Column {
|
|
id: weatherColumn
|
|
width: weatherScroll.width
|
|
spacing: Style.space(14)
|
|
|
|
// ---- Hero row: big icon + temp on the left; location and stats stacked on the right.
|
|
Item {
|
|
width: parent.width
|
|
height: Math.max(heroLeft.height, heroRight.height)
|
|
|
|
Row {
|
|
id: heroLeft
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Style.space(16)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Style.space(16)
|
|
|
|
Text {
|
|
id: heroIcon
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.verticalCenterOffset: 5
|
|
text: root.label || "—"
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
// Decorative condition emoji; intentionally larger than the
|
|
// Style.font.* scale's displayLarge (28).
|
|
font.pixelSize: 64
|
|
}
|
|
|
|
Row {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Style.space(2)
|
|
|
|
Text {
|
|
id: tempBig
|
|
text: root.reportTempNum || "—"
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
// Hero temperature read-out; deliberately oversized, outside
|
|
// the Style.font.* scale.
|
|
font.pixelSize: 56
|
|
font.bold: true
|
|
}
|
|
Text {
|
|
text: root.current ? root.tempUnit : ""
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.display
|
|
anchors.top: tempBig.top
|
|
anchors.topMargin: Style.space(10)
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: heroRight
|
|
width: weatherStats.implicitWidth
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Style.space(20)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Style.space(12)
|
|
|
|
Row {
|
|
visible: !root.editingLocation && root.reportLocation !== ""
|
|
spacing: Style.space(6)
|
|
|
|
TapHandler {
|
|
onTapped: root.startEditingLocation()
|
|
}
|
|
HoverHandler {
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
Text {
|
|
text: "" // nf-fa-map_marker
|
|
color: Qt.darker(root.bar.foreground, 1.4)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
Text {
|
|
text: (root.reportLocation || "").toUpperCase()
|
|
color: Qt.darker(root.bar.foreground, 1.4)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
font.letterSpacing: 1
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
Row {
|
|
visible: root.editingLocation
|
|
spacing: Style.space(6)
|
|
|
|
TextField {
|
|
id: locationField
|
|
width: Style.space(190)
|
|
enabled: !root.savingLocation
|
|
placeholderText: "Search city"
|
|
foreground: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
|
|
onTextChanged: if (root.editingLocation && !root.savingLocation) geocodeDebounce.restart()
|
|
|
|
Keys.onPressed: function(event) {
|
|
if (event.key === Qt.Key_Escape) {
|
|
root.cancelEditingLocation()
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Down) {
|
|
if (root.suggestionIndex < root.locationSuggestions.length - 1) root.suggestionIndex++
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Up) {
|
|
if (root.suggestionIndex > 0) root.suggestionIndex--
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
root.commitLocation()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear back to IP auto-detect. While a committed location is
|
|
// loading, this same compact affordance becomes a spinner.
|
|
Rectangle {
|
|
width: Style.space(18)
|
|
height: Style.space(18)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
radius: Math.min(4, Style.cornerRadius)
|
|
color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: root.savingLocation ? "" : "✕"
|
|
font.family: root.bar.fontFamily
|
|
color: Qt.darker(root.bar.foreground, 1.4)
|
|
font.pixelSize: Style.font.bodySmall
|
|
|
|
RotationAnimator on rotation {
|
|
running: root.savingLocation
|
|
from: 0; to: 360
|
|
duration: 800
|
|
loops: Animation.Infinite
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: clearLocationArea
|
|
anchors.fill: parent
|
|
enabled: !root.savingLocation
|
|
hoverEnabled: true
|
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: root.clearLocation()
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: weatherStats
|
|
visible: !!root.current
|
|
spacing: Style.space(36)
|
|
|
|
Column {
|
|
spacing: Style.space(5)
|
|
Text {
|
|
text: "FEELS"
|
|
color: Qt.darker(root.bar.foreground, 1.5)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.letterSpacing: 1
|
|
}
|
|
Text {
|
|
text: root.reportFeels
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.title
|
|
}
|
|
}
|
|
|
|
Column {
|
|
spacing: Style.space(5)
|
|
Text {
|
|
text: "WIND"
|
|
color: Qt.darker(root.bar.foreground, 1.5)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.letterSpacing: 1
|
|
}
|
|
Text {
|
|
text: root.reportWind
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.title
|
|
}
|
|
}
|
|
|
|
Column {
|
|
spacing: Style.space(5)
|
|
Text {
|
|
text: "HUMID"
|
|
color: Qt.darker(root.bar.foreground, 1.5)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.letterSpacing: 1
|
|
}
|
|
Text {
|
|
text: root.reportHumidity
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Geocoding suggestions while the location is being edited.
|
|
Column {
|
|
visible: root.editingLocation && !root.savingLocation && root.locationSuggestions.length > 0
|
|
width: parent.width
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: root.locationSuggestions
|
|
|
|
Rectangle {
|
|
required property var modelData
|
|
required property int index
|
|
width: parent.width
|
|
height: suggestionRow.implicitHeight + Style.space(12)
|
|
radius: Style.cornerRadius
|
|
color: index === root.suggestionIndex ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
|
|
|
|
Row {
|
|
id: suggestionRow
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Style.space(16)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Style.space(8)
|
|
|
|
Text {
|
|
text: modelData.name
|
|
color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
}
|
|
Text {
|
|
visible: text !== ""
|
|
text: modelData.description
|
|
color: Qt.darker(root.bar.foreground, 1.5)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPositionChanged: root.suggestionIndex = index
|
|
onClicked: root.pickSuggestion(modelData)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: !root.current
|
|
text: "Fetching forecast…"
|
|
color: Qt.darker(root.bar.foreground, 1.5)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.italic: true
|
|
}
|
|
|
|
// ---- Divider between current conditions and forecast.
|
|
Rectangle {
|
|
visible: root.forecastDays.length > 0
|
|
width: parent.width
|
|
height: Style.spacing.hairline
|
|
color: root.bar.foreground
|
|
opacity: 0.12
|
|
}
|
|
|
|
// ---- Forecast row: each cell has the day icon left of a day-name + hi/lo column.
|
|
// Wrapped in an Item so the block of cells can be centered within the popup.
|
|
Item {
|
|
visible: root.forecastDays.length > 0
|
|
width: parent.width
|
|
height: forecastRow.height
|
|
|
|
Row {
|
|
id: forecastRow
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: Style.space(44)
|
|
|
|
Repeater {
|
|
model: root.forecastDays
|
|
|
|
Row {
|
|
required property var modelData
|
|
required property int index
|
|
spacing: Style.space(10)
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.dayIcon(modelData)
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.display
|
|
}
|
|
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Style.space(2)
|
|
|
|
Text {
|
|
text: root.dayName(modelData.date).toUpperCase()
|
|
color: Qt.darker(root.bar.foreground, 1.4)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
Row {
|
|
spacing: Style.space(6)
|
|
|
|
Text {
|
|
text: root.bareTempForDay(modelData, "max")
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
}
|
|
Text {
|
|
text: root.bareTempForDay(modelData, "min")
|
|
color: Qt.darker(root.bar.foreground, 1.5)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|