diff --git a/.weblate b/.weblate new file mode 100644 index 000000000..d21b8ccce --- /dev/null +++ b/.weblate @@ -0,0 +1,36 @@ +; Weblate component discovery config for MistServer. +; See https://docs.weblate.org/en/latest/admin/addons.html#component-discovery +; +; Two independent components: the LSP admin panel (translated client-side, +; in the browser) and the C++ backend (translated server-side, so the JSON +; API itself can return already-translated text to standalone/other-frontend +; callers). They don't share a catalog -- see plan_localization_v2.md for +; why -- so each gets its own template/filemask/source strings. To add +; either in Weblate: either point the "Component discovery" addon at this +; file, or create the component by hand in the Weblate admin using the same +; settings. +[mistserver-lsp] +name = MistServer LSP +slug = mistserver-lsp +project = mistserver +file_format = po +filemask = lsp/lang/*.po +template = lsp/lang/mistserver-lsp.pot +new_lang = add +; msgids are the English source strings (gettext keyword extraction from +; lsp/mist.js) - there is no separate "source_language" catalog file to +; point at. +source_language = en + +[mistserver-backend] +name = MistServer Backend +slug = mistserver-backend +project = mistserver +file_format = po +filemask = src/lang/*.po +template = src/lang/mistserver-backend.pot +new_lang = add +; msgids are the English source strings (gettext keyword extraction from +; src/input|output|process capa["friendly"|"desc"|...] literals wrapped in +; tr(), plus src/controller/controller_capabilities.cpp). +source_language = en diff --git a/generated/meson.build b/generated/meson.build index 7943b5dfe..4b0ac58d9 100644 --- a/generated/meson.build +++ b/generated/meson.build @@ -44,3 +44,38 @@ endforeach server_html = custom_target('embed_server.html', output: 'server.html.h', input: gen_html, command: [sourcery, '@INPUT@', 'server_html', '@OUTPUT@']) + +# Translation catalogs, embedded as a lookup table so the controller can serve +# /translations/.json without a per-language code change. +gen_lang_header = files('../scripts/gen_lang_header.py') +lang_catalogs = custom_target('lang_catalogs', + output: 'lang_catalogs.h', + input: lang_json_tgts, + command: [py_installation, gen_lang_header, '@OUTPUT@'] + lang_specs + ['--', '@INPUT@']) + +# Backend (C++) translation catalogs. Separate from the LSP ones above: these +# are consumed by the controller itself (to translate API responses, since +# the API is also used standalone / by other front-ends), never served to a +# browser. --prefix keeps the generated types/variables from colliding with +# the frontend ones if both headers are ever included in the same file; +# --no-index skips the code -> native-name index JSON the frontend uses for +# its language picker, since nothing here ever lists backend languages. +backend_languages = [ + {'code': 'de-DE', 'name': 'Deutsch'}, +] + +backend_lang_json_tgts = [] +backend_lang_specs = [] + +foreach l : backend_languages + backend_lang_json_tgts += custom_target('backend_lang_json_' + l.get('code'), + input: '../src/lang/' + l.get('code') + '.po', + output: 'backend_' + l.get('code') + '.json', + command: [py_installation, po2json, '@INPUT@', '@OUTPUT@']) + backend_lang_specs += l.get('code') + '=' + l.get('name') +endforeach + +backend_lang_catalogs = custom_target('backend_lang_catalogs', + output: 'backend_lang_catalogs.h', + input: backend_lang_json_tgts, + command: [py_installation, gen_lang_header, '@OUTPUT@', '--prefix', 'Backend', '--no-index'] + backend_lang_specs + ['--', '@INPUT@']) diff --git a/lib/meson.build b/lib/meson.build index 4457cdb5c..619fd8041 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -61,6 +61,7 @@ headers = [ 'url.h', 'urireader.h', 'flac.h', + 'tr.h', ] if have_srt diff --git a/lib/tr.h b/lib/tr.h new file mode 100644 index 000000000..f43a8c234 --- /dev/null +++ b/lib/tr.h @@ -0,0 +1,17 @@ +#pragma once +#include + +/// Marks a backend (C++) string literal as translatable, for +/// `xgettext --keyword=tr` extraction -- nothing more. +/// +/// This is *not* the same thing as the request-time translation lookup +/// (Controller::tr(msgid, lang) in controller_i18n.h). Calls to this tr() +/// happen at capability-introspection time (e.g. `capa["friendly"] = tr( +/// "MP4 over HTTP");` in a plugin's init()), which runs once, long before +/// any request -- and therefore any requested language -- exists. Its +/// result is cached and reused for every future API request in every +/// language, so it can only ever be an identity function here. The actual +/// per-request translation happens later, in the controller, by looking up +/// the (still-English) msgid this function returned. +inline const char *tr(const char *msgid) { return msgid; } +inline const std::string &tr(const std::string &msgid) { return msgid; } diff --git a/lsp/footer.html b/lsp/footer.html index 8fcac986f..b775efc76 100644 --- a/lsp/footer.html +++ b/lsp/footer.html @@ -9,15 +9,15 @@
-

