refactor: initialise settings from config module and persist only the values

Closes #30, #59
This commit is contained in:
Sebastian Lange
2019-04-29 14:04:25 +02:00
parent e1039aa226
commit 235693a9e2
14 changed files with 1673 additions and 256 deletions

View File

@@ -18,8 +18,7 @@ export class StAppsApp {
getPageTitle() { getPageTitle() {
return element(by.tagName('ion-router-outlet')) return element(by.tagName('ion-router-outlet'))
.element(by.tagName('ion-title')) .element(by.tagName('ion-title')).getText();
.element(by.id('title')).getText();
} }
getTitle() { getTitle() {

1218
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@
"scripts": { "scripts": {
"build": "ng build", "build": "ng build",
"build:prod": "ng build --prod", "build:prod": "ng build --prod",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'",
"check-configuration": "openstapps-configuration",
"e2e": "ng e2e", "e2e": "ng e2e",
"docker:build": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm install && npm run build\"", "docker:build": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm install && npm run build\"",
"docker:enter": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash", "docker:enter": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash",
@@ -25,13 +27,13 @@
"test": "ng test" "test": "ng test"
}, },
"dependencies": { "dependencies": {
"@angular/common": "7.2.12", "@angular/common": "7.2.14",
"@angular/core": "7.2.12", "@angular/core": "7.2.14",
"@angular/forms": "7.2.12", "@angular/forms": "7.2.14",
"@angular/http": "7.2.12", "@angular/http": "7.2.14",
"@angular/platform-browser": "7.2.12", "@angular/platform-browser": "7.2.14",
"@angular/platform-browser-dynamic": "7.2.12", "@angular/platform-browser-dynamic": "7.2.14",
"@angular/router": "7.2.12", "@angular/router": "7.2.14",
"@ionic-native/core": "5.4.0", "@ionic-native/core": "5.4.0",
"@ionic-native/geolocation": "5.4.0", "@ionic-native/geolocation": "5.4.0",
"@ionic-native/splash-screen": "5.4.0", "@ionic-native/splash-screen": "5.4.0",
@@ -42,7 +44,7 @@
"@ngx-translate/http-loader": "4.0.0", "@ngx-translate/http-loader": "4.0.0",
"@openstapps/api": "0.6.0", "@openstapps/api": "0.6.0",
"@openstapps/configuration": "0.8.0", "@openstapps/configuration": "0.8.0",
"@openstapps/core": "0.15.0", "@openstapps/core": "0.17.0",
"@openstapps/logger": "0.0.5", "@openstapps/logger": "0.0.5",
"cordova-android": "8.0.0", "cordova-android": "8.0.0",
"cordova-browser": "6.0.0", "cordova-browser": "6.0.0",
@@ -54,6 +56,7 @@
"cordova-plugin-splashscreen": "5.0.2", "cordova-plugin-splashscreen": "5.0.2",
"cordova-plugin-whitelist": "1.3.3", "cordova-plugin-whitelist": "1.3.3",
"core-js": "2.6.5", "core-js": "2.6.5",
"deepmerge": "3.2.0",
"moment": "2.24.0", "moment": "2.24.0",
"ngx-markdown": "7.1.4", "ngx-markdown": "7.1.4",
"ngx-moment": "3.4.0", "ngx-moment": "3.4.0",
@@ -66,16 +69,17 @@
"@angular-devkit/core": "7.3.8", "@angular-devkit/core": "7.3.8",
"@angular-devkit/schematics": "7.3.8", "@angular-devkit/schematics": "7.3.8",
"@angular/cli": "7.3.8", "@angular/cli": "7.3.8",
"@angular/compiler": "7.2.12", "@angular/compiler": "7.2.14",
"@angular/compiler-cli": "7.2.12", "@angular/compiler-cli": "7.2.14",
"@angular/language-service": "7.2.12", "@angular/language-service": "7.2.14",
"@compodoc/compodoc": "1.1.9", "@compodoc/compodoc": "1.1.9",
"@ionic/ng-toolkit": "1.1.0", "@ionic/ng-toolkit": "1.1.0",
"@ionic/schematics-angular": "1.0.7", "@ionic/schematics-angular": "1.0.7",
"@types/jasmine": "3.3.12", "@types/jasmine": "3.3.12",
"@types/jasminewd2": "2.0.6", "@types/jasminewd2": "2.0.6",
"@types/node": "11.13.2", "@types/node": "10.14.6",
"codelyzer": "5.0.0", "codelyzer": "5.0.0",
"conventional-changelog-cli": "2.0.12",
"is-docker": "1.1.0", "is-docker": "1.1.0",
"jasmine-core": "3.4.0", "jasmine-core": "3.4.0",
"jasmine-spec-reporter": "4.2.1", "jasmine-spec-reporter": "4.2.1",
@@ -107,5 +111,20 @@
"browser", "browser",
"android" "android"
] ]
},
"openstappsConfiguration": {
"forPackaging": false,
"hasCli": false,
"ignoreCiEntries": [
"image",
"pages"
],
"ignoreScripts": [
"prepublishOnly",
"compile"
],
"serverSide": false,
"standardBuild": false,
"standardDocumentation": false
} }
} }

View File

