324 lines
11 KiB
QML
324 lines
11 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Constants
|
|
import qs.Utils
|
|
pragma Singleton
|
|
|
|
// Weather logic and caching with stable UI properties
|
|
Singleton {
|
|
//console.log(JSON.stringify(weatherData))
|
|
|
|
id: root
|
|
|
|
property string locationName: "Munich"
|
|
property string locationFile: Qt.resolvedUrl("../Assets/Config/Location.json")
|
|
property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds
|
|
property bool isFetchingWeather: false
|
|
readonly property alias data: adapter // Used to access via LocationService.data.xxx from outside, best to use "adapter" inside the service.
|
|
// Stable UI properties - only updated when location is fully resolved
|
|
property bool coordinatesReady: false
|
|
property string stableLatitude: ""
|
|
property string stableLongitude: ""
|
|
property string stableName: ""
|
|
// Helper property for UI components (outside JsonAdapter to avoid binding loops)
|
|
readonly property string displayCoordinates: {
|
|
if (!root.coordinatesReady || root.stableLatitude === "" || root.stableLongitude === "")
|
|
return "";
|
|
|
|
const lat = parseFloat(root.stableLatitude).toFixed(4);
|
|
const lon = parseFloat(root.stableLongitude).toFixed(4);
|
|
return `${lat}, ${lon}`;
|
|
}
|
|
|
|
// --------------------------------
|
|
function init() {
|
|
// does nothing but ensure the singleton is created
|
|
// do not remove
|
|
Logger.log("Location", "Service started");
|
|
}
|
|
|
|
// --------------------------------
|
|
function resetWeather() {
|
|
Logger.log("Location", "Resetting weather data");
|
|
// Mark as changing to prevent UI updates
|
|
root.coordinatesReady = false;
|
|
// Reset stable properties
|
|
root.stableLatitude = "";
|
|
root.stableLongitude = "";
|
|
root.stableName = "";
|
|
// Reset core data
|
|
adapter.latitude = "";
|
|
adapter.longitude = "";
|
|
adapter.name = "";
|
|
adapter.weatherLastFetch = 0;
|
|
adapter.weather = null;
|
|
// Try to fetch immediately
|
|
updateWeather();
|
|
}
|
|
|
|
// --------------------------------
|
|
function updateWeather() {
|
|
if (isFetchingWeather) {
|
|
Logger.warn("Location", "Weather is still fetching");
|
|
return ;
|
|
}
|
|
if ((adapter.weatherLastFetch === "") || (adapter.weather === null) || (adapter.latitude === "") || (adapter.longitude === "") || (adapter.name !== root.locationName) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency))
|
|
getFreshWeather();
|
|
|
|
}
|
|
|
|
// --------------------------------
|
|
function getFreshWeather() {
|
|
isFetchingWeather = true;
|
|
// Check if location name has changed
|
|
const locationChanged = data.name !== root.locationName;
|
|
if (locationChanged) {
|
|
root.coordinatesReady = false;
|
|
Logger.log("Location", "Location changed from", adapter.name, "to", root.locationName);
|
|
}
|
|
if ((adapter.latitude === "") || (adapter.longitude === "") || locationChanged)
|
|
_geocodeLocation(root.locationName, function(latitude, longitude, name, country) {
|
|
Logger.log("Location", "Geocoded", root.locationName, "to:", latitude, "/", longitude);
|
|
// Save location name
|
|
adapter.name = root.locationName;
|
|
// Save GPS coordinates
|
|
adapter.latitude = latitude.toString();
|
|
adapter.longitude = longitude.toString();
|
|
root.stableName = `${name}, ${country}`;
|
|
_fetchWeather(latitude, longitude, errorCallback);
|
|
}, errorCallback);
|
|
else
|
|
_fetchWeather(adapter.latitude, adapter.longitude, errorCallback);
|
|
}
|
|
|
|
// --------------------------------
|
|
function _geocodeLocation(locationName, callback, errorCallback) {
|
|
Logger.log("Location", "Geocoding location name");
|
|
var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(locationName) + "&language=en&format=json";
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
var geoData = JSON.parse(xhr.responseText);
|
|
if (geoData.lat != null)
|
|
callback(geoData.lat, geoData.lng, geoData.name, geoData.country);
|
|
else
|
|
errorCallback("Location", "could not resolve location name");
|
|
} catch (e) {
|
|
errorCallback("Location", "Failed to parse geocoding data: " + e);
|
|
}
|
|
} else {
|
|
errorCallback("Location", "Geocoding error: " + xhr.status);
|
|
}
|
|
}
|
|
};
|
|
xhr.open("GET", geoUrl);
|
|
xhr.send();
|
|
}
|
|
|
|
// --------------------------------
|
|
function _fetchWeather(latitude, longitude, errorCallback) {
|
|
Logger.log("Location", "Fetching weather from api.open-meteo.com");
|
|
var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto";
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
var weatherData = JSON.parse(xhr.responseText);
|
|
// Save core data
|
|
data.weather = weatherData;
|
|
data.weatherLastFetch = Time.timestamp;
|
|
// Update stable display values only when complete and successful
|
|
root.stableLatitude = data.latitude = weatherData.latitude.toString();
|
|
root.stableLongitude = data.longitude = weatherData.longitude.toString();
|
|
root.coordinatesReady = true;
|
|
isFetchingWeather = false;
|
|
Logger.log("Location", "Cached weather to disk - stable coordinates updated");
|
|
} catch (e) {
|
|
errorCallback("Location", "Failed to parse weather data");
|
|
}
|
|
} else {
|
|
errorCallback("Location", "Weather fetch error: " + xhr.status);
|
|
}
|
|
}
|
|
};
|
|
xhr.open("GET", url);
|
|
xhr.send();
|
|
}
|
|
|
|
// --------------------------------
|
|
function errorCallback(module, message) {
|
|
Logger.error(module, message);
|
|
isFetchingWeather = false;
|
|
}
|
|
|
|
// --------------------------------
|
|
function weatherSymbolFromCode(code) {
|
|
if (code === 0)
|
|
return "weather-sun";
|
|
|
|
if (code === 1 || code === 2)
|
|
return "weather-cloud-sun";
|
|
|
|
if (code === 3)
|
|
return "weather-cloud";
|
|
|
|
if (code >= 45 && code <= 48)
|
|
return "weather-cloud-haze";
|
|
|
|
if (code >= 51 && code <= 67)
|
|
return "weather-cloud-rain";
|
|
|
|
if (code >= 71 && code <= 77)
|
|
return "weather-cloud-snow";
|
|
|
|
if (code >= 71 && code <= 77)
|
|
return "weather-cloud-snow";
|
|
|
|
if (code >= 85 && code <= 86)
|
|
return "weather-cloud-snow";
|
|
|
|
if (code >= 95 && code <= 99)
|
|
return "weather-cloud-lightning";
|
|
|
|
return "weather-cloud";
|
|
}
|
|
|
|
function weatherColorFromCode(code) {
|
|
// Clear sky - bright yellow
|
|
if (code === 0)
|
|
return Colors.yellow;
|
|
|
|
// Mainly clear/Partly cloudy - soft peach/rosewater tones
|
|
if (code === 1 || code === 2)
|
|
return Colors.peach;
|
|
|
|
// Overcast - neutral sky blue
|
|
if (code === 3)
|
|
return Colors.sky;
|
|
|
|
// Fog - soft lavender/muted tone
|
|
if (code >= 45 && code <= 48)
|
|
return Colors.lavender;
|
|
|
|
// Drizzle - light blue/sapphire
|
|
if (code >= 51 && code <= 67)
|
|
return Colors.sapphire;
|
|
|
|
// Snow - cool teal
|
|
if (code >= 71 && code <= 77)
|
|
return Colors.teal;
|
|
|
|
// Rain showers - deeper blue
|
|
if (code >= 80 && code <= 82)
|
|
return Colors.blue;
|
|
|
|
// Snow showers - teal
|
|
if (code >= 85 && code <= 86)
|
|
return Colors.teal;
|
|
|
|
// Thunderstorm - dramatic mauve/pink
|
|
if (code >= 95 && code <= 99)
|
|
return Colors.mauve;
|
|
|
|
// Default - sky blue
|
|
return Colors.sky;
|
|
}
|
|
|
|
// --------------------------------
|
|
function weatherDescriptionFromCode(code) {
|
|
if (code === 0)
|
|
return "Clear sky";
|
|
|
|
if (code === 1)
|
|
return "Mainly clear";
|
|
|
|
if (code === 2)
|
|
return "Partly cloudy";
|
|
|
|
if (code === 3)
|
|
return "Overcast";
|
|
|
|
if (code === 45 || code === 48)
|
|
return "Fog";
|
|
|
|
if (code >= 51 && code <= 67)
|
|
return "Drizzle";
|
|
|
|
if (code >= 71 && code <= 77)
|
|
return "Snow";
|
|
|
|
if (code >= 80 && code <= 82)
|
|
return "Rain showers";
|
|
|
|
if (code >= 95 && code <= 99)
|
|
return "Thunderstorm";
|
|
|
|
return "Unknown";
|
|
}
|
|
|
|
// --------------------------------
|
|
function celsiusToFahrenheit(celsius) {
|
|
return 32 + celsius * 1.8;
|
|
}
|
|
|
|
FileView {
|
|
id: locationFileView
|
|
|
|
path: locationFile
|
|
printErrors: false
|
|
onAdapterUpdated: saveTimer.start()
|
|
onLoaded: {
|
|
Logger.log("Location", "Loaded cached data");
|
|
// Initialize stable properties on load
|
|
if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
|
|
root.stableLatitude = adapter.latitude;
|
|
root.stableLongitude = adapter.longitude;
|
|
root.stableName = adapter.name;
|
|
root.coordinatesReady = true;
|
|
Logger.log("Location", "Coordinates ready");
|
|
}
|
|
updateWeather();
|
|
}
|
|
onLoadFailed: function(error) {
|
|
updateWeather();
|
|
}
|
|
|
|
JsonAdapter {
|
|
id: adapter
|
|
|
|
// Core data properties
|
|
property string latitude: ""
|
|
property string longitude: ""
|
|
property string name: ""
|
|
property int weatherLastFetch: 0
|
|
property var weather: null
|
|
}
|
|
|
|
}
|
|
|
|
// Every 20s check if we need to fetch new weather
|
|
Timer {
|
|
id: updateTimer
|
|
|
|
interval: 20 * 1000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
updateWeather();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: saveTimer
|
|
|
|
running: false
|
|
interval: 1000
|
|
onTriggered: locationFileView.writeAdapter()
|
|
}
|
|
|
|
}
|