Management Interface

+

Management Interface

- Loading.. + Loading..
diff --git a/lsp/generateLSP.sh b/lsp/generateLSP.sh index 8e5e37aaf..b4c4805e9 100755 --- a/lsp/generateLSP.sh +++ b/lsp/generateLSP.sh @@ -2,7 +2,7 @@ echo "Minimizing LSP.." -terser -mo minified.js -- plugins/md5.js plugins/cattablesort.js mist.js +terser -mo minified.js -- plugins/md5.js plugins/cattablesort.js i18n.js mist.js if [ $? -eq 0 ]; then echo "Done." diff --git a/lsp/i18n.js b/lsp/i18n.js new file mode 100644 index 000000000..c352574eb --- /dev/null +++ b/lsp/i18n.js @@ -0,0 +1,198 @@ +/* MistServer LSP internationalization runtime. + * + * Source-string-as-key (gettext style): the English string is the msgid, so a + * missing catalog entry simply falls back to the source string and English + * needs no catalog at all. + * + * This file is concatenated *before* mist.js, so it must not touch UI/mist. + * mist.js hooks it up as UI.lang. + */ + +var MistLang = { + //Currently active language code, "en" means "no catalog, use the msgids". + current: "en", + //msgid -> msgstr (string, or array of plural forms) + catalog: {}, + //code -> native language name, filled in by loadIndex(). English is implicit. + available: {"en": "English"}, + //Set to true once the index has been fetched (successfully or not) + indexLoaded: false, + //Callbacks to run whenever the active language changes + onchange: [], + + /* Where to look for catalogs. When the panel is served by MistController the + * first location applies; the second one is the dev layout (lsp/index.html + * next to lsp/lang/). */ + paths: ["translations/","lang/"], + + //gettext-style plural selector. Only the ones we ship are listed; anything + //else falls back to the English/Germanic rule. + pluralRules: { + "en": function(n){ return n != 1 ? 1 : 0; }, + "de": function(n){ return n != 1 ? 1 : 0; } + }, + + isEnglish: function(code){ + return (!code) || (code == "en") || (code.slice(0,3) == "en-"); + }, + + /* Look up a msgid. Returns the msgid itself when there is no translation. + * Non-strings are passed through untouched so this is safe to use as a + * blanket pass-through on values that may be jQuery objects or numbers. */ + lookup: function(msgid){ + if (typeof msgid != "string") { return msgid; } + var hit = this.catalog[msgid]; + if (hit === undefined) { return msgid; } + if (hit instanceof Array) { hit = hit[0]; } + return (hit === "" || hit === undefined ? msgid : hit); + }, + + lookupPlural: function(singular,plural,n){ + var hit = this.catalog[singular]; + if (hit instanceof Array) { + var rule = this.pluralRules[this.current] || this.pluralRules[this.current.split("-")[0]] || this.pluralRules["en"]; + var idx = rule(n); + if ((idx < hit.length) && (hit[idx] !== "")) { return hit[idx]; } + } + else if ((typeof hit == "string") && (hit !== "") && (n == 1)) { + return hit; + } + return (n == 1 ? singular : plural); + }, + + /* printf-ish interpolation. Supports %s (sequential), %1$s (positional) and + * %% for a literal percent sign. Anything else is left alone. */ + format: function(str,args){ + if ((typeof str != "string") || !args || !args.length) { return str; } + var next = 0; + return str.replace(/%(?:(\d+)\$)?([%s])/g,function(match,pos,kind){ + if (kind == "%") { return "%"; } + var val = (pos ? args[Number(pos)-1] : args[next++]); + return (val === undefined || val === null ? "" : String(val)); + }); + }, + + /* Fetch .json, trying each configured path in order. */ + fetchCatalog: function(code,callback){ + var paths = this.paths.slice(0); + function attempt(){ + if (!paths.length) { callback(false); return; } + var url = paths.shift()+encodeURIComponent(code)+".json"; + var xhr = new XMLHttpRequest(); + xhr.open("GET",url,true); + xhr.onreadystatechange = function(){ + if (xhr.readyState != 4) { return; } + if ((xhr.status >= 200) && (xhr.status < 300)) { + var parsed = null; + try { parsed = JSON.parse(xhr.responseText); } catch (e) { parsed = null; } + if (parsed) { callback(parsed); return; } + } + attempt(); + }; + try { xhr.send(); } catch (e) { attempt(); } + } + attempt(); + }, + + /* Load the list of catalogs the server has available. */ + loadIndex: function(callback){ + var me = this; + if (this.indexLoaded) { if (callback) { callback(this.available); } return; } + this.fetchCatalog("index",function(d){ + me.indexLoaded = true; + if (d && (typeof d == "object")) { + for (var code in d) { me.available[code] = d[code]; } + } + if (callback) { callback(me.available); } + }); + }, + + /* Switch to the given language, fetching its catalog if needed. + * English never fetches anything. */ + setLanguage: function(code,callback){ + var me = this; + function done(){ + document.documentElement.setAttribute("lang",me.current); + for (var i in me.onchange) { me.onchange[i](me.current); } + if (callback) { callback(me.current); } + } + if (this.isEnglish(code)) { + this.current = "en"; + this.catalog = {}; + done(); + return; + } + if (code == this.current) { done(); return; } + this.fetchCatalog(code,function(d){ + if (d) { + me.current = code; + me.catalog = d; + } + else { + //Catalog unavailable: stay on / fall back to English rather than + //pretending we're translated. + me.current = "en"; + me.catalog = {}; + } + done(); + }); + }, + + /* Pick a language for a first-time visitor from the browser preferences. + * We match on the full code including region (de-DE), then on the bare + * language subtag (de -> de-DE) so a "de-AT" browser still gets German. */ + detect: function(){ + var prefs = []; + if (navigator.languages && navigator.languages.length) { + prefs = Array.prototype.slice.call(navigator.languages); + } + else if (navigator.language) { prefs = [navigator.language]; } + for (var i in prefs) { + var want = prefs[i]; + for (var code in this.available) { + if (code.toLowerCase() == want.toLowerCase()) { return code; } + } + var base = want.split("-")[0].toLowerCase(); + for (var code in this.available) { + if (code.split("-")[0].toLowerCase() == base) { return code; } + } + } + return "en"; + } +}; + +/* Translate a single string. + * + * Exposed as a global (and on window) rather than a local so that terser / + * closure-compiler cannot mangle the name away. The string *arguments* are + * always preserved by minifiers, which is what xgettext extracts. + * + * tr("Save") + * tr("Failed (%s)", err) + * tr("%1$s of %2$s", done, total) + */ +function tr(msgid) { + var out = MistLang.lookup(msgid); + if (arguments.length > 1) { + return MistLang.format(out,Array.prototype.slice.call(arguments,1)); + } + return out; +} + +/* ngettext-style plural form. + * + * trn("%s stream","%s streams",n,n) + */ +function trn(singular,plural,n) { + var out = MistLang.lookupPlural(singular,plural,n); + if (arguments.length > 3) { + return MistLang.format(out,Array.prototype.slice.call(arguments,3)); + } + return MistLang.format(out,[n]); +} + +if (typeof window != "undefined") { + window.MistLang = MistLang; + window.tr = tr; + window.trn = trn; +} diff --git a/lsp/lang/de-DE.po b/lsp/lang/de-DE.po new file mode 100644 index 000000000..6d84fffbf --- /dev/null +++ b/lsp/lang/de-DE.po @@ -0,0 +1,158 @@ +# German translations for mistserver-lsp package. +# Copyright (C) 2026 DDVTech +# This file is distributed under the same license as the mistserver-lsp package. +# Automatically generated, 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: mistserver-lsp\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-12 12:49+0000\n" +"PO-Revision-Date: 2026-08-09 10:19+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: lsp/mist.js:182 +msgid "MistServer MI" +msgstr "MistServer MI" + +#: lsp/mist.js:188 +msgid "Interface language" +msgstr "Oberflächensprache" + +#: lsp/mist.js:14048 lsp/mist.js:15765 +msgid "Cancel" +msgstr "Abbrechen" + +#: lsp/mist.js:14054 lsp/mist.js:15771 +msgid "Save" +msgstr "Speichern" + +#: lsp/mist.js:16769 +msgid "Account created" +msgstr "Konto erstellt" + +#: lsp/mist.js:16769 +msgid "Changelog" +msgstr "Änderungsprotokoll" + +#: lsp/mist.js:16769 +msgid "Connections" +msgstr "Verbindungen" + +#: lsp/mist.js:16769 +msgid "Create a new account" +msgstr "Neues Konto erstellen" + +#: lsp/mist.js:16770 +msgid "Disconnect" +msgstr "Trennen" + +#: lsp/mist.js:16770 +msgid "Documentation" +msgstr "Dokumentation" + +#: lsp/mist.js:16770 +msgid "Edit" +msgstr "Bearbeiten" + +#: lsp/mist.js:16770 +msgid "Edit JWK" +msgstr "JWK bearbeiten" + +#: lsp/mist.js:16770 +msgid "Edit Protocol" +msgstr "Protokoll bearbeiten" + +#: lsp/mist.js:16771 +msgid "Edit Trigger" +msgstr "Trigger bearbeiten" + +#: lsp/mist.js:16771 +msgid "Edit external writer" +msgstr "Externen Writer bearbeiten" + +#: lsp/mist.js:16771 +msgid "Edit variable" +msgstr "Variable bearbeiten" + +#: lsp/mist.js:16771 +msgid "Email for Help" +msgstr "E-Mail für Hilfe" + +#: lsp/mist.js:16772 +msgid "Embed" +msgstr "Einbetten" + +#: lsp/mist.js:16772 +msgid "General" +msgstr "Allgemein" + +#: lsp/mist.js:16772 +msgid "Login" +msgstr "Anmelden" + +#: lsp/mist.js:16772 +msgid "Logs" +msgstr "Protokolle (Logs)" + +#: lsp/mist.js:16772 +msgid "Overview" +msgstr "Übersicht" + +#: lsp/mist.js:16772 +msgid "Preview" +msgstr "Vorschau" + +#: lsp/mist.js:16773 +msgid "Protocols" +msgstr "Protokolle" + +#: lsp/mist.js:16773 +msgid "Push" +msgstr "Push" + +#: lsp/mist.js:16773 +msgid "Server Stats" +msgstr "Serverstatistiken" + +#: lsp/mist.js:16773 +msgid "Start Push" +msgstr "Push starten" + +#: lsp/mist.js:16773 +msgid "Statistics" +msgstr "Statistiken" + +#: lsp/mist.js:16774 +msgid "Status" +msgstr "Status" + +#: lsp/mist.js:16774 +msgid "Stream keys" +msgstr "Stream-Schlüssel" + +#: lsp/mist.js:16774 +msgid "Streams" +msgstr "Streams" + +#: lsp/mist.js:16774 +msgid "Triggers" +msgstr "Trigger" + +#: lsp/mist.js:16776 +msgid "Management Interface" +msgstr "Verwaltungsoberfläche" + +#: lsp/mist.js:16776 +msgid "Disconnected" +msgstr "Getrennt" + +#: lsp/mist.js:16776 +msgid "Loading.." +msgstr "Wird geladen.." diff --git a/lsp/lang/mistserver-lsp.pot b/lsp/lang/mistserver-lsp.pot new file mode 100644 index 000000000..17ca1aa1c --- /dev/null +++ b/lsp/lang/mistserver-lsp.pot @@ -0,0 +1,158 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR DDVTech +# This file is distributed under the same license as the mistserver-lsp package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: mistserver-lsp\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-12 12:49+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: lsp/mist.js:182 +msgid "MistServer MI" +msgstr "" + +#: lsp/mist.js:188 +msgid "Interface language" +msgstr "" + +#: lsp/mist.js:14048 lsp/mist.js:15765 +msgid "Cancel" +msgstr "" + +#: lsp/mist.js:14054 lsp/mist.js:15771 +msgid "Save" +msgstr "" + +#: lsp/mist.js:16769 +msgid "Account created" +msgstr "" + +#: lsp/mist.js:16769 +msgid "Changelog" +msgstr "" + +#: lsp/mist.js:16769 +msgid "Connections" +msgstr "" + +#: lsp/mist.js:16769 +msgid "Create a new account" +msgstr "" + +#: lsp/mist.js:16770 +msgid "Disconnect" +msgstr "" + +#: lsp/mist.js:16770 +msgid "Documentation" +msgstr "" + +#: lsp/mist.js:16770 +msgid "Edit" +msgstr "" + +#: lsp/mist.js:16770 +msgid "Edit JWK" +msgstr "" + +#: lsp/mist.js:16770 +msgid "Edit Protocol" +msgstr "" + +#: lsp/mist.js:16771 +msgid "Edit Trigger" +msgstr "" + +#: lsp/mist.js:16771 +msgid "Edit external writer" +msgstr "" + +#: lsp/mist.js:16771 +msgid "Edit variable" +msgstr "" + +#: lsp/mist.js:16771 +msgid "Email for Help" +msgstr "" + +#: lsp/mist.js:16772 +msgid "Embed" +msgstr "" + +#: lsp/mist.js:16772 +msgid "General" +msgstr "" + +#: lsp/mist.js:16772 +msgid "Login" +msgstr "" + +#: lsp/mist.js:16772 +msgid "Logs" +msgstr "" + +#: lsp/mist.js:16772 +msgid "Overview" +msgstr "" + +#: lsp/mist.js:16772 +msgid "Preview" +msgstr "" + +#: lsp/mist.js:16773 +msgid "Protocols" +msgstr "" + +#: lsp/mist.js:16773 +msgid "Push" +msgstr "" + +#: lsp/mist.js:16773 +msgid "Server Stats" +msgstr "" + +#: lsp/mist.js:16773 +msgid "Start Push" +msgstr "" + +#: lsp/mist.js:16773 +msgid "Statistics" +msgstr "" + +#: lsp/mist.js:16774 +msgid "Status" +msgstr "" + +#: lsp/mist.js:16774 +msgid "Stream keys" +msgstr "" + +#: lsp/mist.js:16774 +msgid "Streams" +msgstr "" + +#: lsp/mist.js:16774 +msgid "Triggers" +msgstr "" + +#: lsp/mist.js:16776 +msgid "Management Interface" +msgstr "" + +#: lsp/mist.js:16776 +msgid "Disconnected" +msgstr "" + +#: lsp/mist.js:16776 +msgid "Loading.." +msgstr "" diff --git a/lsp/meson.build b/lsp/meson.build index a2fb4aa5b..f4cbbbbbe 100644 --- a/lsp/meson.build +++ b/lsp/meson.build @@ -5,7 +5,7 @@ if get_option('LSP_MINIFY') closure_compiler = files('closure-compiler.jar') - minified = custom_target('lsp_gen_minified',output: 'minified.js', input: ['plugins/md5.js', 'plugins/cattablesort.js', 'mist.js'], command: [java, '-jar', closure_compiler, '--warning_level', 'QUIET', '@INPUT@'], capture: true) + minified = custom_target('lsp_gen_minified',output: 'minified.js', input: ['plugins/md5.js', 'plugins/cattablesort.js', 'i18n.js', 'mist.js'], command: [java, '-jar', closure_compiler, '--warning_level', 'QUIET', '@INPUT@'], capture: true) endif html_list = ['header.html', @@ -15,8 +15,30 @@ html_list = ['header.html', 'plugins/jquery.flot.min.js', 'plugins/jquery.flot.time.min.js', 'plugins/jquery.qrcode.min.js', + 'i18n.js', ] html_files = files(html_list) html_files += minified +# --- Translation catalogs ---------------------------------------------------- +# Languages shipped with the panel. English is the source language: its strings +# are the msgids, so it needs no catalog. Adding a language means dropping a +# lsp/lang/.po next to the others and adding one line here. +lsp_languages = [ + {'code': 'de-DE', 'name': 'Deutsch'}, +] + +py_installation = import('python').find_installation('python3') +po2json = files('../scripts/po2json.py') + +lang_json_tgts = [] +lang_specs = [] + +foreach l : lsp_languages + lang_json_tgts += custom_target('lang_json_' + l.get('code'), + input: 'lang/' + l.get('code') + '.po', + output: l.get('code') + '.json', + command: [py_installation, po2json, '@INPUT@', '@OUTPUT@']) + lang_specs += l.get('code') + '=' + l.get('name') +endforeach diff --git a/lsp/minified.js b/lsp/minified.js index 9a0a6105d..31761f84c 100644 --- a/lsp/minified.js +++ b/lsp/minified.js @@ -1 +1 @@ -var MD5=function(e){function t(e,t){return e<>>32-t}function a(e,t){var a,i,s,n,r;s=e&2147483648;n=t&2147483648;a=e&1073741824;i=t&1073741824;r=(e&1073741823)+(t&1073741823);if(a&i){return r^2147483648^s^n}if(a|i){if(r&1073741824){return r^3221225472^s^n}else{return r^1073741824^s^n}}else{return r^s^n}}function i(e,t,a){return e&t|~e&a}function s(e,t,a){return e&a|t&~a}function n(e,t,a){return e^t^a}function r(e,t,a){return t^(e|~a)}function o(e,s,n,r,o,l,d){e=a(e,a(a(i(s,n,r),o),d));return a(t(e,l),s)}function l(e,i,n,r,o,l,d){e=a(e,a(a(s(i,n,r),o),d));return a(t(e,l),i)}function d(e,i,s,r,o,l,d){e=a(e,a(a(n(i,s,r),o),d));return a(t(e,l),i)}function c(e,i,s,n,o,l,d){e=a(e,a(a(r(i,s,n),o),d));return a(t(e,l),i)}function p(e){var t;var a=e.length;var i=a+8;var s=(i-i%64)/64;var n=(s+1)*16;var r=Array(n-1);var o=0;var l=0;while(l>>29;return r}function u(e){var t="",a="",i,s;for(s=0;s<=3;s++){i=e>>>s*8&255;a="0"+i.toString(16);t=t+a.substr(a.length-2,2)}return t}function f(e){e=e.replace(/\r\n/g,"\n");var t="";for(var a=0;a127&&i<2048){t+=String.fromCharCode(i>>6|192);t+=String.fromCharCode(i&63|128)}else{t+=String.fromCharCode(i>>12|224);t+=String.fromCharCode(i>>6&63|128);t+=String.fromCharCode(i&63|128)}}return t}var h=Array();var m,v,g,b,y,x,w,$,k;var _=7,U=12,I=17,C=22;var S=5,T=9,M=14,P=20;var A=4,O=11,E=16,N=23;var j=6,L=10,R=15,D=21;e=f(e);h=p(e);x=1732584193;w=4023233417;$=2562383102;k=271733878;for(m=0;mt){return a*1}if(e .menu"),main:$("main"),header:$("header"),connection:{status:$("#connection"),user_and_host:$("#user_and_host"),msg:$("#message")},context_menu:[]};UI.buildMenu();UI.stored.getOpts();document.body.setAttribute("data-browser",function(){var e=window.navigator.userAgent;if(e.indexOf("MSIE ")>=0||e.indexOf("Trident/")>=0){return"ie"}if(e.indexOf("Edge/")>=0){return"edge"}if(e.indexOf("Opera")>=0||e.indexOf("OPR")>=0){return"opera"}if(e.indexOf("Chrome")>=0){return"chrome"}if(e.indexOf("Safari")>=0){return"safari"}if(e.indexOf("Firefox")>=0){return"firefox"}return false}());$("body").on("keydown",function(e){switch(e.key){case"Escape":{for(let e of UI.elements.context_menu){e.hide()}break}}});UI.elements.main.click(function(e){if(!e.isDefaultPrevented()){for(let e of UI.elements.context_menu){e.hide()}}});var e={timeout:false,delay:1500};UI.elements.main.on("mousedown",function(t){var a=t.target;if(a.tagName=="SELECT")return;e.timeout=setTimeout(function(){e.timeout=false;var i=new Event("contextmenu",{bubbles:true});i.pageX=t.pageX;i.pageY=t.pageY;a.dispatchEvent(i);function s(e){e.preventDefault()}function n(){window.removeEventListener("click",s,true);document.removeEventListener("mouseup",n)}window.addEventListener("click",s,true);document.addEventListener("mouseup",function(e){requestAnimationFrame(n)})},e.delay)});UI.elements.main.on("mouseleave",function(t){if(e.timeout){clearTimeout(e.timeout);e.timeout=false}});UI.elements.main.on("mouseup",function(t){if(e.timeout){clearTimeout(e.timeout);e.timeout=false}});try{if("mistLogin"in sessionStorage){var t=JSON.parse(sessionStorage["mistLogin"]);mist.user.name=t.name;mist.user.password=t.password;mist.user.host=t.host}}catch(e){}if(location.hash){var a=decodeURIComponent(location.hash).substring(1).split("@");var i=a[0].split("&");mist.user.name=i[0];if(i[1]){mist.user.host=i[1]}}mist.send(function(e){$(window).trigger("hashchange")},{},{timeout:5,hide:true});var s=0;$("body > div.filler").on("scroll",function(){var e=$(this).scrollLeft();if(e!=s){UI.elements.header.css("margin-right",-1*e+"px")}s=e})});var lastpage=[];$(window).on("hashchange",function(e){var t=decodeURIComponent(location.hash).substring(1).split("@");if(!t[1]){t[1]=""}var a=t[1].split("&");if(a[0]==""){a[0]="Overview"}UI.showTab(a[0],a[1],lastpage);if(lastpage[0]!=a[0]||lastpage[1]!=a[1])lastpage=[a[0],a[1]]});var MistVideoObject={};var otherhost={host:false,https:false};var UI={debug:false,elements:{},stored:{getOpts:function(){var e=localStorage["stored"];if(e){e=JSON.parse(e)}$.extend(true,this.vars,e);return this.vars},saveOpt:function(e,t){this.vars[e]=t;localStorage["stored"]=JSON.stringify(this.vars);return this.vars},vars:{helpme:true}},interval:{list:{},clear:function(){for(var e in this.list){clearInterval(this.list[e].id)}this.list={}},set:function(e,t){if(this.opts){log("[interval]","Set called on interval, but an interval is already active.")}var a={delay:t,callback:e,id:setInterval(e,t)};this.list[a.id]=a;return a.id}},websockets:{list:[],clear:function(){for(var e in this.list){this.list[e].close()}},create:function(e,t){var a=new WebSocket(e);var i=this;this.list.push(a);a.addEventListener("close",function(){for(var e=i.list.length-1;e>=0;e--){if(i.list[e]==a){i.list.splice(e,1)}}});a.addEventListener("error",t);return a}},countrylist:{AF:"Afghanistan",AX:"Åland Islands",AL:"Albania",DZ:"Algeria",AS:"American Samoa",AD:"Andorra",AO:"Angola",AI:"Anguilla",AQ:"Antarctica",AG:"Antigua and Barbuda",AR:"Argentina",AM:"Armenia",AW:"Aruba",AU:"Australia",AT:"Austria",AZ:"Azerbaijan",BS:"Bahamas",BH:"Bahrain",BD:"Bangladesh",BB:"Barbados",BY:"Belarus",BE:"Belgium",BZ:"Belize",BJ:"Benin",BM:"Bermuda",BT:"Bhutan",BO:"Bolivia, Plurinational State of",BQ:"Bonaire, Sint Eustatius and Saba",BA:"Bosnia and Herzegovina",BW:"Botswana",BV:"Bouvet Island",BR:"Brazil",IO:"British Indian Ocean Territory",BN:"Brunei Darussalam",BG:"Bulgaria",BF:"Burkina Faso",BI:"Burundi",KH:"Cambodia",CM:"Cameroon",CA:"Canada",CV:"Cape Verde",KY:"Cayman Islands",CF:"Central African Republic",TD:"Chad",CL:"Chile",CN:"China",CX:"Christmas Island",CC:"Cocos (Keeling) Islands",CO:"Colombia",KM:"Comoros",CG:"Congo",CD:"Congo, the Democratic Republic of the",CK:"Cook Islands",CR:"Costa Rica",CI:"Côte d'Ivoire",HR:"Croatia",CU:"Cuba",CW:"Curaçao",CY:"Cyprus",CZ:"Czech Republic",DK:"Denmark",DJ:"Djibouti",DM:"Dominica",DO:"Dominican Republic",EC:"Ecuador",EG:"Egypt",SV:"El Salvador",GQ:"Equatorial Guinea",ER:"Eritrea",EE:"Estonia",ET:"Ethiopia",FK:"Falkland Islands (Malvinas)",FO:"Faroe Islands",FJ:"Fiji",FI:"Finland",FR:"France",GF:"French Guiana",PF:"French Polynesia",TF:"French Southern Territories",GA:"Gabon",GM:"Gambia",GE:"Georgia",DE:"Germany",GH:"Ghana",GI:"Gibraltar",GR:"Greece",GL:"Greenland",GD:"Grenada",GP:"Guadeloupe",GU:"Guam",GT:"Guatemala",GG:"Guernsey",GN:"Guinea",GW:"Guinea-Bissau",GY:"Guyana",HT:"Haiti",HM:"Heard Island and McDonald Islands",VA:"Holy See (Vatican City State)",HN:"Honduras",HK:"Hong Kong",HU:"Hungary",IS:"Iceland",IN:"India",ID:"Indonesia",IR:"Iran, Islamic Republic of",IQ:"Iraq",IE:"Ireland",IM:"Isle of Man",IL:"Israel",IT:"Italy",JM:"Jamaica",JP:"Japan",JE:"Jersey",JO:"Jordan",KZ:"Kazakhstan",KE:"Kenya",KI:"Kiribati",KP:"Korea, Democratic People's Republic of",KR:"Korea, Republic of",KW:"Kuwait",KG:"Kyrgyzstan",LA:"Lao People's Democratic Republic",LV:"Latvia",LB:"Lebanon",LS:"Lesotho",LR:"Liberia",LY:"Libya",LI:"Liechtenstein",LT:"Lithuania",LU:"Luxembourg",MO:"Macao",MK:"Macedonia, the former Yugoslav Republic of",MG:"Madagascar",MW:"Malawi",MY:"Malaysia",MV:"Maldives",ML:"Mali",MT:"Malta",MH:"Marshall Islands",MQ:"Martinique",MR:"Mauritania",MU:"Mauritius",YT:"Mayotte",MX:"Mexico",FM:"Micronesia, Federated States of",MD:"Moldova, Republic of",MC:"Monaco",MN:"Mongolia",ME:"Montenegro",MS:"Montserrat",MA:"Morocco",MZ:"Mozambique",MM:"Myanmar",NA:"Namibia",NR:"Nauru",NP:"Nepal",NL:"Netherlands",NC:"New Caledonia",NZ:"New Zealand",NI:"Nicaragua",NE:"Niger",NG:"Nigeria",NU:"Niue",NF:"Norfolk Island",MP:"Northern Mariana Islands",NO:"Norway",OM:"Oman",PK:"Pakistan",PW:"Palau",PS:"Palestine, State of",PA:"Panama",PG:"Papua New Guinea",PY:"Paraguay",PE:"Peru",PH:"Philippines",PN:"Pitcairn",PL:"Poland",PT:"Portugal",PR:"Puerto Rico",QA:"Qatar",RE:"Réunion",RO:"Romania",RU:"Russian Federation",RW:"Rwanda",BL:"Saint Barthélemy",SH:"Saint Helena, Ascension and Tristan da Cunha",KN:"Saint Kitts and Nevis",LC:"Saint Lucia",MF:"Saint Martin (French part)",PM:"Saint Pierre and Miquelon",VC:"Saint Vincent and the Grenadines",WS:"Samoa",SM:"San Marino",ST:"Sao Tome and Principe",SA:"Saudi Arabia",SN:"Senegal",RS:"Serbia",SC:"Seychelles",SL:"Sierra Leone",SG:"Singapore",SX:"Sint Maarten (Dutch part)",SK:"Slovakia",SI:"Slovenia",SB:"Solomon Islands",SO:"Somalia",ZA:"South Africa",GS:"South Georgia and the South Sandwich Islands",SS:"South Sudan",ES:"Spain",LK:"Sri Lanka",SD:"Sudan",SR:"Suriname",SJ:"Svalbard and Jan Mayen",SZ:"Swaziland",SE:"Sweden",CH:"Switzerland",SY:"Syrian Arab Republic",TW:"Taiwan, Province of China",TJ:"Tajikistan",TZ:"Tanzania, United Republic of",TH:"Thailand",TL:"Timor-Leste",TG:"Togo",TK:"Tokelau",TO:"Tonga",TT:"Trinidad and Tobago",TN:"Tunisia",TR:"Turkey",TM:"Turkmenistan",TC:"Turks and Caicos Islands",TV:"Tuvalu",UG:"Uganda",UA:"Ukraine",AE:"United Arab Emirates",GB:"United Kingdom",US:"United States",UM:"United States Minor Outlying Islands",UY:"Uruguay",UZ:"Uzbekistan",VU:"Vanuatu",VE:"Venezuela, Bolivarian Republic of",VN:"Viet Nam",VG:"Virgin Islands, British",VI:"Virgin Islands, U.S.",WF:"Wallis and Futuna",EH:"Western Sahara",YE:"Yemen",ZM:"Zambia",ZW:"Zimbabwe"},tooltip:{show:function(e,t){$tooltip=this.element;if(!$.contains(document.body,$tooltip[0])){$("body").append($tooltip)}$tooltip.html(t);clearTimeout(this.hiding);delete this.hiding;var a=$(document).height()-$tooltip.outerHeight();var i=$(document).width()-$tooltip.outerWidth();$tooltip.css("left",Math.min(e.pageX+10,i-10));$tooltip.css("top",Math.min(e.pageY+25,a-10));$tooltip.show().addClass("show")},hide:function(){$tooltip=this.element;$tooltip.removeClass("show");this.hiding=setTimeout(function(){$tooltip.hide()},500)},element:$("
").attr("id","tooltip")},context_menu:function(){var e=$("
").attr("class","context_menu").click(function(e){e.stopPropagation()});e[0].style.display="none";this.ele=e;UI.elements.context_menu.push(this);this.pos=function(t){var a=e.offsetParent();var i=a[0].getBoundingClientRect();var s=a.height()-e.outerHeight();var n=a.width()-e.outerWidth();e.css("left",Math.min(t.pageX-i.x+a.scrollLeft(),n));e.css("top",Math.min(t.pageY-i.y+a.scrollTop(),s))};this.show=function(t,a){if(typeof t=="string"||t instanceof jQuery){e.html(t)}else if(typeof t=="object"){e.html("");if(!Array.isArray(t)){t=[t]}for(var i in t){var s=t[i];if(s instanceof jQuery){e.children().last().remove();e.append(s);e.append($("
"));continue}for(var n in s){var r=s[n];var o=$("
");if(typeof r=="string"){o.text(r)}else if(r instanceof jQuery){e.append(r);continue}else{function l(t){if(!t)return;if(Array.isArray(t)){var a={};a.text=t[0];if(t.length>=2&&typeof t[1]=="function"){a["function"]=t[1]}if(t.length>=3){a.icon=t[2];if(t.length>=4){a.title=t[3]}}t=a}if("function"in t){o.click(function(a){var i=t["function"].apply(this,arguments);if(i!==false){e.hide()}});o.on("keydown",function(e){switch(e.key){case"Enter":{$(this).click();break}}});o.attr("tabindex","0")}if("icon"in t){o.append($("
").addClass("icon").attr("data-icon",t.icon))}if("title"in t){o.attr("title",t.title)}if(typeof t.text=="string"){o[0]._text=document.createTextNode(t.text);o.append(o[0]._text);o[0]._setText=function(e){this._text.nodeValue=e}}else{o.append(t.text);o[0]._setText=function(e){$(this).html(e)}}}l(r)}e.append(o)}e.append($("
"))}e.children().last().remove();e.find("[tabindex]").first().focus()}if(!e.parent()){$("body").append(e)}e[0].style.display="";if(a){this.pos(a)}e.find("[tabindex]").first().focus()};this.hide=function(){e[0].style.display="none"};this.remove=function(){delete UI.element.context_menu;e.remove()};e.on("keydown",function(t){function a(t){var a=e.find(":focus");if(!a.length){e.find("[tabindex]").first().focus();return}if(t=="down"){var i=a.nextAll("[tabindex]");if(!i.length){e.find("[tabindex]").first().focus()}else{i.first().focus()}}else{var s=a.prevAll("[tabindex]");if(!s.length){e.find("[tabindex]").last().focus()}else{s.first().focus()}}}switch(t.key){case"ArrowDown":{a("down");break}case"ArrowUp":{a("up");break}}});this.hide()},pagecontrol:function(e,t){var a=$("
").addClass("page_control");a.elements={prev:$("