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",
|
||||
"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": {
|
||||
"abbreviations": {
|
||||
"friday": "Fri",
|
||||
|
|
|
@ -858,6 +858,10 @@ video {
|
|||
margin-right: 3rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
@ -886,10 +890,6 @@ video {
|
|||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
@ -2662,6 +2662,46 @@ a.link-underline {
|
|||
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) {
|
||||
.max-\[480px\]\:w-full {
|
||||
width: 100%;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { UIEventSource } from "../UIEventSource";
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource";
|
||||
import { QueryParameters } from "../Web/QueryParameters";
|
||||
|
||||
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
|
||||
|
||||
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
||||
id: "gps"
|
||||
"user:location": "yes"
|
||||
date: string
|
||||
id: "gps";
|
||||
"user:location": "yes";
|
||||
date: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,22 +23,22 @@ export class GeoLocationState {
|
|||
*/
|
||||
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
|
||||
"prompt"
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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.
|
||||
|
@ -50,69 +50,50 @@ export class GeoLocationState {
|
|||
*/
|
||||
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
|
||||
LocalStorageSource.Get("geolocation-permissions")
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
const self = this
|
||||
const self = this;
|
||||
|
||||
this.permission.addCallbackAndRunD(async (state) => {
|
||||
console.trace("GEOPERMISSION", state)
|
||||
if (state === "granted") {
|
||||
self._previousLocationGrant.setData("true")
|
||||
self._grantedThisSession.setData(true)
|
||||
self._previousLocationGrant.setData("true");
|
||||
self._grantedThisSession.setData(true);
|
||||
}
|
||||
if (state === "prompt" && self._grantedThisSession.data) {
|
||||
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
|
||||
// This means that the rights have been revoked again!
|
||||
// self.permission.setData("denied")
|
||||
self._previousLocationGrant.setData("false")
|
||||
self.permission.setData("denied")
|
||||
self.currentGPSLocation.setData(undefined)
|
||||
console.warn("Detected a downgrade in permissions!")
|
||||
self._previousLocationGrant.setData("false");
|
||||
self.permission.setData("denied");
|
||||
self.currentGPSLocation.setData(undefined);
|
||||
console.warn("Detected a downgrade in permissions!");
|
||||
}
|
||||
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") {
|
||||
// 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
|
||||
this._previousLocationGrant.setData("false")
|
||||
console.log("Requesting access to GPS as this was previously granted")
|
||||
this._previousLocationGrant.setData("false");
|
||||
console.log("Requesting access to GPS as this was previously granted");
|
||||
const latLonGivenViaUrl =
|
||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
|
||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon");
|
||||
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.
|
||||
* When granted, will be written to the 'geolocationState'.
|
||||
|
@ -121,33 +102,57 @@ export class GeoLocationState {
|
|||
public requestPermission() {
|
||||
if (typeof navigator === "undefined") {
|
||||
// Not compatible with this browser
|
||||
this.permission.setData("denied")
|
||||
return
|
||||
this.permission.setData("denied");
|
||||
return;
|
||||
}
|
||||
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
|
||||
// Hence that we continue the flow if it is "requested"
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
this.permission.setData("requested")
|
||||
this.permission.setData("requested");
|
||||
try {
|
||||
navigator?.permissions
|
||||
?.query({ name: "geolocation" })
|
||||
.then((status) => {
|
||||
console.log("Status update: received geolocation permission is ", status.state)
|
||||
this.permission.setData(status.state)
|
||||
const self = this
|
||||
status.onchange = function () {
|
||||
const self = this;
|
||||
if(status.state === "granted" || status.state === "denied"){
|
||||
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")
|
||||
// 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) {
|
||||
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">
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Geosearch from "./Geosearch.svelte"
|
||||
import IfNot from "../Base/IfNot.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import If from "../Base/If.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import { Utils } from "../../Utils"
|
||||
import Translations from "../i18n/Translations";
|
||||
import Svg from "../../Svg";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import Geosearch from "./Geosearch.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
import { Utils } from "../../Utils";
|
||||
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
|
||||
|
||||
/**
|
||||
* The theme introduction panel
|
||||
*/
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
let selectedLayer = state.selectedLayer
|
||||
export let state: ThemeViewState;
|
||||
let layout = state.layout;
|
||||
let selectedElement = state.selectedElement;
|
||||
let selectedLayer = state.selectedLayer;
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
|
||||
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() {
|
||||
const glstate = state.geolocation.geolocationState
|
||||
const glstate = state.geolocation.geolocationState;
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
state.guistate.themeIsOpened.setData(false)
|
||||
const coor = { lon: c.longitude, lat: c.latitude }
|
||||
state.mapProperties.location.setData(coor)
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
|
||||
state.guistate.themeIsOpened.setData(false);
|
||||
const coor = { lon: c.longitude, lat: c.latitude };
|
||||
state.mapProperties.location.setData(coor);
|
||||
}
|
||||
if (glstate.permission.data !== "granted") {
|
||||
glstate.requestPermission()
|
||||
return
|
||||
glstate.requestPermission();
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -58,12 +62,24 @@
|
|||
</NextButton>
|
||||
|
||||
<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}>
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
|
||||
<Tr t={Translations.t.general.openTheMapAtGeolocation} />
|
||||
</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="w-full">
|
||||
|
|
Loading…
Reference in a new issue