The last two values that could only be changed by editing a file. Weather was pinned to hardcoded coordinates, so the card could not be pointed anywhere else. It is a location search now, not latitude and longitude fields: nobody knows their own coordinates, and a control that demands them is one nobody uses. Open-Meteo's geocoding endpoint needs no key, the same reason the forecast already uses them. Only the search term leaves the machine -- the stored place name is a label -- and coordinates are rounded to four decimals, far finer than a weather reading resolves and coarse enough to keep a precise home location out of the settings file. The graphics readout was hardcoded to card1. This machine has two amdgpu cards, discrete and integrated, so that was right only by luck, and the path is meaningless on any other machine. GPUs are enumerated with a readable name from lspci, since sysfs exposes only numeric ids, and the picker appears only when there is more than one to choose between. A stored path the machine does not have is refused and reported rather than silently measuring nothing. Also merges the per-application notification rules UI. Its three commits were believed integrated but the page half was not actually in the tree: main had the service side in Notifs.qml and zero references to setAppRule in NotificationsPage. Ancestry is not content. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
130 lines
4.4 KiB
QML
130 lines
4.4 KiB
QML
pragma Singleton
|
|
|
|
// Turning a place name into coordinates.
|
|
//
|
|
// The weather card needs latitude and longitude, but nobody knows their own
|
|
// coordinates, and a settings page that demands them is a settings page nobody
|
|
// changes. Open-Meteo publishes a geocoding endpoint that needs no API key and
|
|
// no account, which is the same reason the forecast itself uses them.
|
|
//
|
|
// Fetched with curl rather than XMLHttpRequest for the same reason as
|
|
// services/Weather.qml: curl is guaranteed present, and a search that fails
|
|
// must leave the page usable rather than producing an error popup.
|
|
//
|
|
// Only the query is sent. The stored location label never leaves the machine.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import qs.config
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// [{ name, admin, country, latitude, longitude, label }]
|
|
property var results: []
|
|
property bool searching: false
|
|
property string lastError: ""
|
|
property string lastQuery: ""
|
|
|
|
readonly property string endpoint: "https://geocoding-api.open-meteo.com/v1/search"
|
|
|
|
Process {
|
|
id: fetch
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.parse(this.text)
|
|
}
|
|
|
|
onExited: (exitCode, exitStatus) => {
|
|
root.searching = false;
|
|
if (exitCode !== 0)
|
|
root.lastError = "Could not reach the location service.";
|
|
}
|
|
}
|
|
|
|
// Debounced: typing "Denver" should not fire six searches.
|
|
Timer {
|
|
id: debounce
|
|
interval: 350
|
|
onTriggered: root.run()
|
|
}
|
|
|
|
property string pending: ""
|
|
|
|
function search(query: string): void {
|
|
const trimmed = String(query).trim();
|
|
root.pending = trimmed;
|
|
if (trimmed.length < 2) {
|
|
root.results = [];
|
|
root.lastError = "";
|
|
debounce.stop();
|
|
return;
|
|
}
|
|
debounce.restart();
|
|
}
|
|
|
|
function run(): void {
|
|
if (fetch.running || root.pending.length < 2)
|
|
return;
|
|
root.searching = true;
|
|
root.lastError = "";
|
|
root.lastQuery = root.pending;
|
|
|
|
// --get with --data-urlencode makes curl do the escaping, so a place
|
|
// name with spaces or an ampersand cannot alter the request.
|
|
fetch.exec(["curl", "-s", "--max-time", "10", "--get",
|
|
"--data-urlencode", `name=${root.pending}`,
|
|
"--data-urlencode", "count=8",
|
|
"--data-urlencode", "format=json",
|
|
root.endpoint]);
|
|
}
|
|
|
|
function parse(text: string): void {
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
const out = [];
|
|
for (const item of (parsed.results ?? [])) {
|
|
if (typeof item.latitude !== "number" || typeof item.longitude !== "number")
|
|
continue;
|
|
const admin = item.admin1 ?? "";
|
|
const country = item.country ?? "";
|
|
out.push({
|
|
name: item.name ?? "",
|
|
admin: admin,
|
|
country: country,
|
|
latitude: item.latitude,
|
|
longitude: item.longitude,
|
|
// What the user will see stored as their location label.
|
|
label: [item.name, admin, country].filter(part => !!part).join(", ")
|
|
});
|
|
}
|
|
root.results = out;
|
|
root.lastError = out.length === 0 ? "No places match that name." : "";
|
|
} catch (error) {
|
|
root.results = [];
|
|
root.lastError = "The location service returned something unreadable.";
|
|
}
|
|
}
|
|
|
|
// Stores a chosen place. Coordinates are rounded to four decimals -- roughly
|
|
// ten metres, far finer than a weather reading resolves, and it keeps a
|
|
// precise home location out of the settings file.
|
|
function choose(place: var): bool {
|
|
const latitude = Math.round(place.latitude * 10000) / 10000;
|
|
const longitude = Math.round(place.longitude * 10000) / 10000;
|
|
const label = String(place.label).slice(0, 64);
|
|
|
|
const ok = DesktopPreferences.set("weatherLatitude", latitude)
|
|
&& DesktopPreferences.set("weatherLongitude", longitude)
|
|
&& DesktopPreferences.set("weatherLocation", label);
|
|
if (!ok) {
|
|
root.lastError = "That location could not be saved.";
|
|
return false;
|
|
}
|
|
root.results = [];
|
|
root.lastError = "";
|
|
return true;
|
|
}
|
|
}
|