414 lines
17 KiB
QML
414 lines
17 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import "../../config"
|
|
|
|
// Weather indicator — modeled on omarchy's weather bar pill
|
|
// (github.com/omacom/omarchy, quattro branch,
|
|
// shell/plugins/panels/weather) but rebuilt in this bar's own style: the
|
|
// actual location/polling logic lives in the WeatherData singleton (config/),
|
|
// shared across every monitor's bar; this is just the icon + panel UI,
|
|
// same split as SystemUpdate.qml.
|
|
Item {
|
|
id: root
|
|
implicitWidth: row.implicitWidth + 2
|
|
implicitHeight: Appearance.barHeight
|
|
|
|
// The bar's own PanelWindow — same barWindow-anchoring convention as
|
|
// every other popup-having module here (Volume.qml, SystemUpdate.qml,
|
|
// etc.), so the panel opens in a fixed spot rather than following this
|
|
// icon's exact position in its RowLayout.
|
|
required property var barWindow
|
|
|
|
readonly property bool busy: WeatherData.locating || WeatherData.loading
|
|
|
|
// Local to this popup instance — unlike WeatherData's own state, there's
|
|
// no need to share an in-progress search across monitors' bars.
|
|
property bool searchOpen: false
|
|
property string searchQuery: ""
|
|
|
|
RowLayout {
|
|
id: row
|
|
anchors.centerIn: parent
|
|
spacing: 4
|
|
|
|
Text {
|
|
id: label
|
|
font.family: Appearance.iconFontFamilyRounded
|
|
font.variableAxes: Appearance.iconFontAxes
|
|
font.pixelSize: 18
|
|
color: Colors.foreground
|
|
// Same spinner treatment as SystemUpdate.qml/
|
|
// BluetoothIndicator.qml's scan spinner while the location or
|
|
// conditions are still loading.
|
|
text: root.busy ? "progress_activity" : (WeatherData.hasLocation ? WeatherData.iconFor(WeatherData.weatherCode, WeatherData.isDay) : "cloud_off")
|
|
|
|
RotationAnimation on rotation {
|
|
running: root.busy
|
|
loops: Animation.Infinite
|
|
from: 0
|
|
to: 360
|
|
duration: 900
|
|
onRunningChanged: if (!running) label.rotation = 0
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.foreground
|
|
text: Math.round(WeatherData.tempC) + "°"
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onClicked: (mouse) => {
|
|
if (mouse.button === Qt.RightButton) {
|
|
WeatherData.refresh()
|
|
} else {
|
|
panel.visible = !panel.visible
|
|
grab.active = panel.visible
|
|
}
|
|
}
|
|
}
|
|
|
|
PopupWindow {
|
|
id: panel
|
|
visible: false
|
|
color: "transparent"
|
|
// Same contentWidth/contentPadding split as the other panels
|
|
// (Volume.qml, SystemUpdate.qml, etc.) rather than a single magic
|
|
// implicitWidth.
|
|
readonly property int contentWidth: 220
|
|
readonly property int contentPadding: 16
|
|
implicitWidth: contentWidth + contentPadding * 2
|
|
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
|
|
|
anchor {
|
|
window: root.barWindow
|
|
// Unlike anchor.item, anchor.window doesn't default rect to the
|
|
// window's own bounds — set it explicitly so edges/gravity below
|
|
// resolve against the full bar. Bottom alone (no Left/Right)
|
|
// anchors to the bar's bottom-center, which lines up with this
|
|
// icon since it sits in the bar's centered RowLayout.
|
|
rect.x: 0
|
|
rect.y: 0
|
|
rect.width: root.barWindow.width
|
|
rect.height: root.barWindow.height
|
|
edges: Edges.Bottom
|
|
gravity: Edges.Bottom
|
|
margins.bottom: -4
|
|
}
|
|
|
|
HyprlandFocusGrab {
|
|
id: grab
|
|
windows: [panel]
|
|
onCleared: panel.visible = false
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: 0
|
|
color: Colors.surface
|
|
border.color: Colors.muted
|
|
border.width: 3
|
|
|
|
Column {
|
|
id: mainColumn
|
|
anchors.fill: parent
|
|
anchors.margins: panel.contentPadding
|
|
spacing: 10
|
|
|
|
RowLayout {
|
|
width: parent.width
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: label.text
|
|
font.family: Appearance.iconFontFamilyRounded
|
|
font.variableAxes: Appearance.iconFontAxes
|
|
font.pixelSize: 28
|
|
color: Colors.foreground
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
text: {
|
|
if (WeatherData.locating) return "Locating…"
|
|
if (!WeatherData.hasLocation) return "Location unavailable"
|
|
return WeatherData.locationName || "Weather"
|
|
}
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: 18
|
|
font.bold: true
|
|
color: Colors.foreground
|
|
}
|
|
|
|
Text {
|
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
|
text: WeatherData.loading ? "Updating…" : "Feels like " + Math.round(WeatherData.feelsLikeC) + "°"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.muted
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
|
text: Math.round(WeatherData.tempC) + "°"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: 28
|
|
font.bold: true
|
|
color: Colors.foreground
|
|
}
|
|
|
|
// Toggles the search box below — same tap target size
|
|
// and hover feedback as the picker-target rows in
|
|
// VpnIndicator.qml.
|
|
Text {
|
|
text: root.searchOpen ? "close" : "edit_location"
|
|
font.family: Appearance.iconFontFamilyRounded
|
|
font.variableAxes: Appearance.iconFontAxes
|
|
font.pixelSize: 16
|
|
color: editLocationMouse.containsMouse ? Colors.primary : Colors.muted
|
|
|
|
MouseArea {
|
|
id: editLocationMouse
|
|
anchors.fill: parent
|
|
anchors.margins: -4
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
root.searchOpen = !root.searchOpen
|
|
root.searchQuery = ""
|
|
WeatherData.searchResults = []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// City search — always available regardless of how the
|
|
// current location was resolved (IP guess or a previous
|
|
// search), so a wrong auto-detected city is one click away
|
|
// from being fixed. Same search-box + result-list shape as
|
|
// VpnIndicator.qml's Mullvad location picker.
|
|
Column {
|
|
visible: root.searchOpen
|
|
width: parent.width
|
|
spacing: 6
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
implicitHeight: 24
|
|
radius: 0
|
|
color: Colors.surface
|
|
border.color: Colors.muted
|
|
border.width: 1
|
|
|
|
Text {
|
|
anchors.fill: parent
|
|
anchors.margins: 6
|
|
visible: citySearchInput.text.length === 0
|
|
text: "Search city…"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.muted
|
|
}
|
|
|
|
TextInput {
|
|
id: citySearchInput
|
|
anchors.fill: parent
|
|
anchors.margins: 6
|
|
color: Colors.foreground
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
clip: true
|
|
focus: root.searchOpen
|
|
text: root.searchQuery
|
|
onTextChanged: {
|
|
root.searchQuery = text
|
|
searchDebounce.restart()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: searchDebounce
|
|
interval: 400
|
|
onTriggered: WeatherData.search(root.searchQuery)
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: WeatherData.searching
|
|
text: "Searching…"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.muted
|
|
}
|
|
|
|
Text {
|
|
visible: !WeatherData.searching && citySearchInput.text.length >= 2 && WeatherData.searchResults.length === 0
|
|
text: "No matches"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.muted
|
|
}
|
|
|
|
Flickable {
|
|
width: parent.width
|
|
height: Math.min(resultsColumn.implicitHeight, 150)
|
|
contentWidth: width
|
|
contentHeight: resultsColumn.implicitHeight
|
|
clip: true
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
Column {
|
|
id: resultsColumn
|
|
width: parent.width
|
|
spacing: 1
|
|
|
|
Repeater {
|
|
model: WeatherData.searchResults
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: parent.width
|
|
implicitHeight: resultText.implicitHeight + 8
|
|
radius: 0
|
|
color: resultMouse.containsMouse ? Colors.primary : "transparent"
|
|
|
|
Text {
|
|
id: resultText
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.margins: 4
|
|
elide: Text.ElideRight
|
|
text: modelData.description ? (modelData.name + " — " + modelData.description) : modelData.name
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: resultMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
|
}
|
|
|
|
MouseArea {
|
|
id: resultMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
WeatherData.selectLocation(modelData.name, modelData.latitude, modelData.longitude)
|
|
root.searchOpen = false
|
|
root.searchQuery = ""
|
|
WeatherData.searchResults = []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Humidity / wind — same two-stat layout, just laid out as
|
|
// a row of icon+value pairs rather than full sentences.
|
|
RowLayout {
|
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
|
width: parent.width
|
|
spacing: 16
|
|
|
|
RowLayout {
|
|
spacing: 4
|
|
Text {
|
|
text: "water_drop"
|
|
font.family: Appearance.iconFontFamilyRounded
|
|
font.variableAxes: Appearance.iconFontAxes
|
|
font.pixelSize: 14
|
|
color: Colors.muted
|
|
}
|
|
Text {
|
|
text: WeatherData.humidity + "%"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.foreground
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: 4
|
|
Text {
|
|
text: "air"
|
|
font.family: Appearance.iconFontFamilyRounded
|
|
font.variableAxes: Appearance.iconFontAxes
|
|
font.pixelSize: 14
|
|
color: Colors.muted
|
|
}
|
|
Text {
|
|
text: Math.round(WeatherData.windKmh) + " km/h"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.foreground
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: WeatherData.forecast.length > 0
|
|
width: parent.width
|
|
height: 1
|
|
color: Colors.muted
|
|
}
|
|
|
|
RowLayout {
|
|
visible: WeatherData.forecast.length > 0
|
|
width: parent.width
|
|
spacing: 4
|
|
|
|
Repeater {
|
|
model: WeatherData.forecast
|
|
|
|
delegate: ColumnLayout {
|
|
required property var modelData
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: Qt.formatDateTime(new Date(modelData.date + "T12:00:00"), "ddd")
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.muted
|
|
}
|
|
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: WeatherData.iconFor(modelData.code, true)
|
|
font.family: Appearance.iconFontFamilyRounded
|
|
font.variableAxes: Appearance.iconFontAxes
|
|
font.pixelSize: 18
|
|
color: Colors.foreground
|
|
}
|
|
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: Math.round(modelData.maxC) + "°/" + Math.round(modelData.minC) + "°"
|
|
font.family: Appearance.fontFamily
|
|
font.pixelSize: Appearance.fontSizeSmall
|
|
color: Colors.foreground
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|