import { _get, _post, toggleLoader, notificationBox } from "./modules/common.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; interface sWindow extends Window { messages: {}; } declare var window: sWindow; window.URLBase = ""; window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); const get = (id: string): HTMLElement => document.getElementById(id); const text = (id: string, val: string) => { document.getElementById(id).textContent = val; }; const html = (id: string, val: string) => { document.getElementById(id).innerHTML = val; }; interface boolEvent extends Event { detail: boolean; } class Input { private _el: HTMLInputElement; get value(): string { return ""+this._el.value; } set value(v: string) { this._el.value = v; } // Nothing depends on input, but we add an empty broadcast function so we can just loop over all settings to fix dependents on start. broadcast = () => {} constructor(el: HTMLElement, placeholder?: any, value?: any, depends?: string, dependsTrue?: boolean, section?: string) { this._el = el as HTMLInputElement; if (placeholder) { this._el.placeholder = placeholder; } if (value) { this.value = value; } if (depends) { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { let el = this._el as HTMLElement; if (el.parentElement.tagName == "LABEL") { el = el.parentElement; } if (event.detail !== dependsTrue) { el.classList.add("unfocused"); } else { el.classList.remove("unfocused"); } }); } } } class Checkbox { private _el: HTMLInputElement; private _hideEl: HTMLElement; get value(): string { return this._el.checked ? "true" : "false"; } set value(v: string) { this._el.checked = (v == "true") ? true : false; } private _section: string; private _setting: string; broadcast = () => { let state = this._el.checked; if (this._hideEl.classList.contains("unfocused")) { state = false; } if (this._section && this._setting) { const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": state }) document.dispatchEvent(ev); } } set onchange(f: () => void) { this._el.addEventListener("change", f); } constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { this._el = el as HTMLInputElement; this._hideEl = this._el as HTMLElement; if (this._hideEl.parentElement.tagName == "LABEL") { this._hideEl = this._hideEl.parentElement; } if (section && setting) { this._section = section; this._setting = setting; this._el.onchange = this.broadcast; } if (depends) { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { if (event.detail !== dependsTrue) { this._hideEl.classList.add("unfocused"); this.broadcast(); } else { this._hideEl.classList.remove("unfocused"); this.broadcast(); } }); } if (this._el.hasAttribute("checked")) { this._el.checked = true; } else { this._el.checked = false; } this.broadcast(); } } class BoolRadios { private _els: NodeListOf; get value(): string { return this._els[0].checked ? "true" : "false" } set value(v: string) { const bool = (v == "true") ? true : false; this._els[0].checked = bool; this._els[1].checked = !bool; } private _section: string; private _setting: string; broadcast = () => { if (this._section && this._setting) { const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": this._els[0].checked }) document.dispatchEvent(ev); } } constructor(name: string, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { this._els = document.getElementsByName(name) as NodeListOf; if (section && setting) { this._section = section; this._setting = setting; this._els[0].onchange = this.broadcast; this._els[1].onchange = this.broadcast; } if (depends) { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { if (event.detail !== dependsTrue) { if (this._els[0].parentElement.tagName == "LABEL") { this._els[0].parentElement.classList.add("unfocused"); } if (this._els[1].parentElement.tagName == "LABEL") { this._els[1].parentElement.classList.add("unfocused"); } } else { if (this._els[0].parentElement.tagName == "LABEL") { this._els[0].parentElement.classList.remove("unfocused"); } if (this._els[1].parentElement.tagName == "LABEL") { this._els[1].parentElement.classList.remove("unfocused"); } } }); } } } // class Radios { // private _el: HTMLInputElement; // get value(): string { return this._el.value; } // set value(v: string) { this._el.value = v; } // constructor(name: string, depends?: string, dependsTrue?: boolean, section?: string) { // this._el = document.getElementsByName(name)[0] as HTMLInputElement; // if (depends) { // document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { // let el = this._el as HTMLElement; // if (el.parentElement.tagName == "LABEL") { el = el.parentElement; } // if (event.detail !== dependsTrue) { // el.classList.add("unfocused"); // } else { // el.classList.remove("unfocused"); // } // }); // } // } // } class Select { private _el: HTMLSelectElement; get value(): string { return this._el.value; } set value(v: string) { this._el.value = v; } add = (val: string, label: string) => { const item = document.createElement("option") as HTMLOptionElement; item.value = val; item.textContent = label; this._el.appendChild(item); } set onchange(f: () => void) { this._el.addEventListener("change", f); } private _section: string; private _setting: string; broadcast = () => { if (this._section && this._setting) { const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": this.value ? true : false }) document.dispatchEvent(ev); } } constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { this._el = el as HTMLSelectElement; if (section && setting) { this._section = section; this._setting = setting; this._el.addEventListener("change", this.broadcast); } if (depends) { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { let el = this._el as HTMLElement; while (el.tagName != "LABEL") { el = el.parentElement; } if (event.detail !== dependsTrue) { el.classList.add("unfocused"); } else { el.classList.remove("unfocused"); } }); } } } class LangSelect extends Select { constructor(page: string, el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { super(el, depends, dependsTrue, section, setting); _get("/lang/" + page, null, (req: XMLHttpRequest) => { if (req.readyState == 4 && req.status == 200) { for (let code in req.response) { this.add(code, req.response[code]); } this.value = "en-us"; } }); } } const replaceLink = (elName: string, sect: string, name: string, url: string, text: string) => html(elName, window.lang.var(sect, name, `${text}`)); window.lang = new lang(window.langFile as LangFile); replaceLink("language-description", "language", "description", "https://weblate.jfa-go.com", "Weblate"); replaceLink("email-description", "email", "description", "https://mailgun.com", "Mailgun"); replaceLink("email-dateformat-notice", "email", "dateFormatNotice", "https://strftime.timpetricola.com/", "strftime.timpetricola.com"); replaceLink("updates-description", "updates", "description", "https://builds.hrfee.dev/view/hrfee/jfa-go", "buildrone"); replaceLink("messages-description", "messages", "description", "https://wiki.jfa-go.com", "Wiki"); replaceLink("password_resets-more-info", "passwordResets", "moreInfo", "https://wiki.jfa-go.com/docs/pwr/", "wiki.jfa-go.com"); replaceLink("ombi-stability-warning", "ombi", "stabilityWarning", "https://wiki.jfa-go.com/docs/ombi/", "wiki.jfa-go.com"); const settings = { "jellyfin": { "type": new Select(get("jellyfin-type")), "server": new Input(get("jellyfin-server")), "public_server": new Input(get("jellyfin-public_server")), "username": new Input(get("jellyfin-username")), "password": new Input(get("jellyfin-password")), "substitute_jellyfin_strings": new Input(get("jellyfin-substitute_jellyfin_strings")) }, "updates": { "enabled": new Checkbox(get("updates-enabled"), "", false, "updates", "enabled"), "channel": new Select(get("updates-channel"), "enabled", true, "updates") }, "ui": { "host": new Input(get("ui-host")), "port": new Input(get("ui-port")), "url_base": new Input(get("ui-url_base")), "theme": new Select(get("ui-theme")), "language-form": new LangSelect("form", get("ui-language-form")), "language-admin": new LangSelect("admin", get("ui-language-admin")), "jellyfin_login": new BoolRadios("ui-jellyfin_login", "", false, "ui", "jellyfin_login"), "admin_only": new Checkbox(get("ui-admin_only"), "jellyfin_login", true, "ui"), "allow_all": new Checkbox(get("ui-allow_all"), "jellyfin_login", true, "ui"), "username": new Input(get("ui-username"), "", "", "jellyfin_login", false, "ui"), "password": new Input(get("ui-password"), "", "", "jellyfin_login", false, "ui"), "email": new Input(get("ui-email"), "", "", "jellyfin_login", false, "ui"), "contact_message": new Input(get("ui-contact_message"), window.messages["ui"]["contact_message"]), "help_message": new Input(get("ui-help_message"), window.messages["ui"]["help_message"]), "success_message": new Input(get("ui-success_message"), window.messages["ui"]["success_message"]) }, "password_validation": { "enabled": new Checkbox(get("password_validation-enabled"), "", false, "password_validation", "enabled"), "min_length": new Input(get("password_validation-min_length"), "", 8, "enabled", true, "password_validation"), "upper": new Input(get("password_validation-upper"), "", 1, "enabled", true, "password_validation"), "lower": new Input(get("password_validation-lower"), "", 0, "enabled", true, "password_validation"), "number": new Input(get("password_validation-number"), "", 1, "enabled", true, "password_validation"), "special": new Input(get("password_validation-special"), "", 0, "enabled", true, "password_validation") }, "messages": { "enabled": new Checkbox(get("messages-enabled"), "", false, "messages", "enabled"), "use_24h": new BoolRadios("email-24h", "enabled", true, "messages"), "date_format": new Input(get("email-date_format"), "", "%d/%m/%y", "enabled", true, "messages"), "message": new Input(get("email-message"), window.messages["messages"]["message"], "", "enabled", true, "messages") }, "email": { "language": new LangSelect("email", get("email-language")), "no_username": new Checkbox(get("email-no_username"), "method", true, "email"), "method": new Select(get("email-method"), "", false, "email", "method"), "address": new Input(get("email-address"), "jellyfin@jellyf.in", "", "method", true, "email"), "from": new Input(get("email-from"), "", "Jellyfin", "method", true, "email") }, "password_resets": { "enabled": new Checkbox(get("password_resets-enabled"), "", false, "password_resets", "enabled"), "watch_directory": new Input(get("password_resets-watch_directory"), "", "", "enabled", true, "password_resets"), "subject": new Input(get("password_resets-subject"), "", "", "enabled", true, "password_resets"), "link_reset": new Checkbox(get("password_resets-link_reset"), "enabled", true, "password_resets", "link_reset"), "language": new LangSelect("pwr", get("password_resets-language"), "link_reset", true, "password_resets", "language"), "set_password": new Checkbox(get("password_resets-set_password"), "link_reset", true, "password_resets", "set_password") }, "notifications": { "enabled": new Checkbox(get("notifications-enabled")) }, "user_page": { "enabled": new Checkbox(get("userpage-enabled")) }, "welcome_email": { "enabled": new Checkbox(get("welcome_email-enabled"), "", false, "welcome_email", "enabled"), "subject": new Input(get("welcome_email-subject"), "", "", "enabled", true, "welcome_email") }, "invite_emails": { "enabled": new Checkbox(get("invite_emails-enabled"), "", false, "invite_emails", "enabled"), "subject": new Input(get("invite_emails-subject"), "", "", "enabled", true, "invite_emails"), "url_base": new Input(get("invite_emails-url_base"), "", "", "enabled", true, "invite_emails") }, "mailgun": { "api_url": new Input(get("mailgun-api_url")), "api_key": new Input(get("mailgun-api_key")) }, "smtp": { "username": new Input(get("smtp-username")), "encryption": new Select(get("smtp-encryption")), "server": new Input(get("smtp-server")), "port": new Input(get("smtp-port")), "password": new Input(get("smtp-password")) }, "ombi": { "enabled": new Checkbox(get("ombi-enabled"), "", false, "ombi", "enabled"), "server": new Input(get("ombi-server"), "", "", "enabled", true, "ombi"), "api_key": new Input(get("ombi-api_key"), "", "", "enabled", true, "ombi") }, "jellyseerr": { "enabled": new Checkbox(get("jellyseerr-enabled"), "", false, "jellyseerr", "enabled"), "server": new Input(get("jellyseerr-server"), "", "", "enabled", true, "jellyseerr"), "api_key": new Input(get("jellyseerr-api_key"), "", "", "enabled", true, "jellyseerr"), "import_existing": new Checkbox(get("jellyseerr-import_existing"), "enabled", true, "jellyseerr", "import_existing") }, "advanced": { "tls": new Checkbox(get("advanced-tls"), "", false, "advanced", "tls"), "tls_port": new Input(get("advanced-tls_port"), "", "", "tls", true, "advanced"), "tls_cert": new Input(get("advanced-tls_cert"), "", "", "tls", true, "advanced"), "tls_key": new Input(get("advanced-tls_key"), "", "", "tls", true, "advanced"), "proxy": new Checkbox(get("advanced-proxy"), "", false, "advanced", "proxy"), "proxy_protocol": new Select(get("advanced-proxy_protocol"), "proxy", true, "advanced"), "proxy_address": new Input(get("advanced-proxy_address"), "", "", "proxy", true, "advanced"), "proxy_user": new Input(get("advanced-proxy_user"), "", "", "proxy", true, "advanced"), "proxy_password": new Input(get("advanced-proxy_password"), "", "", "proxy", true, "advanced") } }; const checkTheme = () => { if (settings["ui"]["theme"].value.includes("Dark")) { document.documentElement.classList.add("dark-theme"); document.documentElement.classList.remove("light-theme"); } else { document.documentElement.classList.add("light-theme"); document.documentElement.classList.remove("dark-theme"); } }; settings["ui"]["theme"].onchange = checkTheme; checkTheme(); const restartButton = document.getElementById("restart") as HTMLSpanElement; const serialize = () => { toggleLoader(restartButton); let config = {}; for (let section in settings) { config[section] = {}; for (let setting in settings[section]) { if (settings[section][setting].value) { config[section][setting] = settings[section][setting].value; } } } config["restart-program"] = true; _post("/config", config, (req: XMLHttpRequest) => { if (req.readyState == 4) { toggleLoader(restartButton); if (req.status == 500) { if (req.response == null) { const old = restartButton.textContent; restartButton.classList.add("~critical"); restartButton.classList.remove("~urge"); restartButton.textContent = window.lang.strings("errorUnknown"); setTimeout(() => { restartButton.classList.add("~urge"); restartButton.classList.remove("~critical"); restartButton.textContent = old; }, 5000); return; } if (req.response["error"] as string) { const old = restartButton.textContent; restartButton.classList.add("~critical"); restartButton.classList.remove("~urge"); restartButton.textContent = req.response["error"]; setTimeout(() => { restartButton.classList.add("~urge"); restartButton.classList.remove("~critical"); restartButton.textContent = old; }, 5000); return; } } restartButton.parentElement.querySelector("span.back").classList.add("unfocused"); restartButton.classList.add("unfocused"); const refresh = document.getElementById("refresh") as HTMLSpanElement; refresh.classList.remove("unfocused"); refresh.onclick = () => { let host = window.location.href.split("#")[0].split("?")[0] + settings["ui"]["url_base"].value; window.location.href = host; }; } }, true, (req: XMLHttpRequest) => { if (req.status == 0) { window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused")); } }); } restartButton.onclick = serialize; const relatedToEmail = Array.from(document.getElementsByClassName("related-to-email")); const emailMethodChange = () => { const val = settings["email"]["method"].value; const smtp = document.getElementById("email-smtp"); const mailgun = document.getElementById("email-mailgun"); const emailSect = document.getElementById("email-sect"); const enabled = settings["messages"]["enabled"].value; if (enabled == "false") { for (let el of relatedToEmail) { el.classList.add("hidden"); } emailSect.classList.add("unfocused"); return; } else { for (let el of relatedToEmail) { el.classList.remove("hidden"); } emailSect.classList.remove("unfocused"); } if (val == "smtp") { smtp.classList.remove("unfocused"); mailgun.classList.add("unfocused"); } else if (val == "mailgun") { mailgun.classList.remove("unfocused"); smtp.classList.add("unfocused"); for (let el of relatedToEmail) { el.classList.remove("hidden"); } } else { mailgun.classList.add("unfocused"); smtp.classList.add("unfocused"); } }; settings["email"]["method"].onchange = emailMethodChange; settings["messages"]["enabled"].onchange = emailMethodChange; emailMethodChange(); const jellyfinLoginAccessChange = () => { const adminOnly = settings["ui"]["admin_only"].value == "true"; const allowAll = settings["ui"]["allow_all"].value == "true"; const adminOnlyEl = document.getElementById("ui-admin_only") as HTMLInputElement; const allowAllEls = [document.getElementById("ui-allow_all"), document.getElementById("description-ui-allow_all")]; const nextButton = adminOnlyEl.parentElement.parentElement.parentElement.querySelector("span.next") as HTMLSpanElement; if (adminOnly && !allowAll) { (allowAllEls[0] as HTMLInputElement).disabled = true; adminOnlyEl.disabled = false; nextButton.removeAttribute("disabled"); } else if (!adminOnly && allowAll) { adminOnlyEl.disabled = true; (allowAllEls[0] as HTMLInputElement).disabled = false; nextButton.removeAttribute("disabled"); } else { adminOnlyEl.disabled = false; (allowAllEls[0] as HTMLInputElement).disabled = false; nextButton.setAttribute("disabled", "true") } }; settings["ui"]["admin_only"].onchange = jellyfinLoginAccessChange; settings["ui"]["allow_all"].onchange = jellyfinLoginAccessChange; jellyfinLoginAccessChange(); const embyHidePWR = () => { const pwr = document.getElementById("password-resets"); const val = settings["jellyfin"]["type"].value; if (val == "jellyfin") { pwr.classList.remove("hidden"); } else if (val == "emby") { pwr.classList.add("hidden"); } } settings["jellyfin"]["type"].onchange = embyHidePWR; embyHidePWR(); (window as any).settings = settings; for (let section in settings) { for (let setting in settings[section]) { settings[section][setting].broadcast(); } } const pageNames: string[][] = []; window.history.replaceState("welcome", "Setup - jfa-go"); const changePage = (title: string, pageTitle: string) => { const urlParams = new URLSearchParams(window.location.search); const lang = urlParams.get("lang"); let page = "/#" + title; if (lang) { page += "?lang=" + lang; } window.history.pushState(title || "welcome", pageTitle, page); }; const cards = Array.from(document.getElementById("page-container").getElementsByClassName("card")) as Array; (window as any).cards = cards; window.onpopstate = (event: PopStateEvent) => { if (event.state === "welcome") { cards[0].classList.remove("unfocused"); for (let i = 1; i < cards.length; i++) { cards[i].classList.add("unfocused"); } return; } for (let i = 0; i < cards.length; i++) { if (event.state === pageNames[i][0]) { cards[i].classList.remove("unfocused"); } else { cards[i].classList.add("unfocused"); } } }; (() => { for (let i = 0; i < cards.length; i++) { const card = cards[i]; const back = card.getElementsByClassName("back")[0] as HTMLSpanElement; const next = card.getElementsByClassName("next")[0] as HTMLSpanElement; const titleEl = cards[i].querySelector("span.heading") as HTMLElement; let title = titleEl.textContent.replace("/", "_").replace(" ", "-"); if (titleEl.classList.contains("welcome")) { title = ""; } let pageTitle = titleEl.textContent + " - jfa-go"; pageNames.push([title, pageTitle]); if (back) { back.addEventListener("click", () => { let found = false; for (let ind = cards.length - 1; ind >= 0; ind--) { cards[ind].classList.add("unfocused"); if (ind < i && !(cards[ind].classList.contains("hidden")) && !found) { cards[ind].classList.remove("unfocused"); changePage(pageNames[ind][0], pageNames[ind][1]); found = true; } } window.scrollTo(0, 0); }); } if (next) { const func = () => { if (next.hasAttribute("disabled")) return; let found = false; for (let ind = 0; ind < cards.length; ind++) { cards[ind].classList.add("unfocused"); if (ind > i && !(cards[ind].classList.contains("hidden")) && !found) { cards[ind].classList.remove("unfocused"); changePage(pageNames[ind][0], pageNames[ind][1]); found = true; } } window.scrollTo(0, 0); }; next.addEventListener("click", func) } } })(); (() => { const button = document.getElementById("jellyfin-test-connection") as HTMLSpanElement; const ogText = button.textContent; const nextButton = button.parentElement.querySelector("span.next") as HTMLSpanElement; button.onclick = () => { toggleLoader(button); let send = { "type": settings["jellyfin"]["type"].value, "server": settings["jellyfin"]["server"].value, "username": settings["jellyfin"]["username"].value, "password": settings["jellyfin"]["password"].value, "proxy": settings["advanced"]["proxy"].value == "true", "proxy_protocol": settings["advanced"]["proxy_protocol"].value, "proxy_address": settings["advanced"]["proxy_address"].value, "proxy_user": settings["advanced"]["proxy_user"].value, "proxy_password": settings["advanced"]["proxy_password"].value }; _post("/jellyfin/test", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { toggleLoader(button); if (req.status != 200) { nextButton.setAttribute("disabled", ""); button.classList.add("~critical"); button.classList.remove("~urge"); setTimeout(() => { button.textContent = ogText; button.classList.add("~urge"); button.classList.remove("~critical"); }, 5000); const errorMsg = req.response["error"] as string; if (!errorMsg) { button.textContent = window.lang.strings("error"); } else { button.textContent = window.lang.strings(errorMsg); } return; } nextButton.removeAttribute("disabled"); button.textContent = window.lang.strings("success"); button.classList.add("~positive"); button.classList.remove("~urge"); setTimeout(() => { button.textContent = ogText; button.classList.add("~urge"); button.classList.remove("~positive"); }, 5000); } }, true, (req: XMLHttpRequest) => { if (req.status == 0) { window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused")); } }); }; })(); loadLangSelector("setup");