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 } } } } } } } }