Better management of permissions

* Finish the ability to use custom tile server.
* Store settings whenever they are changed.
* Support new settings for audio/vibrate permission.
This commit is contained in:
Lucas Verney 2018-07-27 16:13:16 +02:00
parent 0d0a5d85d6
commit c19bd60174
9 changed files with 221 additions and 69 deletions

View File

@ -107,6 +107,14 @@ export default {
this.radiusFromAccuracy > this.markerRadius
);
},
tileServer() {
const tileServerSetting = this.$store.state.settings.tileServer;
if (tileServerSetting in constants.TILE_SERVERS) {
return constants.TILE_SERVERS[tileServerSetting];
}
const firstColon = tileServerSetting.indexOf(':');
return tileServerSetting.substring(firstColon + 1);
},
},
data() {
return {
@ -118,7 +126,6 @@ export default {
maxZoom: constants.MAX_ZOOM,
minZoom: constants.MIN_ZOOM,
isRecenterButtonShown: false,
tileServer: constants.TILE_SERVERS[this.$store.state.settings.tileServer],
unknownMarkerIcon: L.icon({
iconAnchor: [20, 40],
iconSize: [40, 40],

View File

@ -0,0 +1,79 @@
<template>
<div>
<v-switch
class="switch"
:messages="[`<i aria-hidden='true' class='v-icon material-icons' style='vertical-align: middle;'>help</i> ${$t('permissions.preventSuspendDescription')}`]"
color="success"
:label="$t('permissions.preventSuspend')"
v-model="hasPreventSuspendPermission"
>
</v-switch>
<v-switch
class="switch"
color="success"
:label="$t('permissions.playSound')"
v-model="hasPlaySoundPermission"
>
</v-switch>
<v-switch
class="switch"
color="success"
:label="$t('permissions.vibrate')"
v-model="hasVibratePermission"
>
</v-switch>
<v-switch
class="switch"
:messages="[`<i aria-hidden='true' class='v-icon material-icons' style='vertical-align: middle;'>help</i> ${$t('permissions.geolocationDescription')}`]"
color="success"
:label="$t('permissions.geolocation')"
v-model="hasGeolocationPermission"
>
</v-switch>
</div>
</template>
<script>
export default {
computed: {
hasGeolocationPermission: {
get() {
return this.$store.state.settings.hasGeolocationPermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasGeolocationPermission', value });
},
},
hasPlaySoundPermission: {
get() {
return this.$store.state.settings.hasPlaySoundPermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasPlaySoundPermission', value });
},
},
hasPreventSuspendPermission: {
get() {
return this.$store.state.settings.hasPreventSuspendPermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasPreventSuspendPermission', value });
},
},
hasVibratePermission: {
get() {
return this.$store.state.settings.hasVibratePermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasVibratePermission', value });
},
},
},
};
</script>
<style>
.switch .v-label {
color: rgba(0,0,0,.87);
}
</style>

View File

@ -13,6 +13,8 @@ import miscIcon from '@/assets/misc.svg';
import obstacleIcon from '@/assets/obstacle.svg';
import potholeIcon from '@/assets/pothole.svg';
export const VERSION = '0.1';
export const REPORT_TYPES = {
accident: {
description: 'reportLabels.accidentDescription',

View File

@ -1,7 +1,6 @@
{
"about": {
"availableReportsTitle": "The available reports so far are:",
"geolocationDescription": "As of current version, your precise geolocation is handled within your device and never sent from it to any external service. The map background is downloaded on demand from the tile provider and it has then access to an estimate of the displayed position. If you refuse to share your geolocation, you can still pick a location manually but you will miss some geolocation dependent features.",
"license": "It is released under an <a href=\"https://opensource.org/licenses/MIT\">MIT license</a> (<a href=\"https://framagit.org/phyks/cyclassist\">source code</a>). Icons are based on creations from Wikimedia, Vecteezy, Pixabay or Flaticon. The map background is using tiles from <a href=\"https://carto.com/location-data-services/basemaps/\">Carto.com</a> or <a href=\"http://thunderforest.com/\">Thunderforest</a>, thanks to <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap contributors</a> and <a href=\"http://leafletjs.com/\">Leaflet</a>. Collected reports are available under <a href=\"https://opendatacommons.org/licenses/odbl/\">ODbL license</a>. Manual location picking uses the awesome API from <a href=\"https://adresse.data.gouv.fr\">adresse.data.gouv.fr</a>.",
"summary": "This app lets you track and share issues with bike lanes.",
"usage": "How to use",
@ -53,6 +52,14 @@
"retry": "Retry",
"spaceBeforeDoublePunctuations": ""
},
"permissions": {
"geolocation": "Geolocation",
"geolocationDescription": "As of current version, your precise geolocation is handled within your device and never sent from it to any external service. The map background is downloaded on demand from the tile provider and it has then access to an estimate of the displayed position. If you refuse to share your geolocation, you can still pick a location manually but you will miss some geolocation dependent features.",
"playSound": "Play sound",
"preventSuspend": "Prevent device from going to sleep",
"preventSuspendDescription": "When the map is displayed, the device will be prevented from going to sleep.",
"vibrate": "Vibrate"
},
"reportCard": {
"Reported": "Reported"
},
@ -75,9 +82,10 @@
"potholeDescription": "A pothole in the ground."
},
"settings": {
"customTileServer": "Custom tile server",
"customTileServerURL": "Custom tile server URL",
"customTileServerURLHint": "For example: http://tile.thunderforest.com/cycle/{z}/{x}/{y}.png",
"locale": "Language",
"preventSuspend": "Prevent device from going to sleep",
"save": "Save",
"skipOnboarding": "Skip onboarding",
"tileServer": "Map tiles server"
},

View File

@ -3,7 +3,7 @@ import Vue from 'vue';
import { messages, getBrowserLocales } from '@/i18n';
import { storageAvailable } from '@/tools';
import { TILE_SERVERS, DEFAULT_TILE_SERVER } from '@/constants';
import { DEFAULT_TILE_SERVER, TILE_SERVERS, VERSION } from '@/constants';
import * as types from './mutations-types';
function loadDataFromStorage(name) {
@ -19,6 +19,23 @@ function loadDataFromStorage(name) {
}
}
function handleMigrations() {
if (!storageAvailable('localStorage')) {
return;
}
const version = loadDataFromStorage('version');
// Migration from pre-0.1 to 0.1
if (version === null) {
const preventSuspend = loadDataFromStorage('preventSuspend');
if (preventSuspend !== null) {
localStorage.setItem('hasPreventSuspendPermission', JSON.stringify(preventSuspend));
}
localStorage.removeItem('preventSuspend');
localStorage.setItem('version', JSON.stringify(VERSION));
}
}
// Load unsent reports from storage
let unsentReports = null;
if (storageAvailable('localStorage')) {
@ -27,15 +44,23 @@ if (storageAvailable('localStorage')) {
// Load settings from storage
let locale = null;
let preventSuspend = null;
let hasGeolocationPermission = null;
let hasPlaySoundPermission = null;
let hasPreventSuspendPermission = null;
let hasVibratePermission = null;
let skipOnboarding = null;
let tileServer = null;
if (storageAvailable('localStorage')) {
preventSuspend = loadDataFromStorage('preventSuspend');
handleMigrations();
hasGeolocationPermission = loadDataFromStorage('hasGeolocationPermission');
hasPlaySoundPermission = loadDataFromStorage('hasPlaySoundPermission');
hasPreventSuspendPermission = loadDataFromStorage('hasPreventSuspendPermission');
hasVibratePermission = loadDataFromStorage('hasVibratePermission');
skipOnboarding = loadDataFromStorage('skipOnboarding');
tileServer = loadDataFromStorage('tileServer');
if (!TILE_SERVERS[tileServer]) {
if (tileServer && !TILE_SERVERS[tileServer] && !tileServer.startsWith('custom:')) {
tileServer = null;
}
@ -80,7 +105,18 @@ export const initialState = {
unsentReports: unsentReports || [],
settings: {
locale: locale || 'en',
preventSuspend: preventSuspend || true,
hasGeolocationPermission: (
hasGeolocationPermission !== null ? hasGeolocationPermission : true
),
hasPlaySoundPermission: (
hasPlaySoundPermission !== null ? hasPlaySoundPermission : true
),
hasPreventSuspendPermission: (
hasPreventSuspendPermission !== null ? hasPreventSuspendPermission : true
),
hasVibratePermission: (
hasVibratePermission !== null ? hasVibratePermission : true
),
skipOnboarding: skipOnboarding || false,
tileServer: tileServer || DEFAULT_TILE_SERVER,
},

View File

@ -2,7 +2,7 @@
<v-container fluid>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<p>{{ $t('about.summary') }} <span v-html="$t('about.geolocationDescription')"></span></p>
<p>{{ $t('about.summary') }} <span v-html="$t('permissions.geolocationDescription')"></span></p>
<h2 class="body-2">{{ $t('about.usage') }}</h2>
<p>{{ $t('about.usageDescription') }}</p>

View File

@ -57,7 +57,6 @@ import i18n from '@/i18n';
import store from '@/store';
function handlePositionError(error) {
// TODO: Not translated when changing locale
store.dispatch('setLocationError', { error: error.code });
}
@ -220,7 +219,7 @@ export default {
this.reportLatLng = null;
},
setNoSleep() {
if (this.$store.state.settings.preventSuspend) {
if (this.$store.state.settings.hasPreventSuspendPermission) {
this.noSleep = new NoSleep();
this.noSleep.enable();
}
@ -238,7 +237,11 @@ export default {
if (this.$route.name !== 'SharedMap') {
// Only enable NoSleep in normal map view (with position tracking).
this.setNoSleep();
this.initializePositionWatching();
if (this.$store.state.settings.hasGeolocationPermission) {
this.initializePositionWatching();
} else {
this.$store.state.location.error = 1;
}
}
this.$store.dispatch('fetchReports');
},

View File

@ -23,9 +23,9 @@
<v-flex xs8 offset-xs2>
<h2 class="headline pa-3">{{ $t('intro.checkingPermissions') }}</h2>
<v-layout row class="white mb-3">
<v-flex xs6 offset-xs3>
<v-switch class="switch" :messages="[`<i aria-hidden='true' class='v-icon material-icons' style='vertical-align: middle;'>help</i> ${$t('about.geolocationDescription')}`]" color="success" :label="$t('geolocation.geolocation')" v-model="hasGeolocationPermission" readonly @click="handleGeolocationPermission"></v-switch>
<v-layout row class="white">
<v-flex class="mb-3 mx-3">
<PermissionsSwitches></PermissionsSwitches>
</v-flex>
</v-layout>
@ -44,17 +44,14 @@
<script>
import ReportsDescription from '@/components/ReportsDescription.vue';
import PermissionsSwitches from '@/components/PermissionsSwitches.vue';
export default {
components: {
ReportsDescription,
PermissionsSwitches,
},
created() {
if (navigator.permissions) {
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
this.hasGeolocationPermission = (result.state === 'granted');
});
}
this.$store.dispatch('markIntroAsSeen');
},
data() {
@ -63,34 +60,10 @@ export default {
step = 3;
}
return {
hasGeolocationPermission: false,
step,
};
},
methods: {
handleGeolocationPermission() {
if (this.hasGeolocationPermission) {
// Permission already granted
return;
}
// Explicitly request the permission to the user
navigator.geolocation.getCurrentPosition(
() => {
this.hasGeolocationPermission = true;
},
(error) => {
if (error.code === 1) { // Permission denied
this.hasGeolocationPermission = false;
} else {
this.hasGeolocationPermission = true;
}
},
{
timeout: 0,
},
);
},
goToMap() {
if (!this.$store.state.settings.skipOnboarding) {
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: true });
@ -106,9 +79,3 @@ export default {
padding: 0;
}
</style>
<style>
.switch .v-label {
color: rgba(0,0,0,.87);
}
</style>

View File

@ -20,17 +20,22 @@
required
></v-select>
<v-checkbox
:label="$t('settings.preventSuspend')"
v-model="preventSuspend"
></v-checkbox>
<v-text-field
ref="shareLinkRef"
:hint="$t('settings.customTileServerURLHint')"
:label="$t('settings.customTileServerURL')"
v-model="customTileServerURL"
v-if="showCustomTileServerURLField"
required
>
</v-text-field>
<v-checkbox
:label="$t('settings.skipOnboarding')"
v-model="skipOnboarding"
></v-checkbox>
<v-btn role="button" @click="submit">{{ $t('settings.save') }}</v-btn>
<PermissionsSwitches></PermissionsSwitches>
</form>
</v-flex>
</v-layout>
@ -41,24 +46,69 @@
import { TILE_SERVERS } from '@/constants';
import { AVAILABLE_LOCALES } from '@/i18n';
import PermissionsSwitches from '@/components/PermissionsSwitches.vue';
export default {
components: {
PermissionsSwitches,
},
computed: {
customTileServerURL: {
get() {
const tileServerStore = this.$store.state.settings.tileServer;
if (tileServerStore in TILE_SERVERS) {
return null;
}
const firstColon = tileServerStore.indexOf(':');
return tileServerStore.substring(firstColon + 1);
},
set(URL) {
this.$store.dispatch('setSetting', { setting: 'tileServer', value: `custom:${URL}` });
},
},
locale: {
get() {
return this.$store.state.settings.locale;
},
set(locale) {
this.$store.dispatch('setLocale', { locale });
},
},
showCustomTileServerURLField() {
return !(this.$store.state.settings.tileServer in TILE_SERVERS);
},
skipOnboarding: {
get() {
return this.$store.state.settings.skipOnboarding;
},
set(skipOnboarding) {
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: skipOnboarding });
},
},
tileServer: {
get() {
const tileServerStore = this.$store.state.settings.tileServer;
if (tileServerStore.startsWith('custom:')) {
return this.$t('settings.customTileServer');
}
return tileServerStore;
},
set(tileServer) {
if (tileServer in TILE_SERVERS) {
this.$store.dispatch('setSetting', { setting: 'tileServer', value: tileServer });
} else {
this.$store.dispatch('setSetting', { setting: 'tileServer', value: 'custom:' });
}
},
},
tileServers() {
return [].concat(Object.keys(TILE_SERVERS), this.$t('settings.customTileServer'));
},
},
data() {
return {
i18nItems: AVAILABLE_LOCALES,
locale: this.$store.state.settings.locale,
preventSuspend: this.$store.state.settings.preventSuspend,
skipOnboarding: this.$store.state.settings.skipOnboarding,
tileServer: this.$store.state.settings.tileServer,
tileServers: Object.keys(TILE_SERVERS),
};
},
methods: {
submit() {
this.$store.dispatch('setLocale', { locale: this.locale });
this.$store.dispatch('setSetting', { setting: 'preventSuspend', value: this.preventSuspend });
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: this.skipOnboarding });
this.$store.dispatch('setSetting', { setting: 'tileServer', value: this.tileServer });
},
},
};
</script>