dashboard: add weather tab
dashboard: Added Weather tab that shows weather forecast
This commit is contained in:
parent
bdcd13222f
commit
7d92c19358
5 changed files with 446 additions and 59 deletions
|
|
@ -106,6 +106,10 @@ Item {
|
|||
index: 2
|
||||
sourceComponent: Performance {}
|
||||
}
|
||||
|
||||
Pane {
|
||||
sourceComponent: Weather {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on contentX {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ Item {
|
|||
text: qsTr("Performance")
|
||||
}
|
||||
|
||||
Tab {
|
||||
iconName: "cloud"
|
||||
text: qsTr("Weather")
|
||||
}
|
||||
|
||||
// Tab {
|
||||
// iconName: "workspaces"
|
||||
// text: qsTr("Workspaces")
|
||||
|
|
|
|||
278
modules/dashboard/Weather.qml
Normal file
278
modules/dashboard/Weather.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ?? "";
|
||||
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 => {
|
||||
function fetchWeatherData() {
|
||||
const url = getWeatherUrl();
|
||||
if (url === "")
|
||||
return;
|
||||
|
||||
Requests.get(url, text => {
|
||||
const json = JSON.parse(text);
|
||||
cc = json.current_condition[0];
|
||||
forecast = json.weather;
|
||||
})
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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: ({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue