colour -> color, behaviour -> behavior, centre -> center, favourite -> favorite, and about twenty other pairs, applied consistently across comments, docs, error/UI copy, and a handful of QML identifiers that used the British spelling as their actual name: SystemSettings' serialiseValue/serialiseTable/normaliseGradient, Displays' normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's _normalise helper, and ShortcutCapture's cancelled signal (with its onCancelled handler in ShortcutsPage.qml). Every call site and the two tests that assert on the literal source text (settings-ownership and settings-backup-live contracts) were updated in lockstep. Left untouched: config/dot/espanso/match/packages/misspell-en/ is a vendored third-party autocorrect dictionary -- its entries are typo corrections, not our prose, and rewriting them would fight the package's own purpose (and any future re-sync from upstream). The already-American `favorites` property (Home page pinned accessories) was never actually misspelled -- only nearby comments and error strings said "favourites" -- so no data migration was needed there. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
136 lines
4.7 KiB
QML
136 lines
4.7 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.";
|
|
|
|
// Typing kept going while this fetch was in flight -- rather than
|
|
// leaving the newer query stranded until another keystroke, go
|
|
// fetch it now. run() no-ops if pending is now too short.
|
|
if (root.pending !== root.lastQuery)
|
|
root.run();
|
|
}
|
|
}
|
|
|
|
// 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 meters, 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;
|
|
}
|
|
}
|