aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/user/UserPage.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 10:29:49 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 10:29:49 +0200
commite1313ee7abb93a8a20f978ac152b462c9ef74a43 (patch)
tree3c9828631ec4e21b801b3bd280b0847248308b0a /desktop/modules/user/UserPage.qml
parentd445397ff656c57589499f7a2215a0fe12a596c3 (diff)
downloadquickshell-e1313ee7abb93a8a20f978ac152b462c9ef74a43.tar.gz
quickshell-e1313ee7abb93a8a20f978ac152b462c9ef74a43.zip
feat(user): location suggestions from OpenStreetMap
There is no place database on this machine, and the user chose Nominatim over a tz city list for coverage. It is asked 1s after typing stops (its policy: one request a second, an identifying User-Agent, nothing personal sent), from three characters, in the desktop's language, and a reply to an older query is dropped. places() turns its JSON into 'place, region, country' labels and treats an error page as no suggestions; test_user.js covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/user/UserPage.qml')
-rw-r--r--desktop/modules/user/UserPage.qml74
1 files changed, 71 insertions, 3 deletions
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 = [];
+ }
+ }
+ }
}
}