@@ -15,7 +15,7 @@
import {HTTP_INTERCEPTORS, HttpClient, import {HTTP_INTERCEPTORS, HttpClient,
HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http'; HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {SCIndexResponse, SCThingType} from '@openstapps/core'; import {SCIndexResponse, SCThingOriginType, SCThingType} from '@openstapps/core';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {map, delay} from 'rxjs/operators'; import {map, delay} from 'rxjs/operators';
import {SampleThings} from './data/sample-things'; import {SampleThings} from './data/sample-things';
@@ -55,7 +55,102 @@ const sampleIndexResponse: SCIndexResponse = {
menus: [], menus: [],
name: 'StApps - Technische Universität Berlin', name: 'StApps - Technische Universität Berlin',
privacyPolicyUrl: 'https://stappsbe01.innocampus.tu-berlin.de/_static/privacy.md', privacyPolicyUrl: 'https://stappsbe01.innocampus.tu-berlin.de/_static/privacy.md',
settings: [], settings: [
{
categories: ['profile'],
description: '',
input: {
defaultValue: 'student',
inputType: 'singleChoice',
values: ['student', 'employee', 'guest'],
},
name: 'group',
order: 1,
origin: {
indexed: '2018-09-11T12:30:00Z',
name: 'Dummy',
type: SCThingOriginType.Remote,
},
translations: {
de: {
categories: ['Benutzer'],
description: 'Mit welcher Benutzergruppe soll die App verwendet werden?'
+ ' Die Einstellung wird beispielsweise für die Vorauswahl der Preiskategorie der Mensa verwendet.',
name: 'Gruppe',
},
en: {
categories: ['User'],
description: 'The user group the app is going to be used.'
+ 'This settings for example is getting used for the predefined price category of mensa meals.',
name: 'Group',
},
},
type: SCThingType.Setting,
uid: '',
},
{
categories: ['profile'],
description: '',
input: {
defaultValue: 'en',
inputType: 'singleChoice',
values: ['en', 'de'],
},
name: 'language',
order: 0,
origin: {
indexed: '2018-09-11T12:30:00Z',
name: 'Dummy',
type: SCThingOriginType.Remote,
},
translations: {
de: {
categories: ['Benutzer'],
description: 'Die Sprache in der die App angezeigt werden soll',
name: 'Sprache',
},
en: {
categories: ['User'],
description: 'The language this app is going to use',
name: 'Language',
},
},
type: SCThingType.Setting,
uid: '',
},
{
categories: ['privacy'],
description: '',
input: {
defaultValue: false,
inputType: 'singleChoice',
values: [true, false],
},
name: 'geoLocation',
order: 0,
origin: {
indexed: '2018-09-11T12:30:00Z',
name: 'Dummy',
type: SCThingOriginType.Remote,
},
translations: {
de: {
categories: ['Privatsphäre'],
description: 'Berechtigung für die Verwendung des Ortungsdienstes, für die Anzeige der aktuellen ' +
'Position \'\n auf der Karte und zur Berechnung der Entfernung zu Gebäuden und Orten des Campus',
name: 'Position',
},
en: {
categories: ['Privacy'],
description: 'Allow the App to use the device location to provide additional informationsbased ' +
'on your actual location',
name: 'Position',
},
},
type: SCThingType.Setting,
uid: '',
},
],
}, },
backend: { backend: {
SCVersion: '1.0.0', SCVersion: '1.0.0',

View File

@@ -72,7 +72,7 @@ export class AppComponent {
try { try {
// set language from settings // set language from settings
const languageCode = await this.settingsProvider.getSettingValue('profile', 'language'); const languageCode = await this.settingsProvider.getValue('profile', 'language');
this.translateService.use(languageCode); this.translateService.use(languageCode);
} catch (error) { } catch (error) {
this.logger.warn(error); this.logger.warn(error);

View File

@@ -13,14 +13,14 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {Geolocation} from '@ionic-native/geolocation/ngx';
import {AlertController} from '@ionic/angular'; import {AlertController} from '@ionic/angular';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import { import {
SCSetting, SCSetting,
SCSettingMeta, SCThingTranslator,
SCTranslations, SCTranslations,
} from '@openstapps/core'; } from '@openstapps/core';
import {Logger} from '@openstapps/logger';
import {SettingsProvider} from '../settings.provider'; import {SettingsProvider} from '../settings.provider';
@Component({ @Component({
@@ -31,22 +31,21 @@ export class SettingsItemComponent {
isVisible = true; isVisible = true;
// limit to languages that are available in StApps Core // limit to languages that are available in StApps Core
language: keyof SCTranslations<any>; language: keyof SCTranslations<any>;
meta = SCSettingMeta; logger = new Logger();
@Input() setting: SCSetting; @Input() setting: SCSetting;
translator: SCThingTranslator;
constructor(private alertCtrl: AlertController, constructor(private alertCtrl: AlertController,
private translateService: TranslateService, private translateService: TranslateService,
private settingsProvider: SettingsProvider, private settingsProvider: SettingsProvider) {
private geoLocation: Geolocation) {
this.meta = SCSettingMeta;
this.language = translateService.currentLang as keyof SCTranslations<any>; this.language = translateService.currentLang as keyof SCTranslations<any>;
this.translator = new SCThingTranslator(this.language, 'de');
translateService.onLangChange.subscribe((event: LangChangeEvent) => { translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.isVisible = false; this.isVisible = false;
this.language = event.lang as keyof SCTranslations<any>; this.language = event.lang as keyof SCTranslations<any>;
// workaround for selected 'select option' not updating translation this.translator = new SCThingTranslator(this.language, 'de');
// TODO: Issue #53 check workaround for selected 'select option' not updating translation
setTimeout(() => this.isVisible = true); setTimeout(() => this.isVisible = true);
}); });
} }
@@ -56,24 +55,11 @@ export class SettingsItemComponent {
* if no permission is granted, setting is set to false and an alert is presented to the user * if no permission is granted, setting is set to false and an alert is presented to the user
*/ */
private async checkGeoLocationPermission() { private async checkGeoLocationPermission() {
// request geoLocation to test the user permission const permissionGranted = await this.settingsProvider.checkGeoLocationPermission();
try { if (!permissionGranted) {
// set enableHighAccuracy, otherwise android platform does not respond // revert setting value
const options = { this.setting.input.value = false;
enableHighAccuracy: true, await this.presentGeoLocationAlert();
};
await this.geoLocation.getCurrentPosition(options);
} catch (error) {
// if error code is 1 the user denied permission,
// other errors like 'timeout' or 'no location' will be ignored here
if (error.code === 1) {
// ios has special error message for disabled location services, for the setting we ignore it
if (error.message.toLowerCase() !== 'location services are disabled.') {
// revert setting value
this.setting.input.value = false;
await this.presentGeoLocationAlert();
}
}
} }
} }
@@ -81,8 +67,8 @@ export class SettingsItemComponent {
* Shows alert with error message on denied user permission or disabled location services * Shows alert with error message on denied user permission or disabled location services
*/ */
private async presentGeoLocationAlert() { private async presentGeoLocationAlert() {
const title = this.translateService.instant('settings.geoLocation.permission_denied_title'); const title = await this.translateService.get('settings.geoLocation.permission_denied_title').toPromise();
const message = this.translateService.instant('settings.geoLocation.permission_denied_message'); const message = await this.translateService.get('settings.geoLocation.permission_denied_message').toPromise();
await this.presentAlert(title, message); await this.presentAlert(title, message);
} }
@@ -124,7 +110,7 @@ export class SettingsItemComponent {
} else { } else {
// reset setting // reset setting
this.setting.input.value = this.setting.input.value =
await this.settingsProvider.getSettingValue(this.setting.categories[0], this.setting.name); await this.settingsProvider.getValue(this.setting.categories[0], this.setting.name);
} }
} }

View File

@@ -1,15 +1,11 @@
<ion-card> <ion-card>
<!-- <ion-card-header> <ion-card-header>
<span id="settingTitle" >{{ meta.getFieldValueTranslation(language, 'name', setting) }}</span> <span>{{ translator.translate(setting).name() }}</span>
</ion-card-header> --> </ion-card-header>
<ion-card-content> <ion-card-content>
<!-- <ion-note >{{ meta.getFieldValueTranslation(language, 'description', setting) }}</ion-note> --> <ion-note>{{ translator.translate(setting).description() }}</ion-note>
<div [ngSwitch]="setting.input.inputType" *ngIf="isVisible" > <div [ngSwitch]="setting.input.inputType" *ngIf="isVisible" >
<ion-item *ngSwitchCase="'toggle'">
<ion-label></ion-label>
<ion-toggle start [(ngModel)]="setting.input.value" (ionChange)="settingChanged()"></ion-toggle>
</ion-item>
<ion-item *ngSwitchCase="'number'"> <ion-item *ngSwitchCase="'number'">
<ion-label></ion-label> <ion-label></ion-label>
<ion-input type='number' [(ngModel)]="setting.input.value" value={{setting.input.value}} (ionChange)="settingChanged()"></ion-input> <ion-input type='number' [(ngModel)]="setting.input.value" value={{setting.input.value}} (ionChange)="settingChanged()"></ion-input>
@@ -45,11 +41,6 @@
</ion-select-option> </ion-select-option>
</ion-select> </ion-select>
</ion-item> </ion-item>
<span *ngSwitchDefault>
<ion-note>no template for {{ setting.name }}</ion-note>
</span>
</div> </div>
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>

View File

@@ -13,8 +13,9 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {AlertController, ToastController} from '@ionic/angular';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {SCSettingMeta, SCTranslations} from '@openstapps/core'; import {SCSettingMeta, SCThingTranslator, SCTranslations} from '@openstapps/core';
import {SettingsCache, SettingsProvider} from '../settings.provider'; import {SettingsCache, SettingsProvider} from '../settings.provider';
@Component({ @Component({
@@ -28,22 +29,81 @@ export class SettingsPageComponent {
meta = SCSettingMeta; meta = SCSettingMeta;
objectKeys = Object.keys; objectKeys = Object.keys;
settingsCache: SettingsCache; settingsCache: SettingsCache;
translator: SCThingTranslator;
constructor(public settingsProvider: SettingsProvider, constructor(private alertController: AlertController,
translateService: TranslateService) { private settingsProvider: SettingsProvider,
private toastController: ToastController,
private translateService: TranslateService) {
this.language = translateService.currentLang as keyof SCTranslations<any>; this.language = translateService.currentLang as keyof SCTranslations<any>;
this.translator = new SCThingTranslator(this.language, 'de');
translateService.onLangChange.subscribe((event: LangChangeEvent) => { translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<any>; this.language = event.lang as keyof SCTranslations<any>;
this.translator = new SCThingTranslator(this.language, 'de');
}); });
this.settingsCache = {}; this.settingsCache = {};
this.categoriesOrder = settingsProvider.getCategoriesOrder(); this.categoriesOrder = settingsProvider.getCategoriesOrder();
} }
/**
* Presents a Toast with message for settings successful reset
*/
private async presentSettingsResetToast() {
const toast = await this.toastController.create({
cssClass: 'text-center',
duration: 2000,
message: this.translateService.instant('settings.resetToast.message'),
});
toast.present();
}
/**
* Loads cache of settings from SettingProvider
*/
async loadSettings(): Promise<void> { async loadSettings(): Promise<void> {
this.settingsCache = await this.settingsProvider.getSettingsCache(); this.settingsCache = await this.settingsProvider.getCache();
} }
async ngOnInit() { async ngOnInit() {
await this.loadSettings(); await this.loadSettings();
} }
/**
* Presents an alert to the user to reset settings to default values
*/
async presentResetAlert() {
const cancelText = await this.translateService.get('settings.resetAlert.buttonCancel').toPromise();
const yesText = await this.translateService.get('settings.resetAlert.buttonYes').toPromise();
const title = await this.translateService.get('settings.resetAlert.title').toPromise();
const message = await this.translateService.get('settings.resetAlert.message').toPromise();
const alert = await this.alertController.create({
buttons: [
{
role: 'cancel',
text: cancelText,
},
{
handler: async () => {
await this.resetSettings();
},
text: yesText,
},
],
header: title,
message: message,
});
alert.present();
}
/**
* Resets all settings to default values
*/
async resetSettings() {
await this.settingsProvider.resetDefault();
await this.loadSettings();
await this.presentSettingsResetToast();
}
} }

View File

@@ -1,20 +1,25 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-back-button></ion-back-button> <ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button> <ion-menu-button></ion-menu-button>
</ion-buttons> </ion-buttons>
<ion-title ><div id="title" > {{'settings.title' | translate}}</div></ion-title> <ion-title>{{'settings.title' | translate}}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content > <ion-content>
<ion-list *ngFor="let categoryKey of categoriesOrder "> <ion-list *ngFor="let categoryKey of categoriesOrder ">
<div *ngIf="objectKeys(settingsCache).includes(categoryKey)"> <div *ngIf="objectKeys(settingsCache).includes(categoryKey)">
<ion-item-divider><h5>{{ meta.getFieldValueTranslation(language, 'categories', <ion-item-divider>
settingsCache[categoryKey].settings[objectKeys(settingsCache[categoryKey].settings)[0]]) }}</h5></ion-item-divider> <h5>{{translator.translate(settingsCache[categoryKey].settings[objectKeys(settingsCache[categoryKey].settings)[0]]).categories()[0]}}
</h5>
</ion-item-divider>
<stapps-settings-item *ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)" [setting]="settingsCache[categoryKey].settings[settingKeys]"></stapps-settings-item> <stapps-settings-item *ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)" [setting]="settingsCache[categoryKey].settings[settingKeys]"></stapps-settings-item>
</div> </div>
</ion-list> </ion-list>
<ion-button color="medium" expand="block" fill="outline" (click)="presentResetAlert()">
{{'settings.resetSettings' | translate}}
<ion-icon slot="start" name="undo"></ion-icon>
</ion-button>
</ion-content> </ion-content>

View File

@@ -20,12 +20,13 @@ import {Geolocation} from '@ionic-native/geolocation/ngx';
import {IonicModule} from '@ionic/angular'; import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {ConfigProvider} from '../config/config.provider';
import {SettingsItemComponent} from './item/settings-item.component'; import {SettingsItemComponent} from './item/settings-item.component';
import {SettingsPageComponent} from './page/settings-page.component'; import {SettingsPageComponent} from './page/settings-page.component';
import {SettingsProvider} from './settings.provider'; import {SettingsProvider} from './settings.provider';
const settingsRoutes: Routes = [ const settingsRoutes: Routes = [
{ path: 'settings', component: SettingsPageComponent }, {path: 'settings', component: SettingsPageComponent},
]; ];
@NgModule({ @NgModule({
@@ -41,6 +42,7 @@ const settingsRoutes: Routes = [
RouterModule.forChild(settingsRoutes), RouterModule.forChild(settingsRoutes),
], ],
providers: [ providers: [
ConfigProvider,
Geolocation, Geolocation,
SettingsProvider, SettingsProvider,
], ],

View File

@@ -14,123 +14,165 @@
*/ */
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {SCSetting, SCThingOriginType, SCThingType} from '@openstapps/core'; import {SCSetting, SCThingOriginType, SCThingType} from '@openstapps/core';
import {StorageModule} from '../storage/storage.module'; import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
import {SettingsProvider} from './settings.provider'; import {SettingsProvider, SettingValuesContainer, STORAGE_KEY_SETTING_VALUES} from './settings.provider';
import {Geolocation} from '@ionic-native/geolocation/ngx';
describe('SettingsProvider', () => { describe('SettingsProvider', () => {
let configProviderSpy: jasmine.SpyObj<ConfigProvider>;
let settingsProvider: SettingsProvider; let settingsProvider: SettingsProvider;
let storageModule: StorageProvider; let storageProviderSpy: jasmine.SpyObj<StorageProvider>;
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ const storageProviderMethodSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
imports: [StorageModule], const configProviderMethodSpy = jasmine.createSpyObj('ConfigProvider', ['getValue']);
providers: [SettingsProvider, StorageProvider],
});
settingsProvider = TestBed.get(SettingsProvider);
storageModule = TestBed.get(StorageProvider);
settingsProvider.clear(); TestBed.configureTestingModule({
imports: [],
providers: [
SettingsProvider,
{
provide: StorageProvider, useValue: storageProviderMethodSpy,
},
{
provide: ConfigProvider, useValue: configProviderMethodSpy,
},
Geolocation,
],
});
configProviderSpy = TestBed.get(ConfigProvider);
// set settings returned from config
configProviderSpy.getValue.and.returnValue(Promise.resolve(CONFIG_SETTINGS_MOCK));
settingsProvider = TestBed.get(SettingsProvider);
storageProviderSpy = TestBed.get(StorageProvider);
storageProviderMethodSpy.has.and.returnValue(false);
}); });
it('should provide and get setting', async () => { it('should provide and get setting', async () => {
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0]))); await settingsProvider.provideSetting(JSON.parse(JSON.stringify(CONFIG_SETTINGS_MOCK[0])));
const setting = await settingsProvider.getSetting(SETTING_MOCKS[0].categories[0], SETTING_MOCKS[0].name); const setting: SCSetting = await settingsProvider
await expect(setting).toBeDefined(); .getSetting(CONFIG_SETTINGS_MOCK[0].categories[0], CONFIG_SETTINGS_MOCK[0].name);
await expect(setting.input.value).toBeDefined();
}); });
it('should provide and get settings value', async () => { it('should provide and get settings value', async () => {
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0]))); await settingsProvider.provideSetting(JSON.parse(JSON.stringify(CONFIG_SETTINGS_MOCK[0])));
const value = await settingsProvider.getSettingValue(SETTING_MOCKS[0].categories[0], SETTING_MOCKS[0].name); const value = await settingsProvider
await expect(value).toBeDefined(); .getValue(CONFIG_SETTINGS_MOCK[0].categories[0], CONFIG_SETTINGS_MOCK[0].name);
await expect(value).toEqual(CONFIG_SETTINGS_MOCK[0].input.defaultValue);
});
it('should get persisted setting value', async () => {
// set return values of storage
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
storageProviderSpy.get.and.returnValue(Promise.resolve(SETTING_VALUES_MOCK));
const value = await settingsProvider
.getValue(CONFIG_SETTINGS_MOCK[3].categories[0], CONFIG_SETTINGS_MOCK[3].name);
await expect(value).toEqual(SETTING_VALUES_MOCK.profile.group);
});
it('should set default setting value if no persisted value exist', async () => {
// set return values of spy objects
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
storageProviderSpy.get.and.returnValue(Promise.resolve([]));
const value = await settingsProvider
.getValue(CONFIG_SETTINGS_MOCK[3].categories[0], CONFIG_SETTINGS_MOCK[3].name);
await expect(value).toEqual(CONFIG_SETTINGS_MOCK[3].input.defaultValue);
});
it('should keep persisted setting values from settings that are not contained in loaded config', async () => {
const settings = [
CONFIG_SETTINGS_MOCK[4],
CONFIG_SETTINGS_MOCK[5],
];
configProviderSpy.getValue.and.returnValue(Promise.resolve(settings));
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
storageProviderSpy.get.and.returnValue(Promise.resolve(SETTING_VALUES_MOCK));
await settingsProvider.init();
await expect(storageProviderSpy.put).toHaveBeenCalledWith(STORAGE_KEY_SETTING_VALUES, SETTING_VALUES_MOCK);
}); });
it('should set value of a provided setting', async () => { it('should set value of a provided setting', async () => {
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0]))); await settingsProvider.provideSetting(JSON.parse(JSON.stringify(CONFIG_SETTINGS_MOCK[1])));
await settingsProvider.setSettingValue(SETTING_MOCKS[0].categories[0], SETTING_MOCKS[0].name, 'updated'); await settingsProvider
const value = await settingsProvider.getSettingValue(SETTING_MOCKS[0].categories[0], SETTING_MOCKS[0].name); .setSettingValue(CONFIG_SETTINGS_MOCK[1].categories[0], CONFIG_SETTINGS_MOCK[1].name, 'updated');
const value = await settingsProvider
.getValue(CONFIG_SETTINGS_MOCK[1].categories[0], CONFIG_SETTINGS_MOCK[1].name);
await expect(value).toEqual('updated'); await expect(value).toEqual('updated');
}); });
it('should return copy of settingsCache', async () => { it('should return copy of settingsCache', async () => {
const category = SETTING_MOCKS[0].categories[0]; const category = CONFIG_SETTINGS_MOCK[0].categories[0];
const name = SETTING_MOCKS[0].name; const name = CONFIG_SETTINGS_MOCK[0].name;
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0]))); await settingsProvider.provideSetting(JSON.parse(JSON.stringify(CONFIG_SETTINGS_MOCK[0])));
const settings = await settingsProvider.getSettingsCache(); const settings = await settingsProvider.getCache();
settings[category].settings[name].input.value = 'testValue'; settings[category].settings[name].input.value = 'testValue';
// cached setting value should still be defaultValue // cached setting value should still be defaultValue
await expect((await settingsProvider.getSettingValue(category, name))) await expect((await settingsProvider.getValue(category, name)))
.toEqual(SETTING_MOCKS[0].input.defaultValue); .toEqual(CONFIG_SETTINGS_MOCK[0].input.defaultValue);
});
it('should call storage put on provideSetting', async () => {
spyOn(storageModule, 'put');
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0])));
await expect(storageModule.put).toHaveBeenCalled();
}); });
it('should call storage put on setSettingValue', async () => { it('should call storage put on setSettingValue', async () => {
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0]))); await settingsProvider.provideSetting(JSON.parse(JSON.stringify(CONFIG_SETTINGS_MOCK[0])));
spyOn(storageModule, 'put'); await settingsProvider
await settingsProvider.setSettingValue(SETTING_MOCKS[0].categories[0], SETTING_MOCKS[0].name, ''); .setSettingValue(CONFIG_SETTINGS_MOCK[0].categories[0], CONFIG_SETTINGS_MOCK[0].name, '');
await expect(storageModule.put).toHaveBeenCalled(); await expect(storageProviderSpy.put).toHaveBeenCalled();
}); });
it('should clear settings', async () => { it('should clear settings', async () => {
const category = SETTING_MOCKS[0].categories[0]; await settingsProvider.reset();
const name = SETTING_MOCKS[0].name; await expect(storageProviderSpy.put).toHaveBeenCalledWith(STORAGE_KEY_SETTING_VALUES, {});
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0])));
await settingsProvider.clear();
const exists = await settingsProvider.settingExists(category, name);
await expect(exists).toEqual(false);
}); });
it('should reset settings', async () => { it('should reset settings', async () => {
const category = SETTING_MOCKS[0].categories[0]; const category = CONFIG_SETTINGS_MOCK[0].categories[0];
const name = SETTING_MOCKS[0].name; const name = CONFIG_SETTINGS_MOCK[0].name;
await settingsProvider.provideSetting(JSON.parse(JSON.stringify(SETTING_MOCKS[0]))); await settingsProvider.provideSetting(JSON.parse(JSON.stringify(CONFIG_SETTINGS_MOCK[0])));
await settingsProvider.setSettingValue(category, name, 'guest'); await settingsProvider.setSettingValue(category, name, 'guest');
await settingsProvider.resetDefault(); await settingsProvider.resetDefault();
const value = await settingsProvider.getSettingValue(SETTING_MOCKS[0].categories[0], SETTING_MOCKS[0].name); const value = await settingsProvider
await expect(value).toEqual(SETTING_MOCKS[0].input.defaultValue); .getValue(CONFIG_SETTINGS_MOCK[0].categories[0], CONFIG_SETTINGS_MOCK[0].name);
await expect(value).toEqual(CONFIG_SETTINGS_MOCK[0].input.defaultValue);
}); });
it('should validate wrong values for inputType text', async () => { it('should validate wrong values for inputType text', async () => {
await testValue(SETTING_MOCKS[0], 123456789); await testValue(CONFIG_SETTINGS_MOCK[0], 123456789);
await testValue(SETTING_MOCKS[0], false); await testValue(CONFIG_SETTINGS_MOCK[0], false);
await testValue(SETTING_MOCKS[0], []); await testValue(CONFIG_SETTINGS_MOCK[0], []);
}); });
it('should validate wrong values for inputType password', async () => { it('should validate wrong values for inputType password', async () => {
await testValue(SETTING_MOCKS[0], 123456789); await testValue(CONFIG_SETTINGS_MOCK[0], 123456789);
await testValue(SETTING_MOCKS[0], false); await testValue(CONFIG_SETTINGS_MOCK[0], false);
await testValue(SETTING_MOCKS[0], []); await testValue(CONFIG_SETTINGS_MOCK[0], []);
}); });
it('should validate wrong values for inputType number', async () => { it('should validate wrong values for inputType number', async () => {
await testValue(SETTING_MOCKS[2], ''); await testValue(CONFIG_SETTINGS_MOCK[2], '');
await testValue(SETTING_MOCKS[2], false); await testValue(CONFIG_SETTINGS_MOCK[2], false);
await testValue(SETTING_MOCKS[2], []); await testValue(CONFIG_SETTINGS_MOCK[2], []);
}); });
it('should validate wrong values for inputType singleChoice text', async () => { it('should validate wrong values for inputType singleChoice text', async () => {
await testValue(SETTING_MOCKS[3], ''); await testValue(CONFIG_SETTINGS_MOCK[3], '');
await testValue(SETTING_MOCKS[3], 123456); await testValue(CONFIG_SETTINGS_MOCK[3], 123456);
await testValue(SETTING_MOCKS[3], false); await testValue(CONFIG_SETTINGS_MOCK[3], false);
await testValue(SETTING_MOCKS[3], []); await testValue(CONFIG_SETTINGS_MOCK[3], []);
}); });
it('should validate wrong values for inputType singleChoice boolean', async () => { it('should validate wrong values for inputType singleChoice boolean', async () => {
await testValue(SETTING_MOCKS[5], ''); await testValue(CONFIG_SETTINGS_MOCK[5], '');
await testValue(SETTING_MOCKS[5], 123456); await testValue(CONFIG_SETTINGS_MOCK[5], 123456);
await testValue(SETTING_MOCKS[5], []); await testValue(CONFIG_SETTINGS_MOCK[5], []);
}); });
it('should validate wrong values for inputType multipleChoice', async () => { it('should validate wrong values for inputType multipleChoice', async () => {
await testValue(SETTING_MOCKS[6], ''); await testValue(CONFIG_SETTINGS_MOCK[6], '');
await testValue(SETTING_MOCKS[6], 123456); await testValue(CONFIG_SETTINGS_MOCK[6], 123456);
await testValue(SETTING_MOCKS[6], false); await testValue(CONFIG_SETTINGS_MOCK[6], false);
await testValue(SETTING_MOCKS[6], [1, 2, 3, 4]); await testValue(CONFIG_SETTINGS_MOCK[6], [1, 2, 3, 4]);
}); });
async function testValue(setting: SCSetting, value: any) { async function testValue(setting: SCSetting, value: any) {
@@ -144,10 +186,10 @@ describe('SettingsProvider', () => {
// @ts-ignore // @ts-ignore
await expect(error).toBeDefined(); await expect(error).toBeDefined();
// @ts-ignore // @ts-ignore
await expect(error.message).toMatch(/Value.*not valid/); await expect(error.message).toMatch(/is not valid/);
} }
const SETTING_MOCKS: SCSetting[] = [ const CONFIG_SETTINGS_MOCK: SCSetting[] = [
{ {
categories: ['credentials'], categories: ['credentials'],
input: { input: {
@@ -354,3 +396,16 @@ describe('SettingsProvider', () => {
}, },
]; ];
}); });
const SETTING_VALUES_MOCK: SettingValuesContainer = {
foo: {
bar: 'foo-bar',
},
privacy: {
geoLocation: 'true',
},
profile: {
group: 'employee',
language: 'de',
},
};

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 StApps * Copyright (C) 2018, 2019 StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -13,7 +13,16 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {SCSetting, SCSettingMultipleChoice, SCSettingSingleChoice, SCSettingValue} from '@openstapps/core'; import {Geolocation} from '@ionic-native/geolocation/ngx';
import {
SCSetting,
SCSettingMultipleChoice,
SCSettingSingleChoice,
SCSettingValue,
} from '@openstapps/core';
import {Logger} from '@openstapps/logger';
import * as deepMerge from 'deepmerge';
import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
export const STORAGE_KEY_SETTINGS = 'settings'; export const STORAGE_KEY_SETTINGS = 'settings';
@@ -35,14 +44,29 @@ export interface SettingsCache {
[key: string]: CategoryWithSettings; [key: string]: CategoryWithSettings;
} }
/**
* Structure with categories and its setting valueContainers for persistence
*/
export interface SettingValuesContainer {
[key: string]: SettingValueContainer;
}
/**
* Structure of a setting and its value
*/
export interface SettingValueContainer {
[key: string]: SCSettingValue | SCSettingValue[] | undefined;
}
/** /**
* Provider for app settings * Provider for app settings
*/ */
@Injectable() @Injectable()
export class SettingsProvider { export class SettingsProvider {
private categoriesOrder: string[]; categoriesOrder: string[];
private initialized = false; initialized = false;
private settingsCache: SettingsCache; logger = new Logger();
settingsCache: SettingsCache;
/** /**
* Return true if all given values are valid to possible values in given settingInput * Return true if all given values are valid to possible values in given settingInput
@@ -102,17 +126,56 @@ export class SettingsProvider {
return isValueValid; return isValueValid;
} }
constructor(public storage: StorageProvider) { constructor(private storage: StorageProvider,
private configProvider: ConfigProvider,
private geoLocation: Geolocation) {
this.categoriesOrder = []; this.categoriesOrder = [];
this.settingsCache = {}; this.settingsCache = {};
} }
/** /**
* Initializes settings cache from storage * Add an Setting to the Cache if not exist and set undefined value to defaultValue
* @param setting
*/
private async addSetting(setting: SCSetting): Promise<void> {
if (!this.categoryExists(setting.categories[0])) {
await this.provideCategory(setting.categories[0]);
}
if (!this.settingExists(setting.categories[0], setting.name)) {
if (setting.input.value === undefined) {
setting.input.value = setting.input.defaultValue;
}
this.settingsCache[setting.categories[0]].settings[setting.name] = setting;
}
}
/**
* Returns all setting values from settingsCache in a SettingsValueContainer
*/
private getSettingValuesFromCache(): SettingValuesContainer {
const settingValuesContainer: SettingValuesContainer = {};
// iterate through keys of categories
for (const categoryKey of Object.keys(this.settingsCache)) {
// iterate through keys of settingValueContainer
for (const settingKey of Object.keys(this.settingsCache[categoryKey].settings)) {
if (typeof settingValuesContainer[categoryKey] === 'undefined') {
settingValuesContainer[categoryKey] = {};
}
settingValuesContainer[categoryKey][settingKey] =
this.settingsCache[categoryKey].settings[settingKey].input.value;
}
}
return settingValuesContainer;
}
/**
* Initializes settings from config and stored values if exist
*/ */
private async initSettings(): Promise<void> { private async initSettings(): Promise<void> {
try { try {
this.settingsCache = await this.storage.get<SettingsCache>(STORAGE_KEY_SETTING_VALUES); const settings: SCSetting[] = (await this.configProvider.getValue('settings')) as SCSetting[];
settings.forEach((setting) => this.addSetting(setting));
for (const category of Object.keys(this.settingsCache)) { for (const category of Object.keys(this.settingsCache)) {
if (!this.categoriesOrder.includes(category)) { if (!this.categoriesOrder.includes(category)) {
this.categoriesOrder.push(category); this.categoriesOrder.push(category);
@@ -121,7 +184,29 @@ export class SettingsProvider {
} catch (error) { } catch (error) {
this.settingsCache = {}; this.settingsCache = {};
} }
await this.saveSettings();
if (await this.storage.has(STORAGE_KEY_SETTING_VALUES)) {
// get setting values from StorageProvider into settingsCache
const valuesContainer: SettingValuesContainer =
await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
// iterate through keys of categories
for (const categoryKey of Object.keys(this.settingsCache)) {
// iterate through setting keys of category
for (const settingKey of Object.keys(this.settingsCache[categoryKey].settings)) {
// if saved setting value exists set it, otherwise set to default value
if (typeof valuesContainer[categoryKey] !== 'undefined'
&& typeof valuesContainer[categoryKey][settingKey] !== 'undefined') {
this.settingsCache[categoryKey].settings[settingKey].input.value =
valuesContainer[categoryKey][settingKey];
} else {
this.settingsCache[categoryKey].settings[settingKey].input.value =
this.settingsCache[categoryKey].settings[settingKey].input.defaultValue;
}
}
}
await this.saveSettingValues();
}
this.initialized = true; this.initialized = true;
} }
@@ -150,16 +235,40 @@ export class SettingsProvider {
} }
/** /**
* Removes all provided settings * Checks for user permission to use location
*/ */
public async clear(): Promise<void> { public async checkGeoLocationPermission(): Promise<boolean> {
await this.init(); // request geoLocation to test the user permission
this.settingsCache = {}; try {
await this.saveSettings(); // set enableHighAccuracy, otherwise android platform does not respond
const options = {
enableHighAccuracy: true,
};
await this.geoLocation.getCurrentPosition(options);
} catch (error) {
// if error code is 1 the user denied permission,
// other errors like 'timeout' or 'no location' will be ignored here
if (error.code === 1) {
// ios has special error message for disabled location services, for the setting we ignore it
if (error.message.toLowerCase() !== 'location services are disabled.') {
// revert setting value
return false;
}
}
}
return true;
} }
/** /**
* returns an array with the order of categories * Returns copy of cached settings
*/
public async getCache(): Promise<SettingsCache> {
await this.init();
return JSON.parse(JSON.stringify(this.settingsCache));
}
/**
* Returns an array with the order of categories
*/ */
public getCategoriesOrder(): string[] { public getCategoriesOrder(): string[] {
return this.categoriesOrder; return this.categoriesOrder;
@@ -175,33 +284,27 @@ export class SettingsProvider {
public async getSetting(category: string, name: string): Promise<any> { public async getSetting(category: string, name: string): Promise<any> {
await this.init(); await this.init();
if (this.settingExists(category, name)) { if (this.settingExists(category, name)) {
// return a copy of the settings
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name])); return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name]));
} else { } else {
throw new Error('Setting "' + name + '" not provided'); throw new Error(`Setting "${name}" not provided`);
} }
} }
/** /**
* Returns copy of cached settings * Returns copy of a settings value if exist
*/
public async getSettingsCache(): Promise<SettingsCache> {
await this.init();
return JSON.parse(JSON.stringify(this.settingsCache));
}
/**
* Returns copy of a setting if exist
* @param category the category of requested setting * @param category the category of requested setting
* @param name the name of requested setting * @param name the name of requested setting
* *
* @throws Exception if setting is not provided * @throws Exception if setting is not provided
*/ */
public async getSettingValue(category: string, name: string): Promise<any> { public async getValue(category: string, name: string): Promise<any> {
await this.init(); await this.init();
if (this.settingExists(category, name)) { if (this.settingExists(category, name)) {
// return a copy of the settings value
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].input.value)); return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].input.value));
} else { } else {
throw new Error('Setting "' + name + '" not provided'); throw new Error(`Setting "${name}" not provided`);
} }
} }
@@ -220,37 +323,44 @@ export class SettingsProvider {
*/ */
public async provideSetting(setting: SCSetting): Promise<void> { public async provideSetting(setting: SCSetting): Promise<void> {
await this.init(); await this.init();
if (!this.categoryExists(setting.categories[0])) { await this.addSetting(setting);
await this.provideCategory(setting.categories[0]); }
}
if (!this.settingExists(setting.categories[0], setting.name)) { /**
// set value to default * Deletes saved values and reinitialising the settings
if (setting.input.value === undefined) { */
setting.input.value = setting.input.defaultValue; public async reset(): Promise<void> {
} await this.storage.put(STORAGE_KEY_SETTING_VALUES, {});
this.settingsCache[setting.categories[0]].settings[setting.name] = setting; await this.initSettings();
await this.saveSettings();
}
} }
/** /**
* Sets values of all settings to defaultValue * Sets values of all settings to defaultValue
*/ */
public async resetDefault(): Promise<void> { async resetDefault(): Promise<void> {
await this.init();
for (const catKey of Object.keys(this.settingsCache)) { for (const catKey of Object.keys(this.settingsCache)) {
for (const settingKey of Object.keys(this.settingsCache[catKey].settings)) { for (const settingKey of Object.keys(this.settingsCache[catKey].settings)) {
const settingInput = this.settingsCache[catKey].settings[settingKey].input; const settingInput = this.settingsCache[catKey].settings[settingKey].input;
settingInput.value = settingInput.defaultValue; settingInput.value = settingInput.defaultValue;
} }
} }
await this.saveSettingValues();
} }
/** /**
* Saves cached settings in app storage * Saves cached settings in app storage
*/ */
public async saveSettings(): Promise<void> { public async saveSettingValues(): Promise<void> {
await this.storage.put(STORAGE_KEY_SETTING_VALUES, this.settingsCache); if (await this.storage.has(STORAGE_KEY_SETTING_VALUES)) {
const savedSettingsValues: SettingValuesContainer =
await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
const cacheSettingsValues = this.getSettingValuesFromCache();
const mergedSettingValues = deepMerge(savedSettingsValues, cacheSettingsValues);
await this.storage
.put<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES, mergedSettingValues);
} else {
await this.storage.put<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES, this.getSettingValuesFromCache());
}
} }
/** /**
@@ -269,20 +379,21 @@ export class SettingsProvider {
* *
* @throws Exception if setting is not provided or value not valid to the settings inputType * @throws Exception if setting is not provided or value not valid to the settings inputType
*/ */
public async setSettingValue(category: string, name: string, value: any): Promise<void> { public async setSettingValue(category: string, name: string,
value: any): Promise<void> {
await this.init(); await this.init();
if (this.settingExists(category, name)) { if (this.settingExists(category, name)) {
const setting: SCSetting = this.settingsCache[category].settings[name]; const setting: SCSetting = this.settingsCache[category].settings[name];
const isValueValid = SettingsProvider.validateValue(setting, value); const isValueValid = SettingsProvider.validateValue(setting, value);
if (isValueValid) { if (isValueValid) {
this.settingsCache[category].settings[name].input.value = value; this.settingsCache[category].settings[name].input.value = value;
await this.saveSettings(); await this.saveSettingValues();
} else { } else {
throw new Error('Value "' + value + '" of type ' + throw new Error(`Value "${value}" of type
typeof value + ' is not valid for ' + setting.input.inputType); ${typeof value} is not valid for ${setting.input.inputType}`);
} }
} else { } else {
throw new Error('setting ' + name + ' is not provided'); throw new Error(`setting ${name} is not provided`);
} }
} }

View File

@@ -16,10 +16,16 @@
} }
}, },
"settings": { "settings": {
"resetAlert.title": "Alle Einstellungen zurücksetzen?",
"resetAlert.message": "Sind Sie sich sicher, alle Einstellungen auf ihre Anfangswerte zurückzusetzen?",
"resetAlert.buttonYes": "Ja",
"resetAlert.buttonCancel": "Abbrechen",
"resetToast.message": "Einstellungen wurden zurückgesetzt",
"title": "Einstellungen", "title": "Einstellungen",
"geoLocation": { "geoLocation": {
"permission_denied_title": "Erlaubnis für Ortungsdienst nicht gegeben", "permission_denied_title": "Erlaubnis für Ortungsdienst nicht gegeben",
"permission_denied_message": "Erlaube der App die Nutzung des Ortungsdienstes, um diese Funktion zu aktivieren." "permission_denied_message": "Erlaube der App die Nutzung des Ortungsdienstes, um diese Funktion zu aktivieren."
} },
"resetSettings": "Einstellungen zurücksetzen"
} }
} }

View File

@@ -16,10 +16,16 @@
} }
}, },
"settings": { "settings": {
"resetAlert.title": "Reset all settings?",
"resetAlert.message": "Are you shure to reset all settings to defaults values?",
"resetAlert.buttonYes": "Yes",
"resetAlert.buttonCancel": "Cancel",
"resetToast.message": "Settings reset",
"title": "Settings", "title": "Settings",
"geoLocation": { "geoLocation": {
"permission_denied_title": "Location permission not granted", "permission_denied_title": "Location permission not granted",
"permission_denied_message": "Allow this app to use location services to activate this feature." "permission_denied_message": "Allow this app to use location services to activate this feature."
} },
"resetSettings": "Reset Settings"
} }
} }