mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-19 08:02:55 +00:00
495 lines
14 KiB
TypeScript
495 lines
14 KiB
TypeScript
/*
|
|
* Copyright (C) 2019, 2020 StApps
|
|
* 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
|
|
* Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
import {Injectable} from '@angular/core';
|
|
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
|
import deepMerge from 'deepmerge';
|
|
import {Subject} from 'rxjs';
|
|
import {ConfigProvider} from '../config/config.provider';
|
|
import {StorageProvider} from '../storage/storage.provider';
|
|
|
|
export const STORAGE_KEY_SETTINGS = 'settings';
|
|
export const STORAGE_KEY_SETTINGS_SEPARATOR = '.';
|
|
export const STORAGE_KEY_SETTING_VALUES = `${STORAGE_KEY_SETTINGS}${STORAGE_KEY_SETTINGS_SEPARATOR}values`;
|
|
|
|
/**
|
|
* Category structure of settings cache
|
|
*/
|
|
export interface CategoryWithSettings {
|
|
/**
|
|
* Category name
|
|
*/
|
|
category: string;
|
|
/**
|
|
* Settings that belong in this category
|
|
*/
|
|
settings: {[key: string]: SCSetting};
|
|
}
|
|
|
|
/**
|
|
* Structure of SettingsCache
|
|
*/
|
|
export interface SettingsCache {
|
|
[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;
|
|
}
|
|
|
|
/**
|
|
* Structure of the settings events
|
|
*/
|
|
export interface SettingsAction {
|
|
/**
|
|
* Data related to the action
|
|
*/
|
|
payload?: {
|
|
/**
|
|
* Setting category
|
|
*/
|
|
category: string;
|
|
/**
|
|
* Setting name
|
|
*/
|
|
name: string;
|
|
/**
|
|
* Setting value
|
|
*/
|
|
value: SCSettingValue | SCSettingValues;
|
|
};
|
|
|
|
/**
|
|
* Type of the settings action
|
|
*/
|
|
type: string;
|
|
}
|
|
|
|
/**
|
|
* Provider for app settings
|
|
*/
|
|
@Injectable()
|
|
export class SettingsProvider {
|
|
/**
|
|
* Source of settings actions
|
|
*/
|
|
private settingsActionSource = new Subject<SettingsAction>();
|
|
|
|
/**
|
|
* Order of the setting categories
|
|
*/
|
|
categoriesOrder: string[];
|
|
|
|
/**
|
|
* Settings actions observable
|
|
*/
|
|
settingsActionChanged$ = this.settingsActionSource.asObservable();
|
|
|
|
/**
|
|
* Cache for the imported settings
|
|
*/
|
|
settingsCache: SettingsCache;
|
|
|
|
/**
|
|
* Return true if all given values are valid to possible values in given settingInput
|
|
*
|
|
* @param possibleValues Possible values
|
|
* @param enteredValues Entered value
|
|
*/
|
|
public static checkMultipleChoiceValue(
|
|
possibleValues: SCSettingValues | undefined,
|
|
enteredValues: SCSettingValues,
|
|
): boolean {
|
|
if (typeof possibleValues === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
for (const value of enteredValues) {
|
|
if (!possibleValues.includes(value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns true if given value is valid to possible values in given settingInput
|
|
*
|
|
* @param possibleValues Possible values
|
|
* @param enteredValue Entered value
|
|
*/
|
|
public static checkSingleChoiceValue(
|
|
possibleValues: SCSettingValues | undefined,
|
|
enteredValue: SCSettingValue,
|
|
): boolean {
|
|
if (typeof possibleValues === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
possibleValues !== undefined &&
|
|
Array.isArray(possibleValues) &&
|
|
possibleValues.includes(enteredValue)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Validates value for given settings inputType. Returns true if value is valid.
|
|
*
|
|
* @param setting setting to check value against
|
|
* @param value value to validate
|
|
*/
|
|
public static validateValue(
|
|
setting: SCSetting,
|
|
value: SCSettingValue | SCSettingValues,
|
|
): boolean {
|
|
let isValueValid = false;
|
|
switch (setting.inputType) {
|
|
case 'number':
|
|
if (typeof value === 'number') {
|
|
isValueValid = true;
|
|
}
|
|
break;
|
|
case 'multiple choice':
|
|
isValueValid = !Array.isArray(value)
|
|
? false
|
|
: SettingsProvider.checkMultipleChoiceValue(setting.values, value);
|
|
break;
|
|
case 'password':
|
|
case 'text':
|
|
if (typeof value === 'string') {
|
|
isValueValid = true;
|
|
}
|
|
break;
|
|
case 'single choice':
|
|
isValueValid = Array.isArray(value)
|
|
? false
|
|
: SettingsProvider.checkSingleChoiceValue(setting.values, value);
|
|
break;
|
|
default:
|
|
}
|
|
|
|
return isValueValid;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param storage TODO
|
|
* @param configProvider TODO
|
|
*/
|
|
constructor(
|
|
private readonly storage: StorageProvider,
|
|
private readonly configProvider: ConfigProvider,
|
|
) {
|
|
this.categoriesOrder = [];
|
|
this.settingsCache = {};
|
|
}
|
|
|
|
/**
|
|
* Add an Setting to the Cache if not exist and set undefined value to defaultValue
|
|
*
|
|
* @param setting Setting with categories, defaultValue, name, input type and valid values
|
|
*/
|
|
private addSetting(setting: SCSetting): void {
|
|
if (!this.categoryExists(setting.categories[0])) {
|
|
this.provideCategory(setting.categories[0]);
|
|
}
|
|
if (!this.settingExists(setting.categories[0], setting.name)) {
|
|
if (setting.value === undefined) {
|
|
setting.value = setting.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].value;
|
|
}
|
|
}
|
|
|
|
return settingValuesContainer;
|
|
}
|
|
|
|
/**
|
|
* Add category if not exists
|
|
*
|
|
* @param category the category to provide
|
|
*/
|
|
private provideCategory(category: string): void {
|
|
if (!this.categoryExists(category)) {
|
|
if (!this.categoriesOrder.includes(category)) {
|
|
this.categoriesOrder.push(category);
|
|
}
|
|
this.settingsCache[category] = {
|
|
category: category,
|
|
settings: {},
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if category exists
|
|
*
|
|
* @param category Category key name
|
|
*/
|
|
public categoryExists(category: string): boolean {
|
|
return this.settingsCache[category] !== undefined;
|
|
}
|
|
|
|
/**
|
|
* 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[] {
|
|
return this.categoriesOrder;
|
|
}
|
|
|
|
/**
|
|
* Returns copy of a setting if exist
|
|
*
|
|
* @param category the category of requested setting
|
|
* @param name the name of requested setting
|
|
* @throws Exception if setting is not provided
|
|
*/
|
|
public async getSetting(category: string, name: string): Promise<SCSetting> {
|
|
await this.init();
|
|
if (this.settingExists(category, name)) {
|
|
// return a copy of the settings
|
|
return JSON.parse(
|
|
JSON.stringify(this.settingsCache[category].settings[name]),
|
|
);
|
|
}
|
|
throw new Error(`Setting "${name}" not provided`);
|
|
}
|
|
|
|
/**
|
|
* Returns copy of a settings value if exist
|
|
*
|
|
* @param category the category of requested setting
|
|
* @param name the name of requested setting
|
|
* @throws Exception if setting is not provided
|
|
*/
|
|
public async getValue(
|
|
category: string,
|
|
name: string,
|
|
): Promise<SCSettingValue | SCSettingValues> {
|
|
await this.init();
|
|
if (this.settingExists(category, name)) {
|
|
// return a copy of the settings value
|
|
return JSON.parse(
|
|
JSON.stringify(this.settingsCache[category].settings[name].value),
|
|
);
|
|
}
|
|
throw new Error(`Setting "${name}" not provided`);
|
|
}
|
|
|
|
/**
|
|
* Initializes settings from config and stored values if exist
|
|
*/
|
|
public async init(): Promise<void> {
|
|
try {
|
|
const settings: SCSetting[] = (await this.configProvider.getValue(
|
|
'settings',
|
|
)) as SCSetting[];
|
|
for (const setting of settings) this.addSetting(setting);
|
|
|
|
for (const category of Object.keys(this.settingsCache)) {
|
|
if (!this.categoriesOrder.includes(category)) {
|
|
this.categoriesOrder.push(category);
|
|
}
|
|
}
|
|
} catch {
|
|
this.settingsCache = {};
|
|
}
|
|
|
|
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].value =
|
|
valuesContainer[categoryKey][settingKey];
|
|
} else {
|
|
this.settingsCache[categoryKey].settings[settingKey].value =
|
|
this.settingsCache[categoryKey].settings[settingKey].defaultValue;
|
|
}
|
|
}
|
|
}
|
|
await this.saveSettingValues();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds given setting and its category if not exist
|
|
*
|
|
* @param setting the setting to add
|
|
*/
|
|
public provideSetting(setting: SCSetting): void {
|
|
this.addSetting(setting);
|
|
}
|
|
|
|
/**
|
|
* Deletes saved values and reinitialising the settings
|
|
*/
|
|
public async reset(): Promise<void> {
|
|
await this.storage.put(STORAGE_KEY_SETTING_VALUES, {});
|
|
await this.init();
|
|
}
|
|
|
|
/**
|
|
* Sets values of all settings to defaultValue
|
|
*/
|
|
async resetDefault(): Promise<void> {
|
|
for (const catKey of Object.keys(this.settingsCache)) {
|
|
for (const settingKey of Object.keys(
|
|
this.settingsCache[catKey].settings,
|
|
)) {
|
|
const settingInput = this.settingsCache[catKey].settings[settingKey];
|
|
settingInput.value = settingInput.defaultValue;
|
|
}
|
|
}
|
|
await this.saveSettingValues();
|
|
}
|
|
|
|
/**
|
|
* Saves cached settings in app storage
|
|
*/
|
|
public async saveSettingValues(): Promise<void> {
|
|
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(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the order the given categories show up in the settings page
|
|
*
|
|
* @param categoryNames the order of the categories
|
|
*/
|
|
public setCategoriesOrder(categoryNames: string[]) {
|
|
this.categoriesOrder = categoryNames;
|
|
}
|
|
|
|
/**
|
|
* Sets a valid value of a setting and persists changes in storage. Also the changes get published bey Events
|
|
*
|
|
* @param category Category key name
|
|
* @param name Setting key name
|
|
* @param value Value to be set
|
|
* @throws Exception if setting is not provided or value not valid to the settings inputType
|
|
*/
|
|
public async setSettingValue(
|
|
category: string,
|
|
name: string,
|
|
value: SCSettingValue | SCSettingValues,
|
|
): Promise<void> {
|
|
await this.init();
|
|
if (this.settingExists(category, name)) {
|
|
const setting: SCSetting = this.settingsCache[category].settings[name];
|
|
const isValueValid = SettingsProvider.validateValue(setting, value);
|
|
if (isValueValid) {
|
|
// set and persist new value
|
|
this.settingsCache[category].settings[name].value = value;
|
|
await this.saveSettingValues();
|
|
// publish setting changes
|
|
this.settingsActionSource.next({
|
|
type: 'stapps.settings.changed',
|
|
payload: {category, name, value},
|
|
});
|
|
} else {
|
|
throw new Error(`Value "${value}" of type ${typeof value}
|
|
is not valid for "${setting.inputType}" of "${category}.${name}".
|
|
Ommiting change`);
|
|
}
|
|
} else {
|
|
throw new Error(`Setting "${name}" doesn't exisits within "${category}"`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if setting in category exists
|
|
*
|
|
* @param category Category key name
|
|
* @param setting Setting key name
|
|
*/
|
|
public settingExists(category: string, setting: string): boolean {
|
|
return (
|
|
this.categoryExists(category) &&
|
|
this.settingsCache[category].settings[setting] !== undefined
|
|
);
|
|
}
|
|
}
|