TeaWeb/shared/js/settings.ts

564 lines
19 KiB
TypeScript
Raw Normal View History

2019-09-01 15:24:06 +00:00
//Used by CertAccept popup
2020-03-30 11:44:18 +00:00
import {createErrorModal} from "tc-shared/ui/elements/Modal";
import {LogCategory} from "tc-shared/log";
import * as loader from "tc-loader";
import * as log from "tc-shared/log";
2018-04-16 18:38:35 +00:00
if(typeof(customElements) !== "undefined") {
2019-01-19 12:03:51 +00:00
try {
class X_Properties extends HTMLElement {}
class X_Property extends HTMLElement {}
2018-03-24 22:38:01 +00:00
2019-01-19 12:03:51 +00:00
customElements.define('x-properties', X_Properties, { extends: 'div' });
customElements.define('x-property', X_Property, { extends: 'div' });
} catch(error) {
2020-03-30 11:44:18 +00:00
console.warn("failed to define costume elements");
2019-01-19 12:03:51 +00:00
}
2018-04-16 18:38:35 +00:00
}
2018-03-24 22:38:01 +00:00
2019-03-17 11:15:39 +00:00
/* T = value type */
2020-03-30 11:44:18 +00:00
export interface SettingsKey<T> {
2019-03-17 11:15:39 +00:00
key: string;
fallback_keys?: string | string[];
fallback_imports?: {[key: string]:(value: string) => T};
description?: string;
2019-04-04 19:47:52 +00:00
default_value?: T;
2019-08-21 08:00:01 +00:00
require_restart?: boolean;
2019-03-17 11:15:39 +00:00
}
2020-03-30 11:44:18 +00:00
export class SettingsBase {
2019-04-04 19:47:52 +00:00
protected static readonly UPDATE_DIRECT: boolean = true;
2018-02-27 16:20:49 +00:00
2019-03-17 11:15:39 +00:00
protected static transformStO?<T>(input?: string, _default?: T, default_type?: string) : T {
default_type = default_type || typeof _default;
2018-04-19 17:46:47 +00:00
if (typeof input === "undefined") return _default;
2019-03-17 11:15:39 +00:00
if (default_type === "string") return input as any;
else if (default_type === "number") return parseInt(input) as any;
else if (default_type === "boolean") return (input == "1" || input == "true") as any;
else if (default_type === "undefined") return input as any;
2018-04-19 17:46:47 +00:00
return JSON.parse(input) as any;
}
2018-02-27 16:20:49 +00:00
2018-04-19 17:46:47 +00:00
protected static transformOtS?<T>(input: T) : string {
if (typeof input === "string") return input as string;
else if (typeof input === "number") return input.toString();
else if (typeof input === "boolean") return input ? "1" : "0";
else if (typeof input === "undefined") return undefined;
2018-04-19 17:46:47 +00:00
return JSON.stringify(input);
}
2018-02-27 16:20:49 +00:00
2019-03-17 11:15:39 +00:00
protected static resolveKey<T>(key: SettingsKey<T>, _default: T, resolver: (key: string) => string | boolean, default_type?: string) : T {
let value = resolver(key.key);
if(!value) {
/* trying fallbacks */
for(const fallback of key.fallback_keys || []) {
value = resolver(fallback);
if(typeof(value) === "string") {
/* fallback key succeeded */
const importer = (key.fallback_imports || {})[fallback];
if(importer)
return importer(value);
break;
}
}
}
if(typeof(value) !== 'string')
return _default;
2019-04-04 19:47:52 +00:00
return SettingsBase.transformStO(value as string, _default, default_type);
2019-03-17 11:15:39 +00:00
}
protected static keyify<T>(key: string | SettingsKey<T>) : SettingsKey<T> {
if(typeof(key) === "string")
return {key: key};
if(typeof(key) === "object" && key.key)
return key;
throw "key is not a key";
}
2019-04-04 19:47:52 +00:00
}
2020-03-30 11:44:18 +00:00
export class StaticSettings extends SettingsBase {
2019-04-04 19:47:52 +00:00
private static _instance: StaticSettings;
static get instance() : StaticSettings {
if(!this._instance)
this._instance = new StaticSettings(true);
return this._instance;
}
2019-03-17 11:15:39 +00:00
2018-04-19 17:46:47 +00:00
protected _handle: StaticSettings;
protected _staticPropsTag: JQuery;
2018-04-16 18:38:35 +00:00
2018-04-19 17:46:47 +00:00
protected constructor(_reserved = undefined) {
2019-04-04 19:47:52 +00:00
super();
2018-04-19 17:46:47 +00:00
if(_reserved && !StaticSettings._instance) {
this._staticPropsTag = $("#properties");
this.initializeStatic();
} else {
this._handle = StaticSettings.instance;
}
2018-04-16 18:38:35 +00:00
}
private initializeStatic() {
2019-04-04 19:47:52 +00:00
let search;
if(window.opener && window.opener !== window) {
search = new URL(window.location.href).search;
} else {
search = location.search;
}
search.substr(1).split("&").forEach(part => {
2018-04-16 18:38:35 +00:00
let item = part.split("=");
$("<x-property></x-property>")
2018-04-16 18:38:35 +00:00
.attr("key", item[0])
.attr("value", item[1])
.appendTo(this._staticPropsTag);
});
}
2019-03-17 11:15:39 +00:00
static?<T>(key: string | SettingsKey<T>, _default?: T, default_type?: string) : T {
if(this._handle) return this._handle.static<T>(key, _default, default_type);
key = StaticSettings.keyify(key);
return StaticSettings.resolveKey(key, _default, key => {
let result = this._staticPropsTag.find("[key='" + key + "']");
if(result.length > 0)
return decodeURIComponent(result.last().attr('value'));
return false;
}, default_type);
2018-02-27 16:20:49 +00:00
}
2019-03-17 11:15:39 +00:00
deleteStatic<T>(key: string | SettingsKey<T>) {
2018-04-19 17:46:47 +00:00
if(this._handle) {
2019-03-17 11:15:39 +00:00
this._handle.deleteStatic<T>(key);
2018-04-19 17:46:47 +00:00
return;
}
2019-03-17 11:15:39 +00:00
key = StaticSettings.keyify(key);
let result = this._staticPropsTag.find("[key='" + key.key + "']");
2018-04-19 17:46:47 +00:00
if(result.length != 0) result.detach();
}
}
2020-03-30 11:44:18 +00:00
export class Settings extends StaticSettings {
2020-03-27 15:15:15 +00:00
static readonly KEY_USER_IS_NEW: SettingsKey<boolean> = {
key: 'user_is_new_user',
default_value: true
};
2019-06-26 12:06:20 +00:00
static readonly KEY_DISABLE_COSMETIC_SLOWDOWN: SettingsKey<boolean> = {
key: 'disable_cosmetic_slowdown',
description: 'Disable the cosmetic slowdows in some processes, like icon upload.'
};
2019-03-17 11:15:39 +00:00
static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey<boolean> = {
key: 'disableContextMenu',
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier',
default_value: false
2019-03-17 11:15:39 +00:00
};
2019-08-21 08:00:01 +00:00
static readonly KEY_DISABLE_GLOBAL_CONTEXT_MENU: SettingsKey<boolean> = {
key: 'disableGlobalContextMenu',
description: 'Disable the general context menu prevention',
default_value: false
};
2019-03-17 11:15:39 +00:00
static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey<boolean> = {
key: 'disableUnloadDialog',
description: 'Disables the unload popup on side closing'
};
static readonly KEY_DISABLE_VOICE: SettingsKey<boolean> = {
key: 'disableVoice',
description: 'Disables the voice bridge. If disabled, the audio and codec workers aren\'t required anymore'
};
2019-04-04 19:47:52 +00:00
static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey<boolean> = {
key: 'disableMultiSession',
2019-08-21 08:00:01 +00:00
default_value: false,
require_restart: true
2019-04-04 19:47:52 +00:00
};
2019-03-17 11:15:39 +00:00
2019-03-25 19:04:04 +00:00
static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey<boolean> = {
key: 'dummy_load_error',
description: 'Triggers a loading error at the end of the loading process.'
};
/* Default client states */
static readonly KEY_CLIENT_STATE_MICROPHONE_MUTED: SettingsKey<boolean> = {
key: 'client_state_microphone_muted',
default_value: false,
fallback_keys: ["mute_input"]
2019-03-17 11:15:39 +00:00
};
static readonly KEY_CLIENT_STATE_SPEAKER_MUTED: SettingsKey<boolean> = {
key: 'client_state_speaker_muted',
default_value: false,
fallback_keys: ["mute_output"]
2019-03-17 11:15:39 +00:00
};
static readonly KEY_CLIENT_STATE_QUERY_SHOWN: SettingsKey<boolean> = {
key: 'client_state_query_shown',
default_value: false,
fallback_keys: ["show_server_queries"]
2019-03-17 11:15:39 +00:00
};
static readonly KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS: SettingsKey<boolean> = {
key: 'client_state_subscribe_all_channels',
default_value: true,
fallback_keys: ["channel_subscribe_all"]
};
static readonly KEY_CLIENT_STATE_AWAY: SettingsKey<boolean> = {
key: 'client_state_away',
default_value: false
};
static readonly KEY_CLIENT_AWAY_MESSAGE: SettingsKey<string> = {
key: 'client_away_message',
default_value: ""
2019-03-17 11:15:39 +00:00
};
/* Connect parameters */
static readonly KEY_FLAG_CONNECT_DEFAULT: SettingsKey<boolean> = {
key: 'connect_default'
};
static readonly KEY_CONNECT_ADDRESS: SettingsKey<string> = {
key: 'connect_address'
};
static readonly KEY_CONNECT_PROFILE: SettingsKey<string> = {
key: 'connect_profile',
default_value: 'default'
2019-03-17 11:15:39 +00:00
};
static readonly KEY_CONNECT_USERNAME: SettingsKey<string> = {
key: 'connect_username'
};
static readonly KEY_CONNECT_PASSWORD: SettingsKey<string> = {
key: 'connect_password'
};
static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = {
key: 'connect_password_hashed'
};
2019-08-21 08:00:01 +00:00
static readonly KEY_CONNECT_HISTORY: SettingsKey<string> = {
key: 'connect_history'
};
2020-06-15 14:56:05 +00:00
static readonly KEY_CONNECT_NO_SINGLE_INSTANCE: SettingsKey<boolean> = {
key: 'connect_no_single_instance',
default_value: false
};
2019-03-17 11:15:39 +00:00
2019-11-24 00:16:07 +00:00
static readonly KEY_CONNECT_NO_DNSPROXY: SettingsKey<boolean> = {
key: 'connect_no_dnsproxy',
default_value: false
};
2019-04-04 19:47:52 +00:00
static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey<string> = {
key: 'certificate_callback'
};
2019-04-29 16:49:01 +00:00
/* sounds */
static readonly KEY_SOUND_MASTER: SettingsKey<number> = {
2019-08-21 08:00:01 +00:00
key: 'audio_master_volume',
default_value: 100
2019-04-29 16:49:01 +00:00
};
static readonly KEY_SOUND_MASTER_SOUNDS: SettingsKey<number> = {
2019-08-21 08:00:01 +00:00
key: 'audio_master_volume_sounds',
default_value: 100
};
static readonly KEY_CHAT_FIXED_TIMESTAMPS: SettingsKey<boolean> = {
key: 'chat_fixed_timestamps',
default_value: false,
description: 'Enables fixed timestamps for chat messages and disabled the updating once (2 seconds ago... etc)'
};
static readonly KEY_CHAT_COLLOQUIAL_TIMESTAMPS: SettingsKey<boolean> = {
key: 'chat_colloquial_timestamps',
default_value: true,
description: 'Enabled colloquial timestamp formatting like "Yesterday at ..." or "Today at ..."'
};
static readonly KEY_CHAT_COLORED_EMOJIES: SettingsKey<boolean> = {
key: 'chat_colored_emojies',
default_value: true,
description: 'Enables colored emojies powered by Twemoji'
};
static readonly KEY_CHAT_TAG_URLS: SettingsKey<boolean> = {
key: 'chat_tag_urls',
default_value: true,
description: 'Automatically link urls with [url]'
};
static readonly KEY_CHAT_ENABLE_MARKDOWN: SettingsKey<boolean> = {
key: 'chat_enable_markdown',
default_value: true,
description: 'Enabled markdown chat support.'
};
static readonly KEY_CHAT_ENABLE_BBCODE: SettingsKey<boolean> = {
key: 'chat_enable_bbcode',
default_value: false,
2019-08-21 08:00:01 +00:00
description: 'Enabled bbcode support in chat.'
};
static readonly KEY_CHAT_IMAGE_WHITELIST_REGEX: SettingsKey<string> = {
key: 'chat_image_whitelist_regex',
default_value: JSON.stringify([])
};
2019-08-21 08:00:01 +00:00
static readonly KEY_SWITCH_INSTANT_CHAT: SettingsKey<boolean> = {
key: 'switch_instant_chat',
default_value: true,
description: 'Directly switch to channel chat on channel select'
};
static readonly KEY_SWITCH_INSTANT_CLIENT: SettingsKey<boolean> = {
key: 'switch_instant_client',
default_value: true,
description: 'Directly switch to client info on client select'
};
static readonly KEY_HOSTBANNER_BACKGROUND: SettingsKey<boolean> = {
key: 'hostbanner_background',
default_value: false,
description: 'Enables a default background begind the hostbanner'
};
static readonly KEY_CHANNEL_EDIT_ADVANCED: SettingsKey<boolean> = {
key: 'channel_edit_advanced',
default_value: false,
description: 'Edit channels in advanced mode with a lot more settings'
};
static readonly KEY_PERMISSIONS_SHOW_ALL: SettingsKey<boolean> = {
key: 'permissions_show_all',
default_value: false,
description: 'Show all permissions even thou they dont make sense for the server/channel group'
};
2019-08-21 08:00:01 +00:00
static readonly KEY_TEAFORO_URL: SettingsKey<string> = {
key: "teaforo_url",
default_value: "https://forum.teaspeak.de/"
};
static readonly KEY_FONT_SIZE: SettingsKey<number> = {
key: "font_size"
2019-04-29 16:49:01 +00:00
};
2019-10-19 15:13:40 +00:00
static readonly KEY_ICON_SIZE: SettingsKey<number> = {
key: "icon_size",
default_value: 100
};
2019-04-29 16:49:01 +00:00
2020-04-10 18:57:50 +00:00
static readonly KEY_KEYCONTROL_DATA: SettingsKey<string> = {
key: "keycontrol_data",
default_value: "{}"
};
2019-10-13 19:33:07 +00:00
static readonly KEY_LAST_INVITE_LINK_TYPE: SettingsKey<string> = {
key: "last_invite_link_type",
default_value: "tea-web"
};
static readonly KEY_TRANSFERS_SHOW_FINISHED: SettingsKey<boolean> = {
key: 'transfers_show_finished',
default_value: true,
description: "Show finished file transfers in the file transfer list"
};
static readonly KEY_TRANSFER_DOWNLOAD_FOLDER: SettingsKey<string> = {
key: "transfer_download_folder",
description: "The download folder for the file transfer downloads",
/* default_value: <users download directory> */
};
2019-10-13 19:33:07 +00:00
static readonly FN_INVITE_LINK_SETTING: (name: string) => SettingsKey<string> = name => {
return {
key: 'invite_link_setting_' + name
}
};
2019-08-31 16:31:01 +00:00
static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel_id: number) => SettingsKey<number> = channel => {
2019-03-17 11:15:39 +00:00
return {
2019-08-31 16:31:01 +00:00
key: 'channel_subscribe_mode_' + channel
2019-03-17 11:15:39 +00:00
}
};
2020-04-18 19:31:13 +00:00
static readonly FN_SERVER_CHANNEL_COLLAPSED: (channel_id: number) => SettingsKey<boolean> = channel => {
return {
key: 'channel_collapsed_' + channel,
default_value: false
}
};
static readonly FN_PROFILE_RECORD: (name: string) => SettingsKey<any> = name => {
return {
key: 'profile_record' + name
}
};
2019-03-17 11:15:39 +00:00
static readonly KEYS = (() => {
const result = [];
for(const key in Settings) {
if(!key.toUpperCase().startsWith("KEY_"))
continue;
if(key.toUpperCase() == "KEYS")
continue;
result.push(key);
}
return result;
})();
2018-04-19 17:46:47 +00:00
2019-08-31 16:31:01 +00:00
static initialize() {
settings = new Settings();
(window as any).settings = settings;
(window as any).Settings = Settings;
2019-08-31 16:31:01 +00:00
}
2018-04-19 17:46:47 +00:00
private cacheGlobal = {};
private saveWorker: NodeJS.Timer;
private updated: boolean = false;
constructor() {
super();
const json = localStorage.getItem("settings.global");
try {
this.cacheGlobal = JSON.parse(json);
} catch(error) {
log.error(LogCategory.GENERAL, tr("Failed to load global settings!\nJson: %s\nError: %o"), json, error);
const show_popup = () => {
createErrorModal(tr("Failed to load global settings"), tr("Failed to load global client settings!\nLookup console for more information.")).open();
};
if(!loader.finished())
loader.register_task(loader.Stage.LOADED, {
priority: 0,
name: "Settings error",
function: async () => show_popup()
});
else
show_popup();
}
2018-04-19 17:46:47 +00:00
if(!this.cacheGlobal) this.cacheGlobal = {};
this.saveWorker = setInterval(() => {
if(this.updated)
this.save();
}, 5 * 1000);
2018-04-16 18:38:35 +00:00
}
2019-03-17 11:15:39 +00:00
static_global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
2019-08-21 08:00:01 +00:00
const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default;
2019-03-17 11:15:39 +00:00
const default_object = { seed: Math.random() } as any;
let _static = this.static(key, default_object, typeof _default);
2019-08-21 08:00:01 +00:00
if(_static !== default_object) return StaticSettings.transformStO(_static, actual_default);
return this.global<T>(key, actual_default);
}
2019-03-17 11:15:39 +00:00
global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
2019-08-21 08:00:01 +00:00
const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default;
return StaticSettings.resolveKey(Settings.keyify(key), actual_default, key => this.cacheGlobal[key]);
2018-02-27 16:20:49 +00:00
}
2019-03-17 11:15:39 +00:00
changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
key = Settings.keyify(key);
if(this.cacheGlobal[key.key] === value) return;
2018-03-07 18:06:52 +00:00
2018-02-27 16:20:49 +00:00
this.updated = true;
2019-03-17 11:15:39 +00:00
this.cacheGlobal[key.key] = StaticSettings.transformOtS(value);
2018-02-27 16:20:49 +00:00
if(Settings.UPDATE_DIRECT)
this.save();
}
2019-04-04 19:47:52 +00:00
save() {
this.updated = false;
let global = JSON.stringify(this.cacheGlobal);
localStorage.setItem("settings.global", global);
2019-04-15 13:33:51 +00:00
if(localStorage.save)
localStorage.save();
2019-04-04 19:47:52 +00:00
}
}
2020-03-30 11:44:18 +00:00
export class ServerSettings extends SettingsBase {
2019-04-04 19:47:52 +00:00
private cacheServer = {};
2019-08-31 16:31:01 +00:00
private _server_unique_id: string;
2019-04-04 19:47:52 +00:00
private _server_save_worker: NodeJS.Timer;
private _server_settings_updated: boolean = false;
2019-08-21 08:00:01 +00:00
private _destroyed = false;
2019-04-04 19:47:52 +00:00
constructor() {
super();
this._server_save_worker = setInterval(() => {
if(this._server_settings_updated)
this.save();
}, 5 * 1000);
}
2019-08-21 08:00:01 +00:00
destroy() {
this._destroyed = true;
2019-08-31 16:31:01 +00:00
this._server_unique_id = undefined;
2019-08-21 08:00:01 +00:00
this.cacheServer = undefined;
clearInterval(this._server_save_worker);
this._server_save_worker = undefined;
}
2019-04-04 19:47:52 +00:00
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
2019-08-21 08:00:01 +00:00
if(this._destroyed) throw "destroyed";
2020-04-21 13:18:16 +00:00
const kkey = Settings.keyify(key);
return StaticSettings.resolveKey(kkey, typeof _default === "undefined" ? kkey.default_value : _default, key => this.cacheServer[key]);
2019-04-04 19:47:52 +00:00
}
2019-03-17 11:15:39 +00:00
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
2019-08-21 08:00:01 +00:00
if(this._destroyed) throw "destroyed";
2019-03-17 11:15:39 +00:00
key = Settings.keyify(key);
2020-04-21 13:18:16 +00:00
if(this.cacheServer[key.key] === value) return;
2018-03-07 18:06:52 +00:00
2019-04-04 19:47:52 +00:00
this._server_settings_updated = true;
2019-03-17 11:15:39 +00:00
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
2018-02-27 16:20:49 +00:00
if(Settings.UPDATE_DIRECT)
this.save();
}
2019-08-31 16:31:01 +00:00
setServer(server_unique_id: string) {
2019-08-21 08:00:01 +00:00
if(this._destroyed) throw "destroyed";
2019-08-31 16:31:01 +00:00
if(this._server_unique_id) {
2018-04-16 18:38:35 +00:00
this.save();
2018-03-07 18:06:52 +00:00
this.cacheServer = {};
2019-08-31 16:31:01 +00:00
this._server_unique_id = undefined;
2018-04-16 18:38:35 +00:00
}
2019-08-31 16:31:01 +00:00
this._server_unique_id = server_unique_id;
2018-04-16 18:38:35 +00:00
2019-08-31 16:31:01 +00:00
if(this._server_unique_id) {
const json = localStorage.getItem("settings.server_" + server_unique_id);
try {
this.cacheServer = JSON.parse(json);
} catch(error) {
log.error(LogCategory.GENERAL, tr("Failed to load server settings for server %s!\nJson: %s\nError: %o"), server_unique_id, json, error);
}
2018-04-16 18:38:35 +00:00
if(!this.cacheServer)
this.cacheServer = {};
2018-03-07 18:06:52 +00:00
}
2018-02-27 16:20:49 +00:00
}
save() {
2019-08-21 08:00:01 +00:00
if(this._destroyed) throw "destroyed";
2019-04-04 19:47:52 +00:00
this._server_settings_updated = false;
2018-02-27 16:20:49 +00:00
2019-08-31 16:31:01 +00:00
if(this._server_unique_id) {
2018-03-07 18:06:52 +00:00
let server = JSON.stringify(this.cacheServer);
2019-08-31 16:31:01 +00:00
localStorage.setItem("settings.server_" + this._server_unique_id, server);
2019-04-15 13:33:51 +00:00
if(localStorage.save)
localStorage.save();
2018-03-07 18:06:52 +00:00
}
2018-02-27 16:20:49 +00:00
}
2019-08-31 16:31:01 +00:00
}
export let settings: Settings = null;