summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEzekiel Gonzales <141341590+notsoeazy@users.noreply.github.com>2026-01-03 15:25:11 +0800
committerGitHub <noreply@github.com>2026-01-03 18:25:11 +1100
commit7d92c19358fe92c19870b75b3b6f06e73ed2da78 (patch)
tree3c071fc1570ace6db5a4f87e0e1a38e398bb6d30
parentMerge pull request #906 from atdma/main (diff)
downloadcaelestia-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.qml4
-rw-r--r--modules/dashboard/Tabs.qml5
-rw-r--r--modules/dashboard/Weather.qml278
-rw-r--r--services/Weather.qml142
-rw-r--r--utils/Icons.qml76
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: ({