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:
parent
0d0a5d85d6
commit
c19bd60174
@ -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],
|
||||
|
79
src/components/PermissionsSwitches.vue
Normal file
79
src/components/PermissionsSwitches.vue
Normal 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>
|
@ -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',
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
if (this.$store.state.settings.hasGeolocationPermission) {
|
||||
this.initializePositionWatching();
|
||||
} else {
|
||||
this.$store.state.location.error = 1;
|
||||
}
|
||||
}
|
||||
this.$store.dispatch('fetchReports');
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user