diff options
| author | Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> | 2026-01-03 15:25:11 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-03 18:25:11 +1100 |
| commit | 7d92c19358fe92c19870b75b3b6f06e73ed2da78 (patch) | |
| tree | 3c071fc1570ace6db5a4f87e0e1a38e398bb6d30 | |
| parent | Merge pull request #906 from atdma/main (diff) | |
| download | caelestia-shell-7d92c19358fe92c19870b75b3b6f06e73ed2da78.tar.gz caelestia-shell-7d92c19358fe92c19870b75b3b6f06e73ed2da78.tar.bz2 caelestia-shell-7d92c19358fe92c19870b75b3b6f06e73ed2da78.zip | |
dashboard: add weather tab
dashboard: Added Weather tab that shows weather forecast
| -rw-r--r-- | modules/dashboard/Content.qml | 4 | ||||
| -rw-r--r-- | modules/dashboard/Tabs.qml | 5 | ||||
| -rw-r--r-- | modules/dashboard/Weather.qml | 278 | ||||
| -rw-r--r-- | services/Weather.qml | 142 | ||||
| -rw-r--r-- | utils/Icons.qml | 76 |
5 files changed, 446 insertions, 59 deletions
diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 707bee3..7809a27 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -106,6 +106,10 @@ Item { index: 2 sourceComponent: Performance {} } + + Pane { + sourceComponent: Weather {} + } } Behavior on contentX { diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index aecb4fa..98ea880 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -45,6 +45,11 @@ Item { text: qsTr("Performance") } + Tab { + iconName: "cloud" + text: qsTr("Weather") + } + // Tab { // iconName: "workspaces" // text: qsTr("Workspaces") diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/Weather.qml new file mode 100644 index 0000000..fd1caa4 --- /dev/null +++ b/modules/dashboard/Weather.qml @@ -0,0 +1,278 @@ +import qs.components +import qs.services +import qs.config +import qs.utils +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 + implicitHeight: layout.implicitHeight + + readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null + + Component.onCompleted: Weather.reload() + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.smaller + + RowLayout { + Layout.leftMargin: Appearance.padding.large + Layout.rightMargin: Appearance.padding.large + Layout.fillWidth: true + + Column { + spacing: Appearance.spacing.small / 2 + + StyledText { + text: Weather.city || qsTr("Loading...") + font.pointSize: Appearance.font.size.extraLarge + font.weight: 600 + color: Colours.palette.m3onSurface + } + + StyledText { + text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } + + Item { + Layout.fillWidth: true + } + + Row { + spacing: Appearance.spacing.large + + WeatherStat { + icon: "wb_twilight" + label: "Sunrise" + value: Weather.sunrise + colour: Colours.palette.m3tertiary + } + + WeatherStat { + icon: "bedtime" + label: "Sunset" + value: Weather.sunset + colour: Colours.palette.m3tertiary + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.large * 2 + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: bigInfoRow + + anchors.centerIn: parent + spacing: Appearance.spacing.large + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: Weather.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + color: Colours.palette.m3secondary + animate: true + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + spacing: -Appearance.spacing.small + + StyledText { + text: Weather.temp + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 500 + color: Colours.palette.m3primary + } + + StyledText { + Layout.leftMargin: Appearance.padding.small + text: Weather.description + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + DetailCard { + icon: "water_drop" + label: "Humidity" + value: Weather.humidity + "%" + colour: Colours.palette.m3secondary + } + DetailCard { + icon: "thermostat" + label: "Feels Like" + value: Weather.feelsLike + colour: Colours.palette.m3primary + } + DetailCard { + icon: "air" + label: "Wind" + value: Weather.windSpeed ? Weather.windSpeed + " km/h" : "--" + colour: Colours.palette.m3tertiary + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + visible: forecastRepeater.count > 0 + text: qsTr("7-Day Forecast") + font.pointSize: Appearance.font.size.normal + font.weight: 600 + color: Colours.palette.m3onSurface + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + Repeater { + id: forecastRepeater + + model: Weather.forecast + + StyledRect { + id: forecastItem + + required property int index + required property var modelData + + Layout.fillWidth: true + implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: forecastItemColumn + + anchors.centerIn: parent + spacing: Appearance.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") + font.pointSize: Appearance.font.size.normal + font.weight: 600 + color: Colours.palette.m3primary + } + + StyledText { + Layout.topMargin: -Appearance.spacing.small / 2 + Layout.alignment: Qt.AlignHCenter + text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") + font.pointSize: Appearance.font.size.small + opacity: 0.7 + color: Colours.palette.m3onSurfaceVariant + } + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.modelData.icon + font.pointSize: Appearance.font.size.extraLarge + color: Colours.palette.m3secondary + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" + font.weight: 600 + color: Colours.palette.m3tertiary + } + } + } + } + } + } + + component DetailCard: StyledRect { + id: detailRoot + + property string icon + property string label + property string value + property color colour + + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Appearance.rounding.small + color: Colours.palette.m3surfaceContainer + + Row { + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + text: detailRoot.icon + color: detailRoot.colour + font.pointSize: Appearance.font.size.large + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + text: detailRoot.label + font.pointSize: Appearance.font.size.smaller + opacity: 0.7 + horizontalAlignment: Text.AlignLeft + } + StyledText { + text: detailRoot.value + font.weight: 600 + horizontalAlignment: Text.AlignLeft + } + } + } + } + + component WeatherStat: Row { + id: weatherStat + + property string icon + property string label + property string value + property color colour + spacing: Appearance.spacing.small + + MaterialIcon { + text: weatherStat.icon + font.pointSize: Appearance.font.size.extraLarge + color: weatherStat.colour + } + Column { + StyledText { + text: weatherStat.label + font.pointSize: Appearance.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + StyledText { + text: weatherStat.value + font.pointSize: Appearance.font.size.small + font.weight: 600 + color: Colours.palette.m3onSurface + } + } + } +} diff --git a/services/Weather.qml b/services/Weather.qml index 73e0b77..bc1f11c 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -10,29 +10,149 @@ Singleton { id: root property string city + property string loc property var cc property var forecast + readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" - readonly property string description: cc?.weatherDesc[0].value ?? qsTr("No weather") + readonly property string description: cc?.weatherDesc ?? qsTr("No weather") readonly property string temp: Config.services.useFahrenheit ? `${cc?.temp_F ?? 0}°F` : `${cc?.temp_C ?? 0}°C` readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.FeelsLikeF ?? 0}°F` : `${cc?.FeelsLikeC ?? 0}°C` readonly property int humidity: cc?.humidity ?? 0 + readonly property real windSpeed: (cc && cc.windSpeed !== undefined) ? cc.windSpeed : 0.0 + readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" + readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" function reload(): void { - if (Config.services.weatherLocation) - city = Config.services.weatherLocation; - else if (!city || timer.elapsed() > 900) + let configLocation = Config.services.weatherLocation; + + if (configLocation && configLocation !== "") { + if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) + loc = configLocation; + else + fetchCoordsFromCity(configLocation); + } else if (!loc || timer.elapsed() > 900) { Requests.get("https://ipinfo.io/json", text => { - city = JSON.parse(text).city ?? ""; - timer.restart(); + const response = JSON.parse(text); + if (response.loc) { + loc = response.loc; + city = response.city ?? ""; + timer.restart(); + } }); + } + } + + function fetchCoordsFromCity(cityName) { + const url = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(cityName) + "&count=1&language=en&format=json"; + + Requests.get(url, text => { + const json = JSON.parse(text); + if (json.results && json.results.length > 0) { + const result = json.results[0]; + loc = result.latitude + "," + result.longitude; + city = result.name; + } else { + loc = ""; + reload(); + } + }); } - onCityChanged: Requests.get(`https://wttr.in/${city}?format=j1`, text => { - const json = JSON.parse(text); - cc = json.current_condition[0]; - forecast = json.weather; - }) + function fetchWeatherData() { + const url = getWeatherUrl(); + if (url === "") + return; + + Requests.get(url, text => { + const json = JSON.parse(text); + if (!json.current || !json.daily) + return; + + cc = { + "weatherCode": String(json.current.weather_code), + "weatherDesc": getWeatherCondition(String(json.current.weather_code)), + "temp_C": Math.round(json.current.temperature_2m), + "temp_F": Math.round(json.current.temperature_2m * 9 / 5 + 32), + "FeelsLikeC": Math.round(json.current.apparent_temperature), + "FeelsLikeF": Math.round(json.current.apparent_temperature * 9 / 5 + 32), + "humidity": json.current.relative_humidity_2m, + "windSpeed": json.current.wind_speed_10m, + "isDay": json.current.is_day, + "sunrise": json.daily.sunrise[0], + "sunset": json.daily.sunset[0] + }; + + const forecastList = []; + for (let i = 0; i < json.daily.time.length; i++) + forecastList.push({ + "date": json.daily.time[i], + "maxTempC": Math.round(json.daily.temperature_2m_max[i]), + "maxTempF": Math.round(json.daily.temperature_2m_max[i] * 9 / 5 + 32), + "minTempC": Math.round(json.daily.temperature_2m_min[i]), + "minTempF": Math.round(json.daily.temperature_2m_min[i] * 9 / 5 + 32), + "weatherCode": String(json.daily.weather_code[i]), + "icon": Icons.getWeatherIcon(String(json.daily.weather_code[i])) + }); + + forecast = forecastList; + }); + } + + function getWeatherUrl() { + if (!loc || loc.indexOf(",") === -1) + return ""; + + const [lat, lon] = loc.split(","); + const baseUrl = "https://api.open-meteo.com/v1/forecast"; + const params = ["latitude=" + lat, "longitude=" + lon, "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; + + return baseUrl + "?" + params.join("&"); + } + + function getWeatherCondition(code: string): string { + const conditions = { + "0": "Clear", + "1": "Clear", + "2": "Partly cloudy", + "3": "Overcast", + "45": "Fog", + "48": "Fog", + "51": "Drizzle", + "53": "Drizzle", + "55": "Drizzle", + "56": "Freezing drizzle", + "57": "Freezing drizzle", + "61": "Light rain", + "63": "Rain", + "65": "Heavy rain", + "66": "Light rain", + "67": "Heavy rain", + "71": "Light snow", + "73": "Snow", + "75": "Heavy snow", + "77": "Snow", + "80": "Light rain", + "81": "Rain", + "82": "Heavy rain", + "85": "Light snow showers", + "86": "Heavy snow showers", + "95": "Thunderstorm", + "96": "Thunderstorm with hail", + "99": "Thunderstorm with hail" + }; + return conditions[code] || "Unknown"; + } + + onLocChanged: fetchWeatherData() + + // Refresh current location hourly + Timer { + interval: 3600000 // 1 hour + running: true + repeat: true + onTriggered: fetchWeatherData() + } ElapsedTimer { id: timer diff --git a/utils/Icons.qml b/utils/Icons.qml index b3f00c5..c06cbf8 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -9,54 +9,34 @@ Singleton { id: root readonly property var weatherIcons: ({ - "113": "clear_day", - "116": "partly_cloudy_day", - "119": "cloud", - "122": "cloud", - "143": "foggy", - "176": "rainy", - "179": "rainy", - "182": "rainy", - "185": "rainy", - "200": "thunderstorm", - "227": "cloudy_snowing", - "230": "snowing_heavy", - "248": "foggy", - "260": "foggy", - "263": "rainy", - "266": "rainy", - "281": "rainy", - "284": "rainy", - "293": "rainy", - "296": "rainy", - "299": "rainy", - "302": "weather_hail", - "305": "rainy", - "308": "weather_hail", - "311": "rainy", - "314": "rainy", - "317": "rainy", - "320": "cloudy_snowing", - "323": "cloudy_snowing", - "326": "cloudy_snowing", - "329": "snowing_heavy", - "332": "snowing_heavy", - "335": "snowing", - "338": "snowing_heavy", - "350": "rainy", - "353": "rainy", - "356": "rainy", - "359": "weather_hail", - "362": "rainy", - "365": "rainy", - "368": "cloudy_snowing", - "371": "snowing", - "374": "rainy", - "377": "rainy", - "386": "thunderstorm", - "389": "thunderstorm", - "392": "thunderstorm", - "395": "snowing" + "0": "clear_day", + "1": "clear_day", + "2": "partly_cloudy_day", + "3": "cloud", + "45": "foggy", + "48": "foggy", + "51": "rainy", + "53": "rainy", + "55": "rainy", + "56": "rainy", + "57": "rainy", + "61": "rainy", + "63": "rainy", + "65": "rainy", + "66": "rainy", + "67": "rainy", + "71": "cloudy_snowing", + "73": "cloudy_snowing", + "75": "snowing_heavy", + "77": "cloudy_snowing", + "80": "rainy", + "81": "rainy", + "82": "rainy", + "85": "cloudy_snowing", + "86": "snowing_heavy", + "95": "thunderstorm", + "96": "thunderstorm", + "99": "thunderstorm" }) readonly property var categoryIcons: ({ |