forked from MapComplete/MapComplete
UX: finetune 'go to your geolocation' interaction on theme introduction panel, fix #1583
This commit is contained in:
parent
c8df0170cc
commit
17b85195a2
4 changed files with 154 additions and 91 deletions
|
@ -344,6 +344,8 @@
|
||||||
},
|
},
|
||||||
"useSearch": "Use the search above to see presets",
|
"useSearch": "Use the search above to see presets",
|
||||||
"useSearchForMore": "Use the search function to search within {total} more values…",
|
"useSearchForMore": "Use the search function to search within {total} more values…",
|
||||||
|
"waitingForGeopermission": "Waiting for your permission to use the geolocation...",
|
||||||
|
"waitingForLocation": "Searching your current location...",
|
||||||
"weekdays": {
|
"weekdays": {
|
||||||
"abbreviations": {
|
"abbreviations": {
|
||||||
"friday": "Fri",
|
"friday": "Fri",
|
||||||
|
|
|
@ -858,6 +858,10 @@ video {
|
||||||
margin-right: 3rem;
|
margin-right: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mr-2 {
|
.mr-2 {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -886,10 +890,6 @@ video {
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-4 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-1 {
|
.ml-1 {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -2662,6 +2662,46 @@ a.link-underline {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-safe\:animate-spin {
|
||||||
|
-webkit-animation: spin 1s linear infinite;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-reduce\:animate-spin {
|
||||||
|
-webkit-animation: spin 1s linear infinite;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.max-\[480px\]\:w-full {
|
.max-\[480px\]\:w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { UIEventSource } from "../UIEventSource"
|
import { UIEventSource } from "../UIEventSource";
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Web/LocalStorageSource";
|
||||||
import { QueryParameters } from "../Web/QueryParameters"
|
import { QueryParameters } from "../Web/QueryParameters";
|
||||||
|
|
||||||
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
|
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
|
||||||
|
|
||||||
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
||||||
id: "gps"
|
id: "gps";
|
||||||
"user:location": "yes"
|
"user:location": "yes";
|
||||||
date: string
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,22 +23,22 @@ export class GeoLocationState {
|
||||||
*/
|
*/
|
||||||
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
|
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
|
||||||
"prompt"
|
"prompt"
|
||||||
)
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Important to determine e.g. if we move automatically on fix or not
|
* Important to determine e.g. if we move automatically on fix or not
|
||||||
*/
|
*/
|
||||||
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined)
|
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined);
|
||||||
/**
|
/**
|
||||||
* If true: the map will center (and re-center) to this location
|
* If true: the map will center (and re-center) to this location
|
||||||
*/
|
*/
|
||||||
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true)
|
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The latest GeoLocationCoordinates, as given by the WebAPI
|
* The latest GeoLocationCoordinates, as given by the WebAPI
|
||||||
*/
|
*/
|
||||||
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
|
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
|
||||||
new UIEventSource<GeolocationCoordinates | undefined>(undefined)
|
new UIEventSource<GeolocationCoordinates | undefined>(undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
|
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
|
||||||
|
@ -50,69 +50,50 @@ export class GeoLocationState {
|
||||||
*/
|
*/
|
||||||
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
|
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
|
||||||
LocalStorageSource.Get("geolocation-permissions")
|
LocalStorageSource.Get("geolocation-permissions")
|
||||||
)
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to detect a permission retraction
|
* Used to detect a permission retraction
|
||||||
*/
|
*/
|
||||||
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const self = this
|
const self = this;
|
||||||
|
|
||||||
this.permission.addCallbackAndRunD(async (state) => {
|
this.permission.addCallbackAndRunD(async (state) => {
|
||||||
|
console.trace("GEOPERMISSION", state)
|
||||||
if (state === "granted") {
|
if (state === "granted") {
|
||||||
self._previousLocationGrant.setData("true")
|
self._previousLocationGrant.setData("true");
|
||||||
self._grantedThisSession.setData(true)
|
self._grantedThisSession.setData(true);
|
||||||
}
|
}
|
||||||
if (state === "prompt" && self._grantedThisSession.data) {
|
if (state === "prompt" && self._grantedThisSession.data) {
|
||||||
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
|
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
|
||||||
// This means that the rights have been revoked again!
|
// This means that the rights have been revoked again!
|
||||||
// self.permission.setData("denied")
|
self._previousLocationGrant.setData("false");
|
||||||
self._previousLocationGrant.setData("false")
|
self.permission.setData("denied");
|
||||||
self.permission.setData("denied")
|
self.currentGPSLocation.setData(undefined);
|
||||||
self.currentGPSLocation.setData(undefined)
|
console.warn("Detected a downgrade in permissions!");
|
||||||
console.warn("Detected a downgrade in permissions!")
|
|
||||||
}
|
}
|
||||||
if (state === "denied") {
|
if (state === "denied") {
|
||||||
self._previousLocationGrant.setData("false")
|
self._previousLocationGrant.setData("false");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
console.log("Previous location grant:", this._previousLocationGrant.data)
|
console.log("Previous location grant:", this._previousLocationGrant.data);
|
||||||
if (this._previousLocationGrant.data === "true") {
|
if (this._previousLocationGrant.data === "true") {
|
||||||
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
|
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
|
||||||
|
|
||||||
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
|
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
|
||||||
this._previousLocationGrant.setData("false")
|
this._previousLocationGrant.setData("false");
|
||||||
console.log("Requesting access to GPS as this was previously granted")
|
console.log("Requesting access to GPS as this was previously granted");
|
||||||
const latLonGivenViaUrl =
|
const latLonGivenViaUrl =
|
||||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
|
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon");
|
||||||
if (!latLonGivenViaUrl) {
|
if (!latLonGivenViaUrl) {
|
||||||
this.requestMoment.setData(new Date())
|
this.requestMoment.setData(new Date());
|
||||||
}
|
}
|
||||||
this.requestPermission()
|
this.requestPermission();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs the listener for updates
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async startWatching() {
|
|
||||||
const self = this
|
|
||||||
navigator.geolocation.watchPosition(
|
|
||||||
function (position) {
|
|
||||||
self.currentGPSLocation.setData(position.coords)
|
|
||||||
self._previousLocationGrant.setData("true")
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
console.warn("Could not get location with navigator.geolocation")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enableHighAccuracy: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the user to allow access to their position.
|
* Requests the user to allow access to their position.
|
||||||
* When granted, will be written to the 'geolocationState'.
|
* When granted, will be written to the 'geolocationState'.
|
||||||
|
@ -121,33 +102,57 @@ export class GeoLocationState {
|
||||||
public requestPermission() {
|
public requestPermission() {
|
||||||
if (typeof navigator === "undefined") {
|
if (typeof navigator === "undefined") {
|
||||||
// Not compatible with this browser
|
// Not compatible with this browser
|
||||||
this.permission.setData("denied")
|
this.permission.setData("denied");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
|
if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
|
||||||
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
|
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
|
||||||
// Hence that we continue the flow if it is "requested"
|
// Hence that we continue the flow if it is "requested"
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.permission.setData("requested")
|
this.permission.setData("requested");
|
||||||
try {
|
try {
|
||||||
navigator?.permissions
|
navigator?.permissions
|
||||||
?.query({ name: "geolocation" })
|
?.query({ name: "geolocation" })
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
console.log("Status update: received geolocation permission is ", status.state)
|
const self = this;
|
||||||
this.permission.setData(status.state)
|
if(status.state === "granted" || status.state === "denied"){
|
||||||
const self = this
|
|
||||||
status.onchange = function () {
|
|
||||||
self.permission.setData(status.state)
|
self.permission.setData(status.state)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
status.addEventListener("change", (e) => {
|
||||||
|
self.permission.setData(status.state);
|
||||||
|
|
||||||
|
});
|
||||||
|
// The code above might have reset it to 'prompt', but we _did_ request permission!
|
||||||
this.permission.setData("requested")
|
this.permission.setData("requested")
|
||||||
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
|
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
|
||||||
self.startWatching()
|
self.startWatching();
|
||||||
})
|
})
|
||||||
.catch((e) => console.error("Could not get geopermission", e))
|
.catch((e) => console.error("Could not get geopermission", e));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not get permission:", e)
|
console.error("Could not get permission:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs the listener for updates
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async startWatching() {
|
||||||
|
const self = this;
|
||||||
|
navigator.geolocation.watchPosition(
|
||||||
|
function(position) {
|
||||||
|
self.currentGPSLocation.setData(position.coords);
|
||||||
|
self._previousLocationGrant.setData("true");
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
console.warn("Could not get location with navigator.geolocation");
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,44 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations";
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg";
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte";
|
||||||
import NextButton from "../Base/NextButton.svelte"
|
import NextButton from "../Base/NextButton.svelte";
|
||||||
import Geosearch from "./Geosearch.svelte"
|
import Geosearch from "./Geosearch.svelte";
|
||||||
import IfNot from "../Base/IfNot.svelte"
|
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
import ThemeViewState from "../../Models/ThemeViewState";
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||||
import If from "../Base/If.svelte"
|
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { twJoin } from "tailwind-merge";
|
||||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { Utils } from "../../Utils";
|
||||||
import { twJoin } from "tailwind-merge"
|
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
|
||||||
import { Utils } from "../../Utils"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The theme introduction panel
|
* The theme introduction panel
|
||||||
*/
|
*/
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState;
|
||||||
let layout = state.layout
|
let layout = state.layout;
|
||||||
let selectedElement = state.selectedElement
|
let selectedElement = state.selectedElement;
|
||||||
let selectedLayer = state.selectedLayer
|
let selectedLayer = state.selectedLayer;
|
||||||
|
|
||||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
|
||||||
let searchEnabled = false
|
let searchEnabled = false;
|
||||||
|
|
||||||
|
let geopermission: Store<GeolocationPermissionState> = state.geolocation.geolocationState.permission;
|
||||||
|
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation;
|
||||||
|
|
||||||
|
geopermission.addCallback(perm => console.log(">>>> Permission", perm));
|
||||||
|
|
||||||
function jumpToCurrentLocation() {
|
function jumpToCurrentLocation() {
|
||||||
const glstate = state.geolocation.geolocationState
|
const glstate = state.geolocation.geolocationState;
|
||||||
if (glstate.currentGPSLocation.data !== undefined) {
|
if (glstate.currentGPSLocation.data !== undefined) {
|
||||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
|
||||||
state.guistate.themeIsOpened.setData(false)
|
state.guistate.themeIsOpened.setData(false);
|
||||||
const coor = { lon: c.longitude, lat: c.latitude }
|
const coor = { lon: c.longitude, lat: c.latitude };
|
||||||
state.mapProperties.location.setData(coor)
|
state.mapProperties.location.setData(coor);
|
||||||
}
|
}
|
||||||
if (glstate.permission.data !== "granted") {
|
if (glstate.permission.data !== "granted") {
|
||||||
glstate.requestPermission()
|
glstate.requestPermission();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -58,12 +62,24 @@
|
||||||
</NextButton>
|
</NextButton>
|
||||||
|
|
||||||
<div class="flex w-full flex-wrap sm:flex-nowrap">
|
<div class="flex w-full flex-wrap sm:flex-nowrap">
|
||||||
<IfNot condition={state.geolocation.geolocationState.permission.map((p) => p === "denied")}>
|
{#if $currentGPSLocation !== undefined || $geopermission === "prompt"}
|
||||||
<button class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
|
<button class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
|
||||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
|
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
|
||||||
<Tr t={Translations.t.general.openTheMapAtGeolocation} />
|
<Tr t={Translations.t.general.openTheMapAtGeolocation} />
|
||||||
</button>
|
</button>
|
||||||
</IfNot>
|
<!-- No geolocation granted - we don't show the button -->
|
||||||
|
{:else if $geopermission === "requested"}
|
||||||
|
<button class="flex w-full items-center gap-x-2 disabled" on:click={jumpToCurrentLocation}>
|
||||||
|
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
|
||||||
|
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("animate-spin")} />
|
||||||
|
<Tr t={Translations.t.general.waitingForGeopermission} />
|
||||||
|
</button>
|
||||||
|
{:else if $geopermission !== "denied"}
|
||||||
|
<button class="flex w-full items-center gap-x-2 disabled">
|
||||||
|
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("motion-safe:animate-spin")} />
|
||||||
|
<Tr t={Translations.t.general.waitingForLocation} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
|
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue