dashboard: add weather tab

dashboard: Added Weather tab that shows weather forecast
This commit is contained in:
Ezekiel Gonzales 2026-01-03 15:25:11 +08:00 committed by GitHub
parent bdcd13222f
commit 7d92c19358
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 446 additions and 59 deletions

View file

@ -106,6 +106,10 @@ Item {
index: 2
sourceComponent: Performance {}
}
Pane {
sourceComponent: Weather {}
}
}
Behavior on contentX {

View file

@ -45,6 +45,11 @@ Item {
text: qsTr("Performance")
}
Tab {
iconName: "cloud"
text: qsTr("Weather")
}
// Tab {
// iconName: "workspaces"
// text: qsTr("Workspaces")

View file

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

View file

@ -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();
}
});
}
}
onCityChanged: Requests.get(`https://wttr.in/${city}?format=j1`, text => {
const json = JSON.parse(text);
cc = json.current_condition[0];
forecast = json.weather;
})
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();
}
});
}
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

View file

@ -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: ({