aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--desktop/modules/user/User.js18
-rw-r--r--desktop/modules/user/UserPage.qml74
-rw-r--r--desktop/modules/user/test_user.js22
3 files changed, 110 insertions, 4 deletions
diff --git a/desktop/modules/user/User.js b/desktop/modules/user/User.js
index 8160b7f..5c3d5a2 100644
--- a/desktop/modules/user/User.js
+++ b/desktop/modules/user/User.js
@@ -44,3 +44,21 @@ function gitName(name) {
if (w.length < 2) return w.join(" ");
return w.slice(0, -1).join(" ") + " " + Array.from(w[w.length - 1])[0] + ".";
}
+
+// Nominatim jsonv2 search results as location labels, "place, region,
+// country", deduplicated. Anything that is not a JSON array (an error page,
+// a rate-limit notice) is no suggestions rather than an error.
+function places(text) {
+ let rows;
+ try { rows = JSON.parse(text); } catch (e) { return []; }
+ if (!Array.isArray(rows)) return [];
+ const out = [];
+ for (const r of rows) {
+ const a = r.address || {};
+ const parts = [a.city || a.town || a.village || a.hamlet || a.municipality || r.name,
+ a.state, a.country].filter(p => p);
+ const label = parts.filter((p, i) => parts.indexOf(p) === i).join(", ");
+ if (label && !out.includes(label)) out.push(label);
+ }
+ return out;
+}
diff --git a/desktop/modules/user/UserPage.qml b/desktop/modules/user/UserPage.qml
index 5e2e448..9fb81a6 100644
--- a/desktop/modules/user/UserPage.qml
+++ b/desktop/modules/user/UserPage.qml
@@ -33,7 +33,7 @@ Column {
readonly property var fields: [
{ key: "name", label: "Name", kind: "text" },
{ key: "email", label: "Email", kind: "text" },
- { key: "location", label: "Location", kind: "text" },
+ { key: "location", label: "Location", kind: "place" },
{ key: "language", label: "Language", kind: "locale" },
{ key: "gitname", label: "Name", kind: "derived", section: "Git" },
{ key: "gitemail", label: "Email", kind: "derived", section: "Git" },
@@ -137,6 +137,35 @@ Column {
});
}
+ // Location suggestions from OpenStreetMap's Nominatim, the user's choice
+ // over an offline city list. Its policy allows one request a second and
+ // asks for an identifying User-Agent: the timer fires 1s after typing
+ // stops, and a reply to an older query is dropped by its sequence number.
+ property var places: []
+ property string placeQuery: ""
+ property int placeSeq: 0
+
+ function lookup(text) {
+ form.places = [];
+ form.placeQuery = text.trim();
+ placeTimer.restart();
+ }
+
+ Timer {
+ id: placeTimer
+ interval: 1000
+ onTriggered: {
+ if (form.placeQuery.length < 3) return;
+ const seq = ++form.placeSeq;
+ const lang = (Quickshell.env("LANG") || "en").slice(0, 2);
+ form.run(["curl", "-sf", "--max-time", "5",
+ "-A", "quickshell-drawer/1.0 (user details page)",
+ "https://nominatim.openstreetmap.org/search?format=jsonv2&addressdetails=1&limit=6"
+ + "&accept-language=" + lang + "&q=" + encodeURIComponent(form.placeQuery)],
+ out => { if (seq === form.placeSeq) form.places = U.places(out); });
+ }
+ }
+
Component.onCompleted: {
form.load({});
form.run([form.ctl, "keys"], out => form.keys = U.keys(out));
@@ -250,7 +279,7 @@ Column {
// Read-only fields are dimmer, but still selectable for copying.
Rectangle {
- visible: field.modelData.kind === "text" || field.derived
+ visible: field.modelData.kind === "text" || field.modelData.kind === "place" || field.derived
width: parent.width
height: 34
radius: 8
@@ -265,7 +294,46 @@ Column {
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
text: field.derived ? form.derived(field.modelData.key)
: form.draft[field.modelData.key] ?? ""
- onTextEdited: form.set(field.modelData.key, text)
+ onTextEdited: {
+ form.set(field.modelData.key, text);
+ if (field.modelData.kind === "place") form.lookup(text);
+ }
+ onActiveFocusChanged: if (!activeFocus && field.modelData.kind === "place") form.places = []
+ }
+ }
+
+ // Suggestions under a place field; a click takes one.
+ Column {
+ visible: field.modelData.kind === "place" && form.places.length > 0
+ width: parent.width
+ Repeater {
+ model: field.modelData.kind === "place" ? form.places : []
+ delegate: Rectangle {
+ required property string modelData
+ width: parent.width
+ height: 30
+ radius: 6
+ color: placeArea.containsMouse ? Qt.alpha(Theme.accent, 0.22) : Qt.alpha(Theme.surface, 0.6)
+ Text {
+ anchors { fill: parent; leftMargin: 10; rightMargin: 10 }
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ text: parent.modelData
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ }
+ MouseArea {
+ id: placeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ form.set(field.modelData.key, parent.modelData);
+ form.placeSeq++;
+ form.places = [];
+ }
+ }
+ }
}
}
diff --git a/desktop/modules/user/test_user.js b/desktop/modules/user/test_user.js
index 3ab22a1..3f1a73d 100644
--- a/desktop/modules/user/test_user.js
+++ b/desktop/modules/user/test_user.js
@@ -18,7 +18,7 @@ const assert = require("assert");
const src = fs.readFileSync(__dirname + "/User.js", "utf8")
.replace(/^\.pragma library$/m, "");
-const U = new Function(src + "\nreturn { parse, keys, locales, gitName };")();
+const U = new Function(src + "\nreturn { parse, keys, locales, gitName, places };")();
// parse: key=value lines; a value may itself hold "=".
assert.deepStrictEqual(U.parse("name=Jane Doèr\nlocation=a=b\n\n"),
@@ -44,4 +44,24 @@ assert.strictEqual(U.gitName("Ève Àlba"), "Ève À.");
assert.strictEqual(U.gitName(" Jane Doèr "), "Jane D.");
assert.strictEqual(U.gitName(""), "");
+// places: Nominatim jsonv2 results as "place, region, country", the most
+// specific settlement name first, duplicates and repeated parts dropped.
+const nominatim = JSON.stringify([
+ { name: "Roma", address: { city: "Roma", state: "Lazio", country: "Italia" } },
+ { name: "Rome", address: { town: "Rome", county: "Floyd County", state: "Georgia", country: "United States" } },
+ { name: "Roma", address: { city: "Roma", state: "Lazio", country: "Italia" } },
+ { name: "Monaco", address: { city: "Monaco", country: "Monaco" } },
+ { name: "Piazza", address: { village: "Piazza", country: "Italia" } },
+ { name: "Somewhere" },
+]);
+assert.deepStrictEqual(U.places(nominatim), [
+ "Roma, Lazio, Italia",
+ "Rome, Georgia, United States",
+ "Monaco",
+ "Piazza, Italia",
+ "Somewhere",
+]);
+assert.deepStrictEqual(U.places(""), []);
+assert.deepStrictEqual(U.places("<html>rate limited</html>"), []);
+
console.log("test_user: ok");