From 817107622a8fe6f2fdaf198da4b2632854aa9bac Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 8 Dec 2025 20:38:30 +0000 Subject: [PATCH] ts: format finally formatted with biome, a config file is provided. --- biome.json | 9 + ts/admin.ts | 100 +-- ts/crash.ts | 2 +- ts/form.ts | 159 ++-- ts/modules/account-linking.ts | 140 ++-- ts/modules/accounts.ts | 1387 +++++++++++++++++++-------------- ts/modules/activity.ts | 482 +++++++----- ts/modules/captcha.ts | 94 ++- ts/modules/common.ts | 235 ++++-- ts/modules/discord.ts | 13 +- ts/modules/invites.ts | 627 +++++++++------ ts/modules/lang.ts | 71 +- ts/modules/list.ts | 201 +++-- ts/modules/login.ts | 52 +- ts/modules/modal.ts | 34 +- ts/modules/pages.ts | 30 +- ts/modules/profiles.ts | 458 ++++++----- ts/modules/search.ts | 188 +++-- ts/modules/settings.ts | 929 +++++++++++++--------- ts/modules/stripmd.ts | 50 +- ts/modules/tabs.ts | 43 +- ts/modules/theme.ts | 51 +- ts/modules/ui.ts | 41 +- ts/modules/update.ts | 117 ++- ts/modules/validator.ts | 87 ++- ts/pwr.ts | 61 +- ts/setup.ts | 564 ++++++++------ ts/typings/d.ts | 52 +- ts/user.ts | 289 ++++--- 29 files changed, 3956 insertions(+), 2610 deletions(-) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..762e42b --- /dev/null +++ b/biome.json @@ -0,0 +1,9 @@ +{ + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "formatWithErrors": false, + "lineWidth": 120 + } +} diff --git a/ts/admin.ts b/ts/admin.ts index dbb8083..22a65fd 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -20,7 +20,7 @@ loadLangSelector("admin"); // _get(`/lang/admin/${window.language}.json`, null, (req: XMLHttpRequest) => { // if (req.readyState == 4 && req.status == 200) { // langLoaded = true; -// window.lang = new lang(req.response as LangFile); +// window.lang = new lang(req.response as LangFile); // } // }); @@ -34,35 +34,37 @@ window.availableProfiles = window.availableProfiles || []; (() => { window.modals = {} as Modals; - window.modals.login = new Modal(document.getElementById('modal-login'), true); + window.modals.login = new Modal(document.getElementById("modal-login"), true); - window.modals.addUser = new Modal(document.getElementById('modal-add-user')); + window.modals.addUser = new Modal(document.getElementById("modal-add-user")); - window.modals.about = new Modal(document.getElementById('modal-about')); - (document.getElementById('setting-about') as HTMLSpanElement).onclick = window.modals.about.toggle; + window.modals.about = new Modal(document.getElementById("modal-about")); + (document.getElementById("setting-about") as HTMLSpanElement).onclick = window.modals.about.toggle; - window.modals.modifyUser = new Modal(document.getElementById('modal-modify-user')); + window.modals.modifyUser = new Modal(document.getElementById("modal-modify-user")); - window.modals.deleteUser = new Modal(document.getElementById('modal-delete-user')); + window.modals.deleteUser = new Modal(document.getElementById("modal-delete-user")); - window.modals.settingsRestart = new Modal(document.getElementById('modal-restart')); + window.modals.settingsRestart = new Modal(document.getElementById("modal-restart")); - window.modals.settingsRefresh = new Modal(document.getElementById('modal-refresh')); + window.modals.settingsRefresh = new Modal(document.getElementById("modal-refresh")); - window.modals.ombiProfile = new Modal(document.getElementById('modal-ombi-profile')); - document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiProfile.close); - - window.modals.jellyseerrProfile = new Modal(document.getElementById('modal-jellyseerr-profile')); - document.getElementById('form-jellyseerr-defaults').addEventListener('submit', window.modals.jellyseerrProfile.close); + window.modals.ombiProfile = new Modal(document.getElementById("modal-ombi-profile")); + document.getElementById("form-ombi-defaults").addEventListener("submit", window.modals.ombiProfile.close); + + window.modals.jellyseerrProfile = new Modal(document.getElementById("modal-jellyseerr-profile")); + document + .getElementById("form-jellyseerr-defaults") + .addEventListener("submit", window.modals.jellyseerrProfile.close); window.modals.profiles = new Modal(document.getElementById("modal-user-profiles")); window.modals.addProfile = new Modal(document.getElementById("modal-add-profile")); - + window.modals.editProfile = new Modal(document.getElementById("modal-edit-profile")); window.modals.announce = new Modal(document.getElementById("modal-announce")); - + window.modals.editor = new Modal(document.getElementById("modal-editor")); window.modals.customizeEmails = new Modal(document.getElementById("modal-customize")); @@ -74,7 +76,7 @@ window.availableProfiles = window.availableProfiles || []; window.modals.matrix = new Modal(document.getElementById("modal-matrix")); window.modals.logs = new Modal(document.getElementById("modal-logs")); - + window.modals.tasks = new Modal(document.getElementById("modal-tasks")); window.modals.backedUp = new Modal(document.getElementById("modal-backed-up")); @@ -111,7 +113,7 @@ var settings = new settingsList(); var profiles = new ProfileEditor(); -window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); +window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement, 5); /*const modifySettingsSource = function () { const profile = document.getElementById('radio-use-profile') as HTMLInputElement; @@ -131,46 +133,47 @@ let isInviteURL = window.invites.isInviteURL(); let isAccountURL = accounts.isAccountURL(); // load tabs -const tabs: { id: string, url: string, reloader: () => void, unloader?: () => void }[] = [ +const tabs: { id: string; url: string; reloader: () => void; unloader?: () => void }[] = [ { id: "invites", url: "", - reloader: () => window.invites.reload(() => { - if (isInviteURL) { - window.invites.loadInviteURL(); - // Don't keep loading the same item on every tab refresh - isInviteURL = false; - } - }), + reloader: () => + window.invites.reload(() => { + if (isInviteURL) { + window.invites.loadInviteURL(); + // Don't keep loading the same item on every tab refresh + isInviteURL = false; + } + }), }, { id: "accounts", url: "accounts", - reloader: () => accounts.reload(() => { - if (isAccountURL) { - accounts.loadAccountURL(); - // Don't keep loading the same item on every tab refresh - isAccountURL = false; - } - accounts.bindPageEvents(); - }), - unloader: accounts.unbindPageEvents - + reloader: () => + accounts.reload(() => { + if (isAccountURL) { + accounts.loadAccountURL(); + // Don't keep loading the same item on every tab refresh + isAccountURL = false; + } + accounts.bindPageEvents(); + }), + unloader: accounts.unbindPageEvents, }, { id: "activity", url: "activity", reloader: () => { - activity.reload() + activity.reload(); activity.bindPageEvents(); }, - unloader: activity.unbindPageEvents + unloader: activity.unbindPageEvents, }, { id: "settings", url: "settings", - reloader: settings.reload - } + reloader: settings.reload, + }, ]; const defaultTab = tabs[0]; @@ -178,10 +181,16 @@ const defaultTab = tabs[0]; window.tabs = new Tabs(); for (let tab of tabs) { - window.tabs.addTab(tab.id, window.pages.Base + window.pages.Admin + "/" + tab.url, null, tab.reloader, tab.unloader || null); + window.tabs.addTab( + tab.id, + window.pages.Base + window.pages.Admin + "/" + tab.url, + null, + tab.reloader, + tab.unloader || null, + ); } -let matchedTab = false +let matchedTab = false; for (const tab of tabs) { if (window.location.pathname.startsWith(window.pages.Base + window.pages.Current + "/" + tab.url)) { window.tabs.switch(tab.url, true); @@ -199,10 +208,13 @@ login.onLogin = () => { window.updater = new Updater(); // FIXME: Decide whether to autoload activity or not reloadProfileNames(); - setInterval(() => { window.invites.reload(); accounts.reloadIfNotInScroll(); }, 30*1000); + setInterval(() => { + window.invites.reload(); + accounts.reloadIfNotInScroll(); + }, 30 * 1000); // Triggers pre and post funcs, even though we're already on that page window.tabs.switch(window.tabs.current); -} +}; bindManualDropdowns(); diff --git a/ts/crash.ts b/ts/crash.ts index 4ae973f..88dd955 100644 --- a/ts/crash.ts +++ b/ts/crash.ts @@ -22,7 +22,7 @@ const buttonChange = (type: string) => { buttonNormal.classList.add("@low"); buttonNormal.classList.remove("@high"); } -} +}; buttonNormal.onclick = () => buttonChange("normal"); buttonSanitized.onclick = () => buttonChange("sanitized"); diff --git a/ts/form.ts b/ts/form.ts index e83efa5..6743a7f 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -50,7 +50,6 @@ window.animationEvent = whichAnimationEvent(); window.successModal = new Modal(document.getElementById("modal-success"), true); - var telegramVerified = false; if (window.telegramEnabled) { window.telegramModal = new Modal(document.getElementById("modal-telegram"), window.telegramRequired); @@ -74,23 +73,25 @@ if (window.telegramEnabled) { checkbox.parentElement.classList.remove("unfocused"); checkbox.checked = true; validator.validate(); - } + }, }; const telegram = new Telegram(telegramConf); - telegramButton.onclick = () => { telegram.onclick(); }; + telegramButton.onclick = () => { + telegram.onclick(); + }; } var discordVerified = false; if (window.discordEnabled) { window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired); const discordButton = document.getElementById("link-discord") as HTMLSpanElement; - + const discordConf: ServiceConfiguration = { modal: window.discordModal as Modal, pin: window.discordPIN, - inviteURL: window.discordInviteLink ? (window.pages.Form + "/" + window.code + "/discord/invite") : "", + inviteURL: window.discordInviteLink ? window.pages.Form + "/" + window.code + "/discord/invite" : "", pinURL: "", verifiedURL: window.pages.Form + "/" + window.code + "/discord/verified/", invalidCodeError: window.messages["errorInvalidPIN"], @@ -103,15 +104,17 @@ if (window.discordEnabled) { document.getElementById("contact-via").classList.remove("unfocused"); document.getElementById("contact-via-email").parentElement.classList.remove("unfocused"); const checkbox = document.getElementById("contact-via-discord") as HTMLInputElement; - checkbox.parentElement.classList.remove("unfocused") + checkbox.parentElement.classList.remove("unfocused"); checkbox.checked = true; validator.validate(); - } + }, }; const discord = new Discord(discordConf); - discordButton.onclick = () => { discord.onclick(); }; + discordButton.onclick = () => { + discord.onclick(); + }; } var matrixVerified = false; @@ -119,7 +122,7 @@ var matrixPIN = ""; if (window.matrixEnabled) { window.matrixModal = new Modal(document.getElementById("modal-matrix"), window.matrixRequired); const matrixButton = document.getElementById("link-matrix") as HTMLSpanElement; - + const matrixConf: MatrixConfiguration = { modal: window.matrixModal as Modal, sendMessageURL: window.pages.Form + "/" + window.code + "/matrix/user", @@ -138,12 +141,14 @@ if (window.matrixEnabled) { checkbox.parentElement.classList.remove("unfocused"); checkbox.checked = true; validator.validate(); - } + }, }; const matrix = new Matrix(matrixConf); - matrixButton.onclick = () => { matrix.show(); }; + matrixButton.onclick = () => { + matrix.show(); + }; } if (window.confirmation) { @@ -154,7 +159,7 @@ declare var window: formWindow; if (window.userExpiryEnabled) { const messageEl = document.getElementById("user-expiry-message") as HTMLElement; const calculateTime = () => { - let time = new Date() + let time = new Date(); time.setMonth(time.getMonth() + window.userExpiryMonths); time.setDate(time.getDate() + window.userExpiryDays); time.setHours(time.getHours() + window.userExpiryHours); @@ -162,7 +167,7 @@ if (window.userExpiryEnabled) { messageEl.textContent = window.userExpiryMessage.replace("{date}", toDateString(time)); setTimeout(calculateTime, 1000); }; - document.addEventListener("timefmt-change", calculateTime) + document.addEventListener("timefmt-change", calculateTime); calculateTime(); } @@ -174,7 +179,8 @@ let usernameField = document.getElementById("create-username") as HTMLInputEleme const emailField = document.getElementById("create-email") as HTMLInputElement; window.emailRequired &&= window.collectEmail; if (!window.usernameEnabled) { - usernameField.parentElement.remove(); usernameField = emailField; + usernameField.parentElement.remove(); + usernameField = emailField; } else if (!window.collectEmail) { emailField.parentElement.classList.add("unfocused"); emailField.value = ""; @@ -229,7 +235,7 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool oncomplete(true); } -let baseValidator = captcha.baseValidatorWrapper(_baseValidator); +let baseValidator = captcha.baseValidatorWrapper(_baseValidator); declare var grecaptcha: GreCAPTCHA; @@ -238,14 +244,14 @@ let validatorConf: ValidatorConf = { rePasswordField: rePasswordField, submitInput: submitInput, submitButton: submitSpan, - validatorFunc: baseValidator + validatorFunc: baseValidator, }; let validator = new Validator(validatorConf); var requirements = validator.requirements; if (window.emailRequired) { - emailField.addEventListener("keyup", validator.validate) + emailField.addEventListener("keyup", validator.validate); } interface sendDTO { @@ -273,7 +279,6 @@ if (window.captcha && !window.reCAPTCHA) { const create = (event: SubmitEvent) => { event.preventDefault(); if (window.captcha && !window.reCAPTCHA && !captcha.verified) { - } addLoader(submitSpan); let send: sendDTO = { @@ -281,8 +286,8 @@ const create = (event: SubmitEvent) => { username: usernameField.value, email: emailField.value, email_contact: true, - password: passwordField.value - } + password: passwordField.value, + }; if (telegramVerified) { send.telegram_pin = window.telegramPIN; const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement; @@ -316,62 +321,74 @@ const create = (event: SubmitEvent) => { send.captcha_text = captcha.input.value; } } - _post("/user/invite", send, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(submitSpan); - let vals = req.response as ValidatorRespDTO; - let valid = true; - for (let type in vals) { - if (requirements[type]) requirements[type].valid = vals[type]; - if (!vals[type]) valid = false; - } - if (req.status == 200 && valid) { - if (window.redirectToJellyfin == true) { - const url = ((document.getElementById("modal-success") as HTMLDivElement).querySelector("a.submit") as HTMLAnchorElement).href; - window.location.href = url; - } else { - if (window.customSuccessCard) { - const content = window.successModal.asElement().querySelector(".card"); - content.innerHTML = content.innerHTML.replace(new RegExp("{username}", "g"), send.username) - } else if (window.userPageEnabled) { - const userPageNoticeArea = document.getElementById("modal-success-user-page-area"); - const link = `${userPageNoticeArea.getAttribute("my-account-term")}`; - userPageNoticeArea.innerHTML = userPageNoticeArea.textContent.replace("{myAccount}", link); - } - window.successModal.show(); + _post( + "/user/invite", + send, + (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(submitSpan); + let vals = req.response as ValidatorRespDTO; + let valid = true; + for (let type in vals) { + if (requirements[type]) requirements[type].valid = vals[type]; + if (!vals[type]) valid = false; } - } else if (req.status != 401 && req.status != 400){ - submitSpan.classList.add("~critical"); - submitSpan.classList.remove("~urge"); - if (req.response["error"] as string) { - submitSpan.textContent = window.messages[req.response["error"]]; - } else { - submitSpan.textContent = window.messages["errorPassword"]; - } - setTimeout(() => { - submitSpan.classList.add("~urge"); - submitSpan.classList.remove("~critical"); - submitSpan.textContent = submitText; - }, 1000); - } - }, true, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(submitSpan); - if (req.status == 401 || req.status == 400) { - if (req.response["error"] as string) { - if (req.response["error"] == "confirmEmail") { - window.confirmationModal.show(); - return; + if (req.status == 200 && valid) { + if (window.redirectToJellyfin == true) { + const url = ( + (document.getElementById("modal-success") as HTMLDivElement).querySelector( + "a.submit", + ) as HTMLAnchorElement + ).href; + window.location.href = url; + } else { + if (window.customSuccessCard) { + const content = window.successModal.asElement().querySelector(".card"); + content.innerHTML = content.innerHTML.replace(new RegExp("{username}", "g"), send.username); + } else if (window.userPageEnabled) { + const userPageNoticeArea = document.getElementById("modal-success-user-page-area"); + const link = `${userPageNoticeArea.getAttribute("my-account-term")}`; + userPageNoticeArea.innerHTML = userPageNoticeArea.textContent.replace("{myAccount}", link); + } + window.successModal.show(); } - if (req.response["error"] in window.messages) { + } else if (req.status != 401 && req.status != 400) { + submitSpan.classList.add("~critical"); + submitSpan.classList.remove("~urge"); + if (req.response["error"] as string) { submitSpan.textContent = window.messages[req.response["error"]]; } else { - submitSpan.textContent = req.response["error"]; + submitSpan.textContent = window.messages["errorPassword"]; } - setTimeout(() => { submitSpan.textContent = submitText; }, 1000); + setTimeout(() => { + submitSpan.classList.add("~urge"); + submitSpan.classList.remove("~critical"); + submitSpan.textContent = submitText; + }, 1000); } - } - }); + }, + true, + (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(submitSpan); + if (req.status == 401 || req.status == 400) { + if (req.response["error"] as string) { + if (req.response["error"] == "confirmEmail") { + window.confirmationModal.show(); + return; + } + if (req.response["error"] in window.messages) { + submitSpan.textContent = window.messages[req.response["error"]]; + } else { + submitSpan.textContent = req.response["error"]; + } + setTimeout(() => { + submitSpan.textContent = submitText; + }, 1000); + } + } + }, + ); }; validator.validate(); @@ -379,6 +396,6 @@ validator.validate(); form.onsubmit = create; const invitedByAside = document.getElementById("invite-from-user"); -if (typeof(invitedByAside) != "undefined" && invitedByAside != null) { +if (typeof invitedByAside != "undefined" && invitedByAside != null) { invitedByAside.textContent = invitedByAside.textContent.replace("{user}", invitedByAside.getAttribute("data-from")); } diff --git a/ts/modules/account-linking.ts b/ts/modules/account-linking.ts index 3968da7..ba39dcc 100644 --- a/ts/modules/account-linking.ts +++ b/ts/modules/account-linking.ts @@ -45,7 +45,7 @@ export interface ServiceConfiguration { accountLinkedError: string; successError: string; successFunc: (modalClosed: boolean) => void; -}; +} export interface DiscordInvite { invite: string; @@ -61,7 +61,9 @@ export class ServiceLinker { protected _name: string; protected _pin: string; - get verified(): boolean { return this._verified; } + get verified(): boolean { + return this._verified; + } constructor(conf: ServiceConfiguration) { this._conf = conf; @@ -90,7 +92,7 @@ export class ServiceLinker { this._verified = true; this._waiting.classList.add("~positive"); this._waiting.classList.remove("~info"); - window.notifications.customPositive(this._name + "Verified", "", this._conf.successError); + window.notifications.customPositive(this._name + "Verified", "", this._conf.successError); if (this._conf.successFunc) { this._conf.successFunc(false); } @@ -100,7 +102,6 @@ export class ServiceLinker { this._conf.successFunc(true); } }, 2000); - } else if (!this._modalClosed) { setTimeout(this._checkVerified, 1500); } @@ -135,29 +136,29 @@ export class ServiceLinker { } export class Discord extends ServiceLinker { - constructor(conf: ServiceConfiguration) { super(conf); this._name = "discord"; this._waiting = document.getElementById("discord-waiting") as HTMLSpanElement; } - private _getInviteURL = () => _get(this._conf.inviteURL, null, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - const inv = req.response as DiscordInvite; - const link = document.getElementById("discord-invite") as HTMLSpanElement; - (link.parentElement as HTMLAnchorElement).href = inv.invite; - (link.parentElement as HTMLAnchorElement).target = "_blank"; - let innerHTML = ``; - if (inv.icon != "") { - innerHTML += `${window.discordServerName}`; - } else { - innerHTML += ` + private _getInviteURL = () => + _get(this._conf.inviteURL, null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + const inv = req.response as DiscordInvite; + const link = document.getElementById("discord-invite") as HTMLSpanElement; + (link.parentElement as HTMLAnchorElement).href = inv.invite; + (link.parentElement as HTMLAnchorElement).target = "_blank"; + let innerHTML = ``; + if (inv.icon != "") { + innerHTML += `${window.discordServerName}`; + } else { + innerHTML += ` ${window.discordServerName} `; - } - link.innerHTML = innerHTML; - }); + } + link.innerHTML = innerHTML; + }); onclick() { if (this._conf.inviteURL != "") { @@ -176,7 +177,7 @@ export class Telegram extends ServiceLinker { this._name = "telegram"; this._waiting = document.getElementById("telegram-waiting") as HTMLSpanElement; } -}; +} export interface MatrixConfiguration { modal: Modal; @@ -198,14 +199,20 @@ export class Matrix { private _input: HTMLInputElement; private _submit: HTMLSpanElement; - get verified(): boolean { return this._verified; } - get pin(): string { return this._pin; } + get verified(): boolean { + return this._verified; + } + get pin(): string { + return this._pin; + } constructor(conf: MatrixConfiguration) { this._conf = conf; this._input = document.getElementById("matrix-userid") as HTMLInputElement; this._submit = document.getElementById("matrix-send") as HTMLSpanElement; - this._submit.onclick = () => { this._onclick(); }; + this._submit.onclick = () => { + this._onclick(); + }; } private _onclick = () => { @@ -220,52 +227,53 @@ export class Matrix { show = () => { this._input.value = ""; this._conf.modal.show(); - } + }; - private _sendMessage = () => _post(this._conf.sendMessageURL, { "user_id": this._input.value }, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(this._submit); - if (req.status == 400 && req.response["error"] == "errorAccountLinked") { - this._conf.modal.close(); - window.notifications.customError("accountLinkedError", this._conf.accountLinkedError); - return; - } else if (req.status != 200) { - this._conf.modal.close(); - window.notifications.customError("unknownError", this._conf.unknownError); - return; - } - this._userID = this._input.value; - this._submit.classList.add("~positive"); - this._submit.classList.remove("~info"); - setTimeout(() => { - this._submit.classList.add("~info"); - this._submit.classList.remove("~positive"); - }, 2000); - this._input.placeholder = "PIN"; - this._input.value = ""; - }); - - private _verifyCode = () => _get(this._conf.verifiedURL + this._userID + "/" + this._input.value, null, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(this._submit); - const valid = req.response["success"] as boolean; - if (valid) { - this._conf.modal.close(); - window.notifications.customPositive(this._name + "Verified", "", this._conf.successError); - this._verified = true; - this._pin = this._input.value; - if (this._conf.successFunc) { - this._conf.successFunc(); + private _sendMessage = () => + _post(this._conf.sendMessageURL, { user_id: this._input.value }, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(this._submit); + if (req.status == 400 && req.response["error"] == "errorAccountLinked") { + this._conf.modal.close(); + window.notifications.customError("accountLinkedError", this._conf.accountLinkedError); + return; + } else if (req.status != 200) { + this._conf.modal.close(); + window.notifications.customError("unknownError", this._conf.unknownError); + return; } - } else { - window.notifications.customError("invalidCodeError", this._conf.invalidCodeError); - this._submit.classList.add("~critical"); + this._userID = this._input.value; + this._submit.classList.add("~positive"); this._submit.classList.remove("~info"); setTimeout(() => { this._submit.classList.add("~info"); - this._submit.classList.remove("~critical"); - }, 800); - } - }); -} + this._submit.classList.remove("~positive"); + }, 2000); + this._input.placeholder = "PIN"; + this._input.value = ""; + }); + private _verifyCode = () => + _get(this._conf.verifiedURL + this._userID + "/" + this._input.value, null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(this._submit); + const valid = req.response["success"] as boolean; + if (valid) { + this._conf.modal.close(); + window.notifications.customPositive(this._name + "Verified", "", this._conf.successError); + this._verified = true; + this._pin = this._input.value; + if (this._conf.successFunc) { + this._conf.successFunc(); + } + } else { + window.notifications.customError("invalidCodeError", this._conf.invalidCodeError); + this._submit.classList.add("~critical"); + this._submit.classList.remove("~info"); + setTimeout(() => { + this._submit.classList.add("~info"); + this._submit.classList.remove("~critical"); + }, 800); + } + }); +} diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 2825e4a..ccde9c9 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -1,16 +1,26 @@ -import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateString, insertText, toClipboard } from "../modules/common" -import { templateEmail } from "../modules/settings" +import { + _get, + _post, + _delete, + toggleLoader, + addLoader, + removeLoader, + toDateString, + insertText, + toClipboard, +} from "../modules/common"; +import { templateEmail } from "../modules/settings"; import { Marked } from "@ts-stack/markdown"; -import { stripMarkdown } from "../modules/stripmd" -import { DiscordUser, newDiscordSearch } from "../modules/discord" -import { SearchConfiguration, QueryType, SearchableItem, SearchableItemDataAttribute } from "../modules/search" -import { HiddenInputField } from "./ui" -import { PaginatedList } from "./list" +import { stripMarkdown } from "../modules/stripmd"; +import { DiscordUser, newDiscordSearch } from "../modules/discord"; +import { SearchConfiguration, QueryType, SearchableItem, SearchableItemDataAttribute } from "../modules/search"; +import { HiddenInputField } from "./ui"; +import { PaginatedList } from "./list"; declare var window: GlobalWindow; -const USER_DEFAULT_SORT_FIELD = "name"; -const USER_DEFAULT_SORT_ASCENDING = true; +const USER_DEFAULT_SORT_FIELD = "name"; +const USER_DEFAULT_SORT_ASCENDING = true; const dateParser = require("any-date-parser"); @@ -18,8 +28,8 @@ enum SelectAllState { None = 0, Some = 0.1, AllVisible = 0.9, - All = 1 -}; + All = 1, +} interface User { id: string; @@ -54,117 +64,118 @@ interface announcementTemplate { } var addDiscord: (passData: string) => void; - -const queries = (): { [field: string]: QueryType } => { return { - "id": { - // We don't use a translation here to circumvent the name substitution feature. - name: "Jellyfin/Emby ID", - getter: "id", - bool: false, - string: true, - date: false - }, - "label": { - name: window.lang.strings("label"), - getter: "label", - bool: true, - string: true, - date: false - }, - "username": { - name: window.lang.strings("username"), - getter: "name", - bool: false, - string: true, - date: false - }, - "name": { - name: window.lang.strings("username"), - getter: "name", - bool: false, - string: true, - date: false, - show: false - }, - "admin": { - name: window.lang.strings("admin"), - getter: "admin", - bool: true, - string: false, - date: false - }, - "disabled": { - name: window.lang.strings("disabled"), - getter: "disabled", - bool: true, - string: false, - date: false - }, - "access-jfa": { - name: window.lang.strings("accessJFA"), - getter: "accounts_admin", - bool: true, - string: false, - date: false, - dependsOnElement: ".accounts-header-access-jfa" - }, - "email": { - name: window.lang.strings("emailAddress"), - getter: "email", - bool: true, - string: true, - date: false, - dependsOnElement: ".accounts-header-email" - }, - "telegram": { - name: "Telegram", - getter: "telegram", - bool: true, - string: true, - date: false, - dependsOnElement: ".accounts-header-telegram" - }, - "matrix": { - name: "Matrix", - getter: "matrix", - bool: true, - string: true, - date: false, - dependsOnElement: ".accounts-header-matrix" - }, - "discord": { - name: "Discord", - getter: "discord", - bool: true, - string: true, - date: false, - dependsOnElement: ".accounts-header-discord" - }, - "expiry": { - name: window.lang.strings("expiry"), - getter: "expiry", - bool: true, - string: false, - date: true, - dependsOnElement: ".accounts-header-expiry" - }, - "last-active": { - name: window.lang.strings("lastActiveTime"), - getter: "last_active", - bool: true, - string: false, - date: true - }, - "referrals-enabled": { - name: window.lang.strings("referrals"), - getter: "referrals_enabled", - bool: true, - string: false, - date: false, - dependsOnElement: ".accounts-header-referrals" - } -}}; +const queries = (): { [field: string]: QueryType } => { + return { + id: { + // We don't use a translation here to circumvent the name substitution feature. + name: "Jellyfin/Emby ID", + getter: "id", + bool: false, + string: true, + date: false, + }, + label: { + name: window.lang.strings("label"), + getter: "label", + bool: true, + string: true, + date: false, + }, + username: { + name: window.lang.strings("username"), + getter: "name", + bool: false, + string: true, + date: false, + }, + name: { + name: window.lang.strings("username"), + getter: "name", + bool: false, + string: true, + date: false, + show: false, + }, + admin: { + name: window.lang.strings("admin"), + getter: "admin", + bool: true, + string: false, + date: false, + }, + disabled: { + name: window.lang.strings("disabled"), + getter: "disabled", + bool: true, + string: false, + date: false, + }, + "access-jfa": { + name: window.lang.strings("accessJFA"), + getter: "accounts_admin", + bool: true, + string: false, + date: false, + dependsOnElement: ".accounts-header-access-jfa", + }, + email: { + name: window.lang.strings("emailAddress"), + getter: "email", + bool: true, + string: true, + date: false, + dependsOnElement: ".accounts-header-email", + }, + telegram: { + name: "Telegram", + getter: "telegram", + bool: true, + string: true, + date: false, + dependsOnElement: ".accounts-header-telegram", + }, + matrix: { + name: "Matrix", + getter: "matrix", + bool: true, + string: true, + date: false, + dependsOnElement: ".accounts-header-matrix", + }, + discord: { + name: "Discord", + getter: "discord", + bool: true, + string: true, + date: false, + dependsOnElement: ".accounts-header-discord", + }, + expiry: { + name: window.lang.strings("expiry"), + getter: "expiry", + bool: true, + string: false, + date: true, + dependsOnElement: ".accounts-header-expiry", + }, + "last-active": { + name: window.lang.strings("lastActiveTime"), + getter: "last_active", + bool: true, + string: false, + date: true, + }, + "referrals-enabled": { + name: window.lang.strings("referrals"), + getter: "referrals_enabled", + bool: true, + string: false, + date: false, + dependsOnElement: ".accounts-header-referrals", + }, + }; +}; class user implements User, SearchableItem { private _id = ""; @@ -195,7 +206,7 @@ class user implements User, SearchableItem { private _label: HTMLInputElement; private _labelEditor: HiddenInputField; private _userLabel: string; - private _accounts_admin: HTMLInputElement + private _accounts_admin: HTMLInputElement; private _selected: boolean; private _referralsEnabled: boolean; private _referralsEnabledCheck: HTMLElement; @@ -212,7 +223,7 @@ class user implements User, SearchableItem { if (matrix) return "matrix"; if (telegram) return "telegram"; if (email) return "email"; - } + }; private _checkUnlinkArea = () => { const unlinkHeader = this._notifyDropdown.querySelector(".accounts-unlink-header") as HTMLSpanElement; @@ -221,9 +232,11 @@ class user implements User, SearchableItem { } else { unlinkHeader.classList.remove("unfocused"); } - } + }; - get selected(): boolean { return this._selected; } + get selected(): boolean { + return this._selected; + } set selected(state: boolean) { this.setSelected(state, true); } @@ -231,29 +244,37 @@ class user implements User, SearchableItem { setSelected(state: boolean, dispatchEvent: boolean) { this._selected = state; this._check.checked = state; - if (dispatchEvent) state ? document.dispatchEvent(this._checkEvent()) : document.dispatchEvent(this._uncheckEvent()); + if (dispatchEvent) + state ? document.dispatchEvent(this._checkEvent()) : document.dispatchEvent(this._uncheckEvent()); } + get name(): string { + return this._username.textContent; + } + set name(value: string) { + this._username.textContent = value; + } - get name(): string { return this._username.textContent; } - set name(value: string) { this._username.textContent = value; } - - get admin(): boolean { return !(this._admin.classList.contains("hidden")); } + get admin(): boolean { + return !this._admin.classList.contains("hidden"); + } set admin(state: boolean) { if (state) { - this._admin.classList.remove("hidden") + this._admin.classList.remove("hidden"); this._admin.textContent = window.lang.strings("admin"); } else { - this._admin.classList.add("hidden") + this._admin.classList.add("hidden"); this._admin.textContent = ""; } } - get accounts_admin(): boolean { return this._accounts_admin.checked; } + get accounts_admin(): boolean { + return this._accounts_admin.checked; + } set accounts_admin(a: boolean) { if (!window.jellyfinLogin) return; this._accounts_admin.checked = a; - this._accounts_admin.disabled = (window.jfAllowAll || (a && this.admin && window.jfAdminOnly)); + this._accounts_admin.disabled = window.jfAllowAll || (a && this.admin && window.jfAdminOnly); if (this._accounts_admin.disabled) { this._accounts_admin.title = window.lang.strings("accessJFASettings"); } else { @@ -261,18 +282,22 @@ class user implements User, SearchableItem { } } - get disabled(): boolean { return !(this._disabled.classList.contains("hidden")); } + get disabled(): boolean { + return !this._disabled.classList.contains("hidden"); + } set disabled(state: boolean) { if (state) { - this._disabled.classList.remove("hidden") + this._disabled.classList.remove("hidden"); this._disabled.textContent = window.lang.strings("disabled"); } else { - this._disabled.classList.add("hidden") + this._disabled.classList.add("hidden"); this._disabled.textContent = ""; } } - get email(): string { return this._emailAddress; } + get email(): string { + return this._emailAddress; + } set email(value: string) { this._emailAddress = value; this._emailEditor.value = value; @@ -287,14 +312,18 @@ class user implements User, SearchableItem { } } - get notify_email(): boolean { return this._notifyEmail; } + get notify_email(): boolean { + return this._notifyEmail; + } set notify_email(s: boolean) { if (this._notifyDropdown) { (this._notifyDropdown.querySelector(".accounts-contact-email") as HTMLInputElement).checked = s; } } - get referrals_enabled(): boolean { return this._referralsEnabled; } + get referrals_enabled(): boolean { + return this._referralsEnabled; + } set referrals_enabled(v: boolean) { this._referralsEnabled = v; if (!window.referralsEnabled) return; @@ -363,9 +392,13 @@ class user implements User, SearchableItem { for (let i = 0; i < checks.length; i++) { checks[i].onclick = () => this._setNotifyMethod(); } - + for (let service of ["telegram", "discord", "matrix"]) { - el.querySelector(".accounts-unlink-"+service).addEventListener("click", () => _delete(`/users/${service}`, {"id": this.id}, () => document.dispatchEvent(new CustomEvent("accounts-reload")))); + el.querySelector(".accounts-unlink-" + service).addEventListener("click", () => + _delete(`/users/${service}`, { id: this.id }, () => + document.dispatchEvent(new CustomEvent("accounts-reload")), + ), + ); } button.onclick = () => { @@ -373,15 +406,19 @@ class user implements User, SearchableItem { document.addEventListener("click", outerClickListener); }; const outerClickListener = (event: Event) => { - if (!(event.target instanceof HTMLElement && (el.contains(event.target) || button.contains(event.target)))) { + if ( + !(event.target instanceof HTMLElement && (el.contains(event.target) || button.contains(event.target))) + ) { dropdown.classList.remove("selected"); document.removeEventListener("click", outerClickListener); } }; return el; - } + }; - get matrix(): string { return this._matrixID; } + get matrix(): string { + return this._matrixID; + } set matrix(u: string) { if (!window.matrixEnabled) { this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused"); @@ -409,12 +446,14 @@ class user implements User, SearchableItem { `; if (lastNotifyMethod) { - (this._matrix.querySelector(".accounts-settings-area") as HTMLDivElement).appendChild(this._notifyDropdown); + (this._matrix.querySelector(".accounts-settings-area") as HTMLDivElement).appendChild( + this._notifyDropdown, + ); } } this._checkUnlinkArea(); } - + private _addMatrix = () => { const addButton = this._matrix.querySelector(".btn") as HTMLSpanElement; const input = this._matrix.querySelector("input.stealth-input") as HTMLInputElement; @@ -423,9 +462,14 @@ class user implements User, SearchableItem { input.classList.remove("unfocused"); addIcon.classList.add("ri-check-line"); addIcon.classList.remove("ri-link"); - addButton.classList.remove("chip") + addButton.classList.remove("chip"); const outerClickListener = (event: Event) => { - if (!(event.target instanceof HTMLElement && (this._matrix.contains(event.target) || addButton.contains(event.target)))) { + if ( + !( + event.target instanceof HTMLElement && + (this._matrix.contains(event.target) || addButton.contains(event.target)) + ) + ) { document.dispatchEvent(new CustomEvent("accounts-reload")); document.removeEventListener("click", outerClickListener); } @@ -435,29 +479,36 @@ class user implements User, SearchableItem { if (input.value.charAt(0) != "@" || !input.value.includes(":")) return; const send = { jf_id: this.id, - user_id: input.value - } + user_id: input.value, + }; _post("/users/matrix", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { document.dispatchEvent(new CustomEvent("accounts-reload")); if (req.status != 200) { - window.notifications.customError("errorConnectMatrix", window.lang.notif("errorFailureCheckLogs")); + window.notifications.customError( + "errorConnectMatrix", + window.lang.notif("errorFailureCheckLogs"), + ); return; } window.notifications.customSuccess("connectMatrix", window.lang.notif("accountConnected")); } }); } - } + }; - get notify_matrix(): boolean { return this._notifyMatrix; } + get notify_matrix(): boolean { + return this._notifyMatrix; + } set notify_matrix(s: boolean) { if (this._notifyDropdown) { (this._notifyDropdown.querySelector(".accounts-contact-matrix") as HTMLInputElement).checked = s; } } - - get telegram(): string { return this._telegramUsername; } + + get telegram(): string { + return this._telegramUsername; + } set telegram(u: string) { if (!window.telegramEnabled) { this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused"); @@ -480,13 +531,17 @@ class user implements User, SearchableItem { `; if (lastNotifyMethod) { - (this._telegram.querySelector(".accounts-settings-area") as HTMLDivElement).appendChild(this._notifyDropdown); + (this._telegram.querySelector(".accounts-settings-area") as HTMLDivElement).appendChild( + this._notifyDropdown, + ); } } this._checkUnlinkArea(); } - - get notify_telegram(): boolean { return this._notifyTelegram; } + + get notify_telegram(): boolean { + return this._notifyTelegram; + } set notify_telegram(s: boolean) { if (this._notifyDropdown) { (this._notifyDropdown.querySelector(".accounts-contact-telegram") as HTMLInputElement).checked = s; @@ -497,37 +552,49 @@ class user implements User, SearchableItem { const email = this._notifyDropdown.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement; let send = { id: this.id, - email: email.checked - } + email: email.checked, + }; if (window.telegramEnabled && this._telegramUsername) { - const telegram = this._notifyDropdown.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement; + const telegram = this._notifyDropdown.getElementsByClassName( + "accounts-contact-telegram", + )[0] as HTMLInputElement; send["telegram"] = telegram.checked; } if (window.discordEnabled && this._discordUsername) { - const discord = this._notifyDropdown.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement; + const discord = this._notifyDropdown.getElementsByClassName( + "accounts-contact-discord", + )[0] as HTMLInputElement; send["discord"] = discord.checked; } - _post("/users/contact", send, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 200) { - window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings")); + _post( + "/users/contact", + send, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) { + window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings")); + document.dispatchEvent(new CustomEvent("accounts-reload")); + return; + } + } + }, + false, + (req: XMLHttpRequest) => { + if (req.status == 0) { + window.notifications.connectionError(); document.dispatchEvent(new CustomEvent("accounts-reload")); return; + } else if (req.status == 401) { + window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); + document.dispatchEvent(new CustomEvent("accounts-reload")); } - } - }, false, (req: XMLHttpRequest) => { - if (req.status == 0) { - window.notifications.connectionError(); - document.dispatchEvent(new CustomEvent("accounts-reload")); - return; - } else if (req.status == 401) { - window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); - document.dispatchEvent(new CustomEvent("accounts-reload")); - } - }); + }, + ); + }; + + get discord(): string { + return this._discordUsername; } - - get discord(): string { return this._discordUsername; } set discord(u: string) { if (!window.discordEnabled) { this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused"); @@ -550,48 +617,60 @@ class user implements User, SearchableItem { `; if (lastNotifyMethod) { - (this._discord.querySelector(".accounts-settings-area") as HTMLDivElement).appendChild(this._notifyDropdown); + (this._discord.querySelector(".accounts-settings-area") as HTMLDivElement).appendChild( + this._notifyDropdown, + ); } } this._checkUnlinkArea(); } - get discord_id(): string { return this._discordID; } + get discord_id(): string { + return this._discordID; + } set discord_id(id: string) { - if (!window.discordEnabled || this._discordUsername == "") return; + if (!window.discordEnabled || this._discordUsername == "") return; this._discordID = id; const link = this._discord.getElementsByClassName("discord-link")[0] as HTMLAnchorElement; link.href = `https://discord.com/users/${id}`; } - - get notify_discord(): boolean { return this._notifyDiscord; } + + get notify_discord(): boolean { + return this._notifyDiscord; + } set notify_discord(s: boolean) { if (this._notifyDropdown) { (this._notifyDropdown.querySelector(".accounts-contact-discord") as HTMLInputElement).checked = s; } } - get expiry(): number { return this._expiryUnix; } + get expiry(): number { + return this._expiryUnix; + } set expiry(unix: number) { this._expiryUnix = unix; if (unix == 0) { this._expiry.textContent = ""; } else { - this._expiry.textContent = toDateString(new Date(unix*1000)); + this._expiry.textContent = toDateString(new Date(unix * 1000)); } } - get last_active(): number { return this._lastActiveUnix; } + get last_active(): number { + return this._lastActiveUnix; + } set last_active(unix: number) { this._lastActiveUnix = unix; if (unix == 0) { this._lastActive.textContent == "n/a"; } else { - this._lastActive.textContent = toDateString(new Date(unix*1000)); + this._lastActive.textContent = toDateString(new Date(unix * 1000)); } } - get label(): string { return this._userLabel; } + get label(): string { + return this._userLabel; + } set label(l: string) { this._userLabel = l ? l : ""; this._labelEditor.value = l ? l : ""; @@ -607,10 +686,10 @@ class user implements User, SearchableItem { this.matrix.toLowerCase().includes(query) || this.telegram.toLowerCase().includes(query) ); - } + }; - private _checkEvent = () => new CustomEvent("accountCheckEvent", {detail: this.id}); - private _uncheckEvent = () => new CustomEvent("accountUncheckEvent", {detail: this.id}); + private _checkEvent = () => new CustomEvent("accountCheckEvent", { detail: this.id }); + private _uncheckEvent = () => new CustomEvent("accountUncheckEvent", { detail: this.id }); constructor(user: User) { this._row = document.createElement("tr") as HTMLTableRowElement; @@ -688,8 +767,10 @@ class user implements User, SearchableItem { clickAwayShouldSave: false, }); - this._check.onchange = () => { this.selected = this._check.checked; } - + this._check.onchange = () => { + this.selected = this._check.checked; + }; + if (window.jellyfinLogin) { this._accounts_admin.onchange = () => { this.accounts_admin = this._accounts_admin.checked; @@ -705,7 +786,7 @@ class user implements User, SearchableItem { }); }; } - + if (window.referralsEnabled) { this._referralsEnabledCheck = this._row.querySelector(".accounts-referrals"); } @@ -713,13 +794,13 @@ class user implements User, SearchableItem { this._notifyDropdown = this._constructDropdown(); this.update(user); - + document.addEventListener("timefmt-change", () => { this.expiry = this.expiry; this.last_active = this.last_active; }); } - + private _updateLabel = () => { let send = {}; send[this.id] = this._labelEditor.value; @@ -739,66 +820,83 @@ class user implements User, SearchableItem { _post("/users/emails", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200) { - window.notifications.customSuccess("emailChanged", window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`)); + window.notifications.customSuccess( + "emailChanged", + window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`), + ); } else { this.email = this._emailEditor.previous; - window.notifications.customError("emailChanged", window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`)); + window.notifications.customError( + "emailChanged", + window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`), + ); } } }); - } - - private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => { - if (req.readyState == 4 && req.status == 200) { - const modal = window.modals.telegram.modal; - const pin = modal.getElementsByClassName("pin")[0] as HTMLElement; - const link = modal.getElementsByClassName("link")[0] as HTMLAnchorElement; - const username = modal.getElementsByClassName("username")[0] as HTMLElement; - const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement; - let resp = req.response as getPinResponse; - pin.textContent = resp.token; - link.href = "https://t.me/" + resp.username; - username.textContent = resp.username; - addLoader(waiting); - let modalClosed = false; - window.modals.telegram.onclose = () => { - modalClosed = true; - removeLoader(waiting); - } - let send = { - token: resp.token, - id: this.id - }; - const checkVerified = () => _post("/users/telegram", send, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 200 && req.response["success"] as boolean) { - removeLoader(waiting); - waiting.classList.add("~positive"); - waiting.classList.remove("~info"); - window.notifications.customSuccess("telegramVerified", window.lang.notif("telegramVerified")); - setTimeout(() => { - window.modals.telegram.close(); - waiting.classList.add("~info"); - waiting.classList.remove("~positive"); - }, 2000); - document.dispatchEvent(new CustomEvent("accounts-reload")); - } else if (!modalClosed) { - setTimeout(checkVerified, 1500); - } - } - }, true); - window.modals.telegram.show(); - checkVerified(); - } - }); + }; - get id() { return this._id; } + private _addTelegram = () => + _get("/telegram/pin", null, (req: XMLHttpRequest) => { + if (req.readyState == 4 && req.status == 200) { + const modal = window.modals.telegram.modal; + const pin = modal.getElementsByClassName("pin")[0] as HTMLElement; + const link = modal.getElementsByClassName("link")[0] as HTMLAnchorElement; + const username = modal.getElementsByClassName("username")[0] as HTMLElement; + const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement; + let resp = req.response as getPinResponse; + pin.textContent = resp.token; + link.href = "https://t.me/" + resp.username; + username.textContent = resp.username; + addLoader(waiting); + let modalClosed = false; + window.modals.telegram.onclose = () => { + modalClosed = true; + removeLoader(waiting); + }; + let send = { + token: resp.token, + id: this.id, + }; + const checkVerified = () => + _post( + "/users/telegram", + send, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 && (req.response["success"] as boolean)) { + removeLoader(waiting); + waiting.classList.add("~positive"); + waiting.classList.remove("~info"); + window.notifications.customSuccess( + "telegramVerified", + window.lang.notif("telegramVerified"), + ); + setTimeout(() => { + window.modals.telegram.close(); + waiting.classList.add("~info"); + waiting.classList.remove("~positive"); + }, 2000); + document.dispatchEvent(new CustomEvent("accounts-reload")); + } else if (!modalClosed) { + setTimeout(checkVerified, 1500); + } + } + }, + true, + ); + window.modals.telegram.show(); + checkVerified(); + } + }); + + get id() { + return this._id; + } set id(v: string) { this._id = v; this._row.setAttribute(SearchableItemDataAttribute, v); } - update = (user: User) => { this.id = user.id; this.name = user.name; @@ -822,16 +920,18 @@ class user implements User, SearchableItem { this.label = user.label; this.accounts_admin = user.accounts_admin; this.referrals_enabled = user.referrals_enabled; - } + }; - asElement = (): HTMLTableRowElement => { return this._row; } + asElement = (): HTMLTableRowElement => { + return this._row; + }; remove = () => { if (this.selected) { document.dispatchEvent(this._uncheckEvent()); } - this._row.remove(); - } -} + this._row.remove(); + }; +} interface UsersDTO extends paginatedDTO { users: User[]; @@ -851,7 +951,7 @@ declare interface ExtendExpiryDTO { export class accountsList extends PaginatedList { protected _container = document.getElementById("accounts-list") as HTMLTableSectionElement; - + private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement; private _announceButton = document.getElementById("accounts-announce") as HTMLSpanElement; private _announceSaveButton = document.getElementById("save-announce") as HTMLSpanElement; @@ -866,7 +966,7 @@ export class accountsList extends PaginatedList { private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement; private _expiryDropdown = document.getElementById("accounts-expiry-dropdown") as HTMLElement; private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement; - private _extendExpiryForm = document.getElementById("form-extend-expiry") as HTMLFormElement; + private _extendExpiryForm = document.getElementById("form-extend-expiry") as HTMLFormElement; private _extendExpiryTextInput = document.getElementById("extend-expiry-text") as HTMLInputElement; private _extendExpiryFieldInputs = document.getElementById("extend-expiry-field-inputs") as HTMLElement; private _extendExpiryFromPreviousExpiry = document.getElementById("expiry-use-previous") as HTMLInputElement; @@ -891,14 +991,18 @@ export class accountsList extends PaginatedList { private _applyHomescreen = document.getElementById("modify-user-homescreen") as HTMLInputElement; private _applyConfiguration = document.getElementById("modify-user-configuration") as HTMLInputElement; - private _applyOmbi = window.ombiEnabled ? document.getElementById("modify-user-ombi") as HTMLInputElement : null; - private _applyJellyseerr = window.jellyseerrEnabled ? document.getElementById("modify-user-jellyseerr") as HTMLInputElement : null; + private _applyOmbi = window.ombiEnabled ? (document.getElementById("modify-user-ombi") as HTMLInputElement) : null; + private _applyJellyseerr = window.jellyseerrEnabled + ? (document.getElementById("modify-user-jellyseerr") as HTMLInputElement) + : null; private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; private _selectAllState: SelectAllState = SelectAllState.None; // private _users: { [id: string]: user }; // private _ordering: string[] = []; - get users(): { [id: string]: user } { return this._search.items as { [id: string]: user }; } + get users(): { [id: string]: user } { + return this._search.items as { [id: string]: user }; + } // set users(v: { [id: string]: user }) { this._search.items = v as SearchableItems; } // Whether the enable/disable button should enable or not. @@ -909,7 +1013,7 @@ export class accountsList extends PaginatedList { private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement; private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement; private _addUserProfile = this._addUserForm.querySelector("select") as HTMLSelectElement; - + // Columns for sorting. private _columns: { [className: string]: Column } = {}; @@ -923,24 +1027,28 @@ export class accountsList extends PaginatedList { const fieldIDs = ["months", "days", "hours", "minutes"]; const prefixes = ["extend-expiry-"]; for (let i = 0; i < fieldIDs.length; i++) { - for (let j = 0; j < prefixes.length; j++) { + for (let j = 0; j < prefixes.length; j++) { const field = document.getElementById(prefixes[j] + fieldIDs[i]); - field.textContent = ''; + field.textContent = ""; for (let n = 0; n <= this._maxDayHourMinuteOptions; n++) { - const opt = document.createElement("option") as HTMLOptionElement; - opt.textContent = ""+n; - opt.value = ""+n; - field.appendChild(opt); + const opt = document.createElement("option") as HTMLOptionElement; + opt.textContent = "" + n; + opt.value = "" + n; + field.appendChild(opt); } } } - } - + }; + constructor() { super({ loader: document.getElementById("accounts-loader"), - loadMoreButtons: Array.from([document.getElementById("accounts-load-more") as HTMLButtonElement]) as Array, - loadAllButtons: Array.from(document.getElementsByClassName("accounts-load-all")) as Array, + loadMoreButtons: Array.from([ + document.getElementById("accounts-load-more") as HTMLButtonElement, + ]) as Array, + loadAllButtons: Array.from( + document.getElementsByClassName("accounts-load-all"), + ) as Array, refreshButton: document.getElementById("accounts-refresh") as HTMLButtonElement, filterArea: document.getElementById("accounts-filter-area"), searchOptionsHeader: document.getElementById("accounts-search-options-header"), @@ -951,7 +1059,7 @@ export class accountsList extends PaginatedList { itemsPerPage: 40, maxItemsLoadedForSearch: 200, appendNewItems: (resp: paginatedDTO) => { - for (let u of ((resp as UsersDTO).users || [])) { + for (let u of (resp as UsersDTO).users || []) { if (u.id in this.users) { this.users[u.id].update(u); } else { @@ -962,14 +1070,16 @@ export class accountsList extends PaginatedList { this._search.setOrdering( this._columns[this._search.sortField].sort(this.users), this._search.sortField, - this._search.ascending + this._search.ascending, ); }, replaceWithNewItems: (resp: paginatedDTO) => { let accountsOnDOM: { [id: string]: boolean } = {}; - for (let id of Object.keys(this.users)) { accountsOnDOM[id] = true; } - for (let u of ((resp as UsersDTO).users || [])) { + for (let id of Object.keys(this.users)) { + accountsOnDOM[id] = true; + } + for (let u of (resp as UsersDTO).users || []) { if (u.id in accountsOnDOM) { this.users[u.id].update(u); delete accountsOnDOM[u.id]; @@ -981,14 +1091,14 @@ export class accountsList extends PaginatedList { // Delete accounts w/ remaining IDs (those not in resp.users) // console.log("Removing", Object.keys(accountsOnDOM).length, "from DOM"); for (let id in accountsOnDOM) { - this.users[id].remove() + this.users[id].remove(); delete this.users[id]; } this._search.setOrdering( this._columns[this._search.sortField].sort(this.users), this._search.sortField, - this._search.ascending + this._search.ascending, ); }, defaultSortField: USER_DEFAULT_SORT_FIELD, @@ -997,10 +1107,10 @@ export class accountsList extends PaginatedList { if (req.readyState != 4) return; // FIXME: Error message if (req.status != 200) return; - } + }, }); this._populateNumbers(); - + let searchConfig: SearchConfiguration = { filterArea: this._c.filterArea, sortingByButton: this._sortingByButton, @@ -1019,9 +1129,9 @@ export class accountsList extends PaginatedList { searchServer: null, clearServerSearch: null, }; - + this.initSearch(searchConfig); - + // FIXME: Remove! (window as any).accs = this; @@ -1029,8 +1139,14 @@ export class accountsList extends PaginatedList { this._selectAllState = SelectAllState.None; this._selectAll.onchange = () => this.cycleSelectAll(); document.addEventListener("accounts-reload", () => this.reload()); - document.addEventListener("accountCheckEvent", () => { this._counter.selected++; this.processSelectedAccounts(); }); - document.addEventListener("accountUncheckEvent", () => { this._counter.selected--; this.processSelectedAccounts(); }); + document.addEventListener("accountCheckEvent", () => { + this._counter.selected++; + this.processSelectedAccounts(); + }); + document.addEventListener("accountUncheckEvent", () => { + this._counter.selected--; + this.processSelectedAccounts(); + }); this._addUserButton.onclick = () => { this._populateAddUserProfiles(); window.modals.addUser.toggle(); @@ -1052,7 +1168,7 @@ export class accountsList extends PaginatedList { const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement; if (this._modifySettingsProfile.checked) { this._userSelect.parentElement.classList.add("unfocused"); - this._profileSelect.parentElement.classList.remove("unfocused") + this._profileSelect.parentElement.classList.remove("unfocused"); profileSpan.classList.add("@high"); profileSpan.classList.remove("@low"); userSpan.classList.remove("@high"); @@ -1080,7 +1196,7 @@ export class accountsList extends PaginatedList { console.debug("States:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked); if (this._enableReferralsProfile.checked) { this._referralsInviteSelect.parentElement.classList.add("unfocused"); - this._referralsProfileSelect.parentElement.classList.remove("unfocused") + this._referralsProfileSelect.parentElement.classList.remove("unfocused"); profileSpan.classList.add("@high"); profileSpan.classList.remove("@low"); inviteSpan.classList.remove("@high"); @@ -1099,7 +1215,7 @@ export class accountsList extends PaginatedList { this._enableReferralsInvite.checked = false; checkReferralSource(); }; - inviteSpan.onclick = () => {; + inviteSpan.onclick = () => { this._enableReferralsInvite.checked = true; this._enableReferralsProfile.checked = false; checkReferralSource(); @@ -1116,17 +1232,21 @@ export class accountsList extends PaginatedList { this._announceButton.onclick = this.announce; this._announceButton.parentElement.classList.add("unfocused"); - this._extendExpiry.onclick = () => { this.extendExpiry(); }; - this._removeExpiry.onclick = () => { this.removeExpiry(); }; + this._extendExpiry.onclick = () => { + this.extendExpiry(); + }; + this._removeExpiry.onclick = () => { + this.removeExpiry(); + }; this._expiryDropdown.classList.add("unfocused"); this._extendExpiryDate.classList.add("unfocused"); this._extendExpiryTextInput.onkeyup = () => { this._extendExpiryTextInput.parentElement.classList.remove("opacity-60"); this._extendExpiryFieldInputs.classList.add("opacity-60"); - this._usingExtendExpiryTextInput = true; + this._usingExtendExpiryTextInput = true; this._displayExpiryDate(); - } + }; this._extendExpiryTextInput.onclick = () => { this._extendExpiryTextInput.parentElement.classList.remove("opacity-60"); @@ -1143,9 +1263,9 @@ export class accountsList extends PaginatedList { }; this._extendExpiryFromPreviousExpiry.onclick = this._displayExpiryDate; - + for (let field of ["months", "days", "hours", "minutes"]) { - (document.getElementById("extend-expiry-"+field) as HTMLSelectElement).onchange = () => { + (document.getElementById("extend-expiry-" + field) as HTMLSelectElement).onchange = () => { this._extendExpiryFieldInputs.classList.remove("opacity-60"); this._extendExpiryTextInput.parentElement.classList.add("opacity-60"); this._usingExtendExpiryTextInput = false; @@ -1156,7 +1276,9 @@ export class accountsList extends PaginatedList { this._disableEnable.onclick = this.enableDisableUsers; this._disableEnable.parentElement.classList.add("unfocused"); - this._enableExpiry.onclick = () => { this.extendExpiry(true); }; + this._enableExpiry.onclick = () => { + this.extendExpiry(true); + }; this._enableExpiryNotify.onchange = () => { if (this._enableExpiryNotify.checked) { this._enableExpiryReason.classList.remove("unfocused"); @@ -1181,19 +1303,27 @@ export class accountsList extends PaginatedList { }*/ this._announceTextarea.onkeyup = this.loadPreview; - addDiscord = newDiscordSearch(window.lang.strings("linkDiscord"), window.lang.strings("searchDiscordUser"), window.lang.strings("add"), (user: DiscordUser, id: string) => { - _post("/users/discord", {jf_id: id, discord_id: user.id}, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - document.dispatchEvent(new CustomEvent("accounts-reload")); - if (req.status != 200) { - window.notifications.customError("errorConnectDiscord", window.lang.notif("errorFailureCheckLogs")); - return + addDiscord = newDiscordSearch( + window.lang.strings("linkDiscord"), + window.lang.strings("searchDiscordUser"), + window.lang.strings("add"), + (user: DiscordUser, id: string) => { + _post("/users/discord", { jf_id: id, discord_id: user.id }, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + document.dispatchEvent(new CustomEvent("accounts-reload")); + if (req.status != 200) { + window.notifications.customError( + "errorConnectDiscord", + window.lang.notif("errorFailureCheckLogs"), + ); + return; + } + window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected")); + window.modals.discord.close(); } - window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected")); - window.modals.discord.close() - } - }); - }); + }); + }, + ); this._announceSaveButton.onclick = this.saveAnnouncement; const announceVarUsername = document.getElementById("announce-variables-username") as HTMLSpanElement; @@ -1202,12 +1332,38 @@ export class accountsList extends PaginatedList { this.loadPreview(); }; - const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active", "referrals"]; - const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active", "referrals_enabled"]; + const headerNames: string[] = [ + "username", + "access-jfa", + "email", + "telegram", + "matrix", + "discord", + "expiry", + "last-active", + "referrals", + ]; + const headerGetters: string[] = [ + "name", + "accounts_admin", + "email", + "telegram", + "matrix", + "discord", + "expiry", + "last_active", + "referrals_enabled", + ]; for (let i = 0; i < headerNames.length; i++) { - const header: HTMLTableCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableCellElement; + const header: HTMLTableCellElement = document.querySelector( + ".accounts-header-" + headerNames[i], + ) as HTMLTableCellElement; if (header !== null) { - this._columns[headerGetters[i]] = new Column(header, headerGetters[i], Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get); + this._columns[headerGetters[i]] = new Column( + header, + headerGetters[i], + Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get, + ); } } @@ -1226,7 +1382,7 @@ export class accountsList extends PaginatedList { this._search.setOrdering( this._columns[event.detail].sort(this.users), event.detail, - this._columns[event.detail].ascending + this._columns[event.detail].ascending, ); this._sortingByButton.replaceChildren(this._columns[event.detail].asElement()); this._sortingByButton.classList.remove("hidden"); @@ -1252,26 +1408,25 @@ export class accountsList extends PaginatedList { window.modals.announce.onclose = () => { const preview = document.getElementById("announce-preview") as HTMLDivElement; preview.textContent = ``; - } + }; } reload = (callback?: (resp: paginatedDTO) => void) => { this._reload(callback); this.loadTemplates(); - } + }; loadMore = (loadAll: boolean = false, callback?: (resp?: paginatedDTO) => void) => { - this._loadMore( - loadAll, - callback - ); + this._loadMore(loadAll, callback); }; loadAll = (callback?: (resp?: paginatedDTO) => void) => { this._loadAll(callback); }; - get selectAll(): SelectAllState { return this._selectAllState; } + get selectAll(): SelectAllState { + return this._selectAllState; + } cycleSelectAll = () => { let next: SelectAllState; @@ -1316,15 +1471,15 @@ export class accountsList extends PaginatedList { } this._selectAll.indeterminate = this.lastPage ? false : true; this.processSelectedAccounts(); - } + }; if (next == SelectAllState.AllVisible) { selectAllVisible(); return; } - + if (next == SelectAllState.All) { this.loadAll((_: paginatedDTO) => { - if (!(this.lastPage)) { + if (!this.lastPage) { // Pretend to live-select elements as they load. this._counter.selected = this._counter.shown; return; @@ -1333,24 +1488,24 @@ export class accountsList extends PaginatedList { }); return; } - } - + }; + selectAllBetweenIDs = (startID: string, endID: string) => { let inRange = false; for (let id of this._search.ordering) { if (!(inRange || id == startID)) continue; inRange = true; - if (!(this._container.contains(this.users[id].asElement()))) continue; + if (!this._container.contains(this.users[id].asElement())) continue; this.users[id].selected = true; if (id == endID) return; } - } + }; add = (u: User) => { let domAccount = new user(u); this.users[u.id] = domAccount; // console.log("after appending lengths:", Object.keys(this.users).length, Object.keys(this._search.items).length); - } + }; private processSelectedAccounts = () => { console.debug("processSelectedAccounts"); @@ -1401,7 +1556,7 @@ export class accountsList extends PaginatedList { let noContactCount = 0; let referralState = Number(this.users[list[0]].referrals_enabled); // -1 = hide, 0 = show "enable", 1 = show "disable" // Only show enable/disable button if all selected have the same state. - this._shouldEnable = this.users[list[0]].disabled + this._shouldEnable = this.users[list[0]].disabled; let showDisableEnable = true; for (let id of list) { if (!anyNonExpiries && !this.users[id].expiry) { @@ -1415,11 +1570,17 @@ export class accountsList extends PaginatedList { showDisableEnable = false; this._disableEnable.parentElement.classList.add("unfocused"); } - if (!showDisableEnable && anyNonExpiries) { break; } + if (!showDisableEnable && anyNonExpiries) { + break; + } if (!this.users[id].lastNotifyMethod()) { noContactCount++; } - if (window.referralsEnabled && referralState != -1 && Number(this.users[id].referrals_enabled) != referralState) { + if ( + window.referralsEnabled && + referralState != -1 && + Number(this.users[id].referrals_enabled) != referralState + ) { referralState = -1; } } @@ -1460,7 +1621,7 @@ export class accountsList extends PaginatedList { if (window.referralsEnabled) { if (referralState == -1) { this._enableReferrals.classList.add("unfocused"); - } else { + } else { this._enableReferrals.classList.remove("unfocused"); } if (referralState == 0) { @@ -1474,24 +1635,26 @@ export class accountsList extends PaginatedList { } } } - } - + }; + private _collectUsers = (): string[] => { let list: string[] = []; for (let id of this._visible) { - if (this.users[id].selected) { list.push(id) } + if (this.users[id].selected) { + list.push(id); + } } return list; - } + }; private _addUser = (event: Event) => { event.preventDefault(); const button = this._addUserForm.querySelector("span.submit") as HTMLSpanElement; const send = { - "username": this._addUserName.value, - "email": this._addUserEmail.value, - "password": this._addUserPassword.value, - "profile": this._addUserProfile.value, + username: this._addUserName.value, + email: this._addUserEmail.value, + password: this._addUserPassword.value, + profile: this._addUserProfile.value, }; for (let field in send) { if (!send[field]) { @@ -1500,32 +1663,40 @@ export class accountsList extends PaginatedList { } } toggleLoader(button); - _post("/user", send, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - toggleLoader(button); - if (req.status == 200 || (req.response["user"] as boolean)) { - window.notifications.customSuccess("addUser", window.lang.var("notifications", "userCreated", `"${send['username']}"`)); - if (!req.response["email"]) { - window.notifications.customError("sendWelcome", window.lang.notif("errorSendWelcomeEmail")); - console.error("User created, but welcome email failed"); + _post( + "/user", + send, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 200 || (req.response["user"] as boolean)) { + window.notifications.customSuccess( + "addUser", + window.lang.var("notifications", "userCreated", `"${send["username"]}"`), + ); + if (!req.response["email"]) { + window.notifications.customError("sendWelcome", window.lang.notif("errorSendWelcomeEmail")); + console.error("User created, but welcome email failed"); + } + } else { + let msg = window.lang.var("notifications", "errorUserCreated", `"${send["username"]}"`); + if ("error" in req.response) { + let realError = window.lang.notif(req.response["error"]); + if (realError) msg = realError; + } + window.notifications.customError("addUser", msg); } - } else { - let msg = window.lang.var("notifications", "errorUserCreated", `"${send['username']}"`); - if ("error" in req.response) { - let realError = window.lang.notif(req.response["error"]); - if (realError) msg = realError; + if (req.response["error"] as String) { + console.error(req.response["error"]); } - window.notifications.customError("addUser", msg); - } - if (req.response["error"] as String) { - console.error(req.response["error"]); - } - this.reload(); - window.modals.addUser.close(); - } - }, true); - } + this.reload(); + window.modals.addUser.close(); + } + }, + true, + ); + }; loadPreview = () => { let content = this._announceTextarea.value; if (!this._previewLoaded) { @@ -1535,7 +1706,7 @@ export class accountsList extends PaginatedList { content = Marked.parse(content); this._announcePreview.innerHTML = content; } - } + }; saveAnnouncement = (event: Event) => { event.preventDefault(); const form = document.getElementById("form-announce") as HTMLFormElement; @@ -1550,13 +1721,15 @@ export class accountsList extends PaginatedList { return; } const name = (this._announceNameLabel.querySelector("input") as HTMLInputElement).value; - if (!name) { return; } + if (!name) { + return; + } const subject = document.getElementById("announce-subject") as HTMLInputElement; let send: announcementTemplate = { name: name, subject: subject.value, - message: this._announceTextarea.value - } + message: this._announceTextarea.value, + }; _post("/users/announce/template", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { this.reload(); @@ -1569,7 +1742,7 @@ export class accountsList extends PaginatedList { } } }); - } + }; announce = (event?: Event, template?: announcementTemplate) => { const modalHeader = document.getElementById("header-announce"); modalHeader.textContent = window.lang.quantity("announceTo", this._collectUsers().length); @@ -1594,18 +1767,24 @@ export class accountsList extends PaginatedList { event.preventDefault(); toggleLoader(button); let send = { - "users": list, - "subject": subject.value, - "message": this._announceTextarea.value - } + users: list, + subject: subject.value, + message: this._announceTextarea.value, + }; _post("/users/announce", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { toggleLoader(button); window.modals.announce.close(); if (req.status != 200 && req.status != 204) { - window.notifications.customError("announcementError", window.lang.notif("errorFailureCheckLogs")); + window.notifications.customError( + "announcementError", + window.lang.notif("errorFailureCheckLogs"), + ); } else { - window.notifications.customSuccess("announcementSuccess", window.lang.notif("sentAnnouncement")); + window.notifications.customSuccess( + "announcementSuccess", + window.lang.notif("sentAnnouncement"), + ); } } }); @@ -1619,7 +1798,7 @@ export class accountsList extends PaginatedList { this._previewLoaded = false; return; } - + let templ = req.response as templateEmail; if (!templ.html) { preview.innerHTML = `
`;
@@ -1633,64 +1812,77 @@ export class accountsList extends PaginatedList {
                 window.modals.announce.show();
             }
         });
-    }
-    loadTemplates = () => _get("/users/announce", null, (req: XMLHttpRequest) => {
-        if (req.readyState == 4) {
-            if (req.status != 200) {
-                this._announceButton.nextElementSibling.children[0].classList.add("unfocused");
-                return;
-            }
-            this._announceButton.nextElementSibling.children[0].classList.remove("unfocused");
-            const list = req.response["announcements"] as string[];
-            if (list.length == 0) {
-                this._announceButton.nextElementSibling.children[0].classList.add("unfocused");
-                return;
-            }
-            if (list.length > 0) {
-                this._announceButton.innerHTML = `${window.lang.strings("announce")} `;
-            }
-            const dList = document.getElementById("accounts-announce-templates") as HTMLDivElement;
-            dList.textContent = '';
-            for (let name of list) {
-                const el = document.createElement("div") as HTMLDivElement;
-                el.classList.add("flex", "flex-row", "gap-2", "justify-between", "truncate");
-                el.innerHTML = `
+    };
+    loadTemplates = () =>
+        _get("/users/announce", null, (req: XMLHttpRequest) => {
+            if (req.readyState == 4) {
+                if (req.status != 200) {
+                    this._announceButton.nextElementSibling.children[0].classList.add("unfocused");
+                    return;
+                }
+                this._announceButton.nextElementSibling.children[0].classList.remove("unfocused");
+                const list = req.response["announcements"] as string[];
+                if (list.length == 0) {
+                    this._announceButton.nextElementSibling.children[0].classList.add("unfocused");
+                    return;
+                }
+                if (list.length > 0) {
+                    this._announceButton.innerHTML = `${window.lang.strings("announce")} `;
+                }
+                const dList = document.getElementById("accounts-announce-templates") as HTMLDivElement;
+                dList.textContent = "";
+                for (let name of list) {
+                    const el = document.createElement("div") as HTMLDivElement;
+                    el.classList.add("flex", "flex-row", "gap-2", "justify-between", "truncate");
+                    el.innerHTML = `
                 ${name}×
                 `;
-                let urlSafeName = encodeURIComponent(encodeURIComponent(name));
-                (el.querySelector("span.accounts-announce-template-button") as HTMLSpanElement).onclick = () => {
-                    _get("/users/announce/" + urlSafeName, null, (req: XMLHttpRequest) => {
-                        if (req.readyState == 4) {
-                            let template: announcementTemplate;
-                            if (req.status != 200) {
-                                window.notifications.customError("getTemplateError", window.lang.notif("errorFailureCheckLogs"));
-                            } else {
-                                template = req.response;
+                    let urlSafeName = encodeURIComponent(encodeURIComponent(name));
+                    (el.querySelector("span.accounts-announce-template-button") as HTMLSpanElement).onclick = () => {
+                        _get("/users/announce/" + urlSafeName, null, (req: XMLHttpRequest) => {
+                            if (req.readyState == 4) {
+                                let template: announcementTemplate;
+                                if (req.status != 200) {
+                                    window.notifications.customError(
+                                        "getTemplateError",
+                                        window.lang.notif("errorFailureCheckLogs"),
+                                    );
+                                } else {
+                                    template = req.response;
+                                }
+                                this.announce(null, template);
                             }
-                            this.announce(null, template);
-                        }
-                    });
-                };
-                (el.querySelector("span.accounts-announce-template-delete") as HTMLSpanElement).onclick = () => {
-                    _delete("/users/announce/" + urlSafeName, null, (req: XMLHttpRequest) => {
-                        if (req.readyState == 4) {
-                            if (req.status != 200) {
-                                window.notifications.customError("deleteTemplateError", window.lang.notif("errorFailureCheckLogs"));
+                        });
+                    };
+                    (el.querySelector("span.accounts-announce-template-delete") as HTMLSpanElement).onclick = () => {
+                        _delete("/users/announce/" + urlSafeName, null, (req: XMLHttpRequest) => {
+                            if (req.readyState == 4) {
+                                if (req.status != 200) {
+                                    window.notifications.customError(
+                                        "deleteTemplateError",
+                                        window.lang.notif("errorFailureCheckLogs"),
+                                    );
+                                }
+                                this.reload();
                             }
-                            this.reload();
-                        }
-                    });
-                };
-                dList.appendChild(el);
+                        });
+                    };
+                    dList.appendChild(el);
+                }
             }
-        }
-    });
+        });
 
-    private _enableDisableUsers = (users: string[], enable: boolean, notify: boolean, reason: string|null, post: (req: XMLHttpRequest) => void) => {
+    private _enableDisableUsers = (
+        users: string[],
+        enable: boolean,
+        notify: boolean,
+        reason: string | null,
+        post: (req: XMLHttpRequest) => void,
+    ) => {
         let send = {
-            "users": users,
-            "enabled": enable,
-            "notify": notify
+            users: users,
+            enabled: enable,
+            notify: notify,
         };
         if (reason) send["reason"] = reason;
         _post("/users/enable", send, post, true);
@@ -1719,27 +1911,39 @@ export class accountsList extends PaginatedList {
         form.onsubmit = (event: Event) => {
             event.preventDefault();
             toggleLoader(button);
-            this._enableDisableUsers(list, this._shouldEnable, this._deleteNotify.checked, this._deleteNotify ? this._deleteReason.value : null, (req: XMLHttpRequest) => {
-                if (req.readyState == 4) {
-                    toggleLoader(button);
-                    window.modals.deleteUser.close();
-                    if (req.status != 200 && req.status != 204) {
-                        let errorMsg = window.lang.notif("errorFailureCheckLogs");
-                        if (!("error" in req.response)) {
-                            errorMsg = window.lang.notif("errorPartialFailureCheckLogs");
+            this._enableDisableUsers(
+                list,
+                this._shouldEnable,
+                this._deleteNotify.checked,
+                this._deleteNotify ? this._deleteReason.value : null,
+                (req: XMLHttpRequest) => {
+                    if (req.readyState == 4) {
+                        toggleLoader(button);
+                        window.modals.deleteUser.close();
+                        if (req.status != 200 && req.status != 204) {
+                            let errorMsg = window.lang.notif("errorFailureCheckLogs");
+                            if (!("error" in req.response)) {
+                                errorMsg = window.lang.notif("errorPartialFailureCheckLogs");
+                            }
+                            window.notifications.customError("deleteUserError", errorMsg);
+                        } else if (this._shouldEnable) {
+                            window.notifications.customSuccess(
+                                "enableUserSuccess",
+                                window.lang.quantity("enabledUser", list.length),
+                            );
+                        } else {
+                            window.notifications.customSuccess(
+                                "disableUserSuccess",
+                                window.lang.quantity("disabledUser", list.length),
+                            );
                         }
-                        window.notifications.customError("deleteUserError", errorMsg);
-                    } else if (this._shouldEnable) {
-                        window.notifications.customSuccess("enableUserSuccess", window.lang.quantity("enabledUser", list.length));
-                    } else {
-                        window.notifications.customSuccess("disableUserSuccess", window.lang.quantity("disabledUser", list.length));
+                        this.reload();
                     }
-                    this.reload();
-                }
-            });
-        }
+                },
+            );
+        };
         window.modals.deleteUser.show();
-    }
+    };
 
     deleteUsers = () => {
         const modalHeader = document.getElementById("header-delete-user");
@@ -1757,9 +1961,9 @@ export class accountsList extends PaginatedList {
             event.preventDefault();
             toggleLoader(button);
             let send = {
-                "users": list,
-                "notify": this._deleteNotify.checked,
-                "reason": this._deleteNotify ? this._deleteReason.value : ""
+                users: list,
+                notify: this._deleteNotify.checked,
+                reason: this._deleteNotify ? this._deleteReason.value : "",
             };
             _delete("/users", send, (req: XMLHttpRequest) => {
                 if (req.readyState == 4) {
@@ -1772,15 +1976,18 @@ export class accountsList extends PaginatedList {
                         }
                         window.notifications.customError("deleteUserError", errorMsg);
                     } else {
-                        window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", list.length));
+                        window.notifications.customSuccess(
+                            "deleteUserSuccess",
+                            window.lang.quantity("deletedUser", list.length),
+                        );
                     }
                     this.reload();
                 }
             });
         };
         window.modals.deleteUser.show();
-    }
-    
+    };
+
     sendPWR = () => {
         addLoader(this._sendPWR);
         let list = this._collectUsers();
@@ -1788,58 +1995,64 @@ export class accountsList extends PaginatedList {
         for (let id of list) {
             let user = this.users[id];
             if (!user.lastNotifyMethod() && !user.email) {
-                manualUser  = user;
+                manualUser = user;
                 break;
             }
         }
         const messageBox = document.getElementById("send-pwr-note") as HTMLParagraphElement;
         let message: string;
         let send = {
-            users: list
+            users: list,
         };
-        _post("/users/password-reset", send, (req: XMLHttpRequest) => {
-            if (req.readyState != 4) return;
-            removeLoader(this._sendPWR);
-            let link: string;
-            if (req.status == 200) {
-                link = req.response["link"];
-                if (req.response["manual"] as boolean) {
-                    message = window.lang.var("strings", "sendPWRManual", manualUser.name);
-                } else {
-                    message = window.lang.strings("sendPWRSuccess") + " " + window.lang.strings("sendPWRSuccessManual");
-                }
-            } else if (req.status == 204) {
+        _post(
+            "/users/password-reset",
+            send,
+            (req: XMLHttpRequest) => {
+                if (req.readyState != 4) return;
+                removeLoader(this._sendPWR);
+                let link: string;
+                if (req.status == 200) {
+                    link = req.response["link"];
+                    if (req.response["manual"] as boolean) {
+                        message = window.lang.var("strings", "sendPWRManual", manualUser.name);
+                    } else {
+                        message =
+                            window.lang.strings("sendPWRSuccess") + " " + window.lang.strings("sendPWRSuccessManual");
+                    }
+                } else if (req.status == 204) {
                     message = window.lang.strings("sendPWRSuccess");
-            } else {
-                window.notifications.customError("errorSendPWR", window.lang.strings("errorFailureCheckLogs"));
-                return;
-            }
-            message += " " + window.lang.strings("sendPWRValidFor");
-            messageBox.textContent = message;
-            let linkButton = document.getElementById("send-pwr-link") as HTMLSpanElement;
-            if (link) {
-                linkButton.classList.remove("unfocused");
-                linkButton.onclick = () => {
-                    toClipboard(link);
-                    linkButton.textContent = window.lang.strings("copied");
-                    linkButton.classList.add("~positive");
-                    linkButton.classList.remove("~urge");
-                    setTimeout(() => {
-                        linkButton.textContent = window.lang.strings("copy");
-                        linkButton.classList.add("~urge");
-                        linkButton.classList.remove("~positive");
-                    }, 800);
-                };
-            } else {
-                linkButton.classList.add("unfocused");
-            }
-            window.modals.sendPWR.show();
-        }, true);
-    }
+                } else {
+                    window.notifications.customError("errorSendPWR", window.lang.strings("errorFailureCheckLogs"));
+                    return;
+                }
+                message += " " + window.lang.strings("sendPWRValidFor");
+                messageBox.textContent = message;
+                let linkButton = document.getElementById("send-pwr-link") as HTMLSpanElement;
+                if (link) {
+                    linkButton.classList.remove("unfocused");
+                    linkButton.onclick = () => {
+                        toClipboard(link);
+                        linkButton.textContent = window.lang.strings("copied");
+                        linkButton.classList.add("~positive");
+                        linkButton.classList.remove("~urge");
+                        setTimeout(() => {
+                            linkButton.textContent = window.lang.strings("copy");
+                            linkButton.classList.add("~urge");
+                            linkButton.classList.remove("~positive");
+                        }, 800);
+                    };
+                } else {
+                    linkButton.classList.add("unfocused");
+                }
+                window.modals.sendPWR.show();
+            },
+            true,
+        );
+    };
 
     modifyUsers = () => {
         const modalHeader = document.getElementById("header-modify-user");
-        modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length)
+        modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length);
         let list = this._collectUsers();
 
         (() => {
@@ -1867,9 +2080,9 @@ export class accountsList extends PaginatedList {
             event.preventDefault();
             toggleLoader(button);
             let send = {
-                "apply_to": list,
-                "homescreen": this._applyHomescreen.checked,
-                "configuration": this._applyConfiguration.checked,
+                apply_to: list,
+                homescreen: this._applyHomescreen.checked,
+                configuration: this._applyConfiguration.checked,
             };
             if (window.ombiEnabled) {
                 send["ombi"] = this._applyOmbi.checked;
@@ -1877,7 +2090,7 @@ export class accountsList extends PaginatedList {
             if (window.jellyseerrEnabled) {
                 send["jellyseerr"] = this._applyJellyseerr.checked;
             }
-            if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) { 
+            if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
                 send["from"] = "profile";
                 send["profile"] = this._profileSelect.value;
             } else if (this._modifySettingsUser.checked && !this._modifySettingsProfile.checked) {
@@ -1905,7 +2118,10 @@ export class accountsList extends PaginatedList {
                         }
                         window.notifications.customError("modifySettingsError", errorMsg);
                     } else if (req.status == 200 || req.status == 204) {
-                        window.notifications.customSuccess("modifySettingsSuccess", window.lang.quantity("appliedSettings", this._collectUsers().length));
+                        window.notifications.customSuccess(
+                            "modifySettingsSuccess",
+                            window.lang.quantity("appliedSettings", this._collectUsers().length),
+                        );
                     }
                     this.reload();
                     window.modals.modifyUser.close();
@@ -1913,23 +2129,26 @@ export class accountsList extends PaginatedList {
             });
         };
         window.modals.modifyUser.show();
-    }
-    
+    };
+
     enableReferrals = () => {
         const modalHeader = document.getElementById("header-enable-referrals-user");
-        modalHeader.textContent = window.lang.quantity("enableReferralsFor", this._collectUsers().length)
+        modalHeader.textContent = window.lang.quantity("enableReferralsFor", this._collectUsers().length);
         let list = this._collectUsers();
 
         // Check if we're disabling or enabling
         if (this.users[list[0]].referrals_enabled) {
-            _delete("/users/referral", {"users": list}, (req: XMLHttpRequest) => {
+            _delete("/users/referral", { users: list }, (req: XMLHttpRequest) => {
                 if (req.readyState != 4 || req.status != 200) return;
-                window.notifications.customSuccess("disabledReferralsSuccess", window.lang.quantity("appliedSettings", list.length));
+                window.notifications.customSuccess(
+                    "disabledReferralsSuccess",
+                    window.lang.quantity("appliedSettings", list.length),
+                );
                 this.reload();
             });
             return;
         }
-            
+
         (() => {
             _get("/invites", null, (req: XMLHttpRequest) => {
                 if (req.readyState != 4 || req.status != 200) return;
@@ -1951,9 +2170,9 @@ export class accountsList extends PaginatedList {
                     this._enableReferralsInvite.checked = false;
                     innerHTML += ``;
                 }
-                this._enableReferralsProfile.checked = !(this._enableReferralsInvite.checked);
+                this._enableReferralsProfile.checked = !this._enableReferralsInvite.checked;
                 this._referralsInviteSelect.innerHTML = innerHTML;
-            
+
                 // 2. Profiles
 
                 innerHTML = "";
@@ -1970,34 +2189,49 @@ export class accountsList extends PaginatedList {
             event.preventDefault();
             toggleLoader(button);
             let send = {
-                "users": list
+                users: list,
             };
-            // console.log("profile:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked); 
-            if (this._enableReferralsProfile.checked && !this._enableReferralsInvite.checked) { 
+            // console.log("profile:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
+            if (this._enableReferralsProfile.checked && !this._enableReferralsInvite.checked) {
                 send["from"] = "profile";
                 send["profile"] = this._referralsProfileSelect.value;
             } else if (this._enableReferralsInvite.checked && !this._enableReferralsProfile.checked) {
                 send["from"] = "invite";
                 send["id"] = this._referralsInviteSelect.value;
             }
-            _post("/users/referral/" + send["from"] + "/" + (send["id"] ? send["id"] : send["profile"]) + "/" + (this._referralsExpiry.checked ? "with-expiry" : "none"), send, (req: XMLHttpRequest) => {
-                if (req.readyState == 4) {
-                    toggleLoader(button);
-                    if (req.status == 400) {
-                        window.notifications.customError("noReferralTemplateError", window.lang.notif("errorNoReferralTemplate"));
-                    } else if (req.status == 200 || req.status == 204) {
-                        window.notifications.customSuccess("enableReferralsSuccess", window.lang.quantity("appliedSettings", list.length));
+            _post(
+                "/users/referral/" +
+                    send["from"] +
+                    "/" +
+                    (send["id"] ? send["id"] : send["profile"]) +
+                    "/" +
+                    (this._referralsExpiry.checked ? "with-expiry" : "none"),
+                send,
+                (req: XMLHttpRequest) => {
+                    if (req.readyState == 4) {
+                        toggleLoader(button);
+                        if (req.status == 400) {
+                            window.notifications.customError(
+                                "noReferralTemplateError",
+                                window.lang.notif("errorNoReferralTemplate"),
+                            );
+                        } else if (req.status == 200 || req.status == 204) {
+                            window.notifications.customSuccess(
+                                "enableReferralsSuccess",
+                                window.lang.quantity("appliedSettings", list.length),
+                            );
+                        }
+                        this.reload();
+                        window.modals.enableReferralsUser.close();
                     }
-                    this.reload();
-                    window.modals.enableReferralsUser.close();
-                }
-            });
+                },
+            );
         };
         this._enableReferralsProfile.checked = true;
         this._enableReferralsInvite.checked = false;
         this._referralsExpiry.checked = false;
         window.modals.enableReferralsUser.show();
-    }
+    };
 
     removeExpiry = () => {
         const list = this._collectUsers();
@@ -2015,12 +2249,15 @@ export class accountsList extends PaginatedList {
         }
 
         if (success) {
-            window.notifications.customSuccess("modifySettingsSuccess", window.lang.quantity("appliedSettings", list.length));
+            window.notifications.customSuccess(
+                "modifySettingsSuccess",
+                window.lang.quantity("appliedSettings", list.length),
+            );
         } else {
             window.notifications.customError("modifySettingsError", window.lang.notif("errorSettingsFailed"));
         }
         this.reload();
-    }
+    };
 
     _displayExpiryDate = () => {
         let date: Date;
@@ -2038,18 +2275,22 @@ export class accountsList extends PaginatedList {
                     document.getElementById("extend-expiry-months") as HTMLSelectElement,
                     document.getElementById("extend-expiry-days") as HTMLSelectElement,
                     document.getElementById("extend-expiry-hours") as HTMLSelectElement,
-                    document.getElementById("extend-expiry-minutes") as HTMLSelectElement
+                    document.getElementById("extend-expiry-minutes") as HTMLSelectElement,
                 ];
-                invalid = fields[0].value == "0" && fields[1].value == "0" && fields[2].value == "0" && fields[3].value == "0";
+                invalid =
+                    fields[0].value == "0" &&
+                    fields[1].value == "0" &&
+                    fields[2].value == "0" &&
+                    fields[3].value == "0";
                 let id = users.length > 0 ? users[0] : "";
                 if (!id) invalid = true;
                 else {
-                    date = new Date(this.users[id].expiry*1000);
+                    date = new Date(this.users[id].expiry * 1000);
                     if (this.users[id].expiry == 0) date = new Date();
-                    date.setMonth(date.getMonth() + (+fields[0].value))
-                    date.setDate(date.getDate() + (+fields[1].value));
-                    date.setHours(date.getHours() + (+fields[2].value));
-                    date.setMinutes(date.getMinutes() + (+fields[3].value));
+                    date.setMonth(date.getMonth() + +fields[0].value);
+                    date.setDate(date.getDate() + +fields[1].value);
+                    date.setHours(date.getHours() + +fields[2].value);
+                    date.setMinutes(date.getMinutes() + +fields[3].value);
                 }
             }
         }
@@ -2068,13 +2309,13 @@ export class accountsList extends PaginatedList {
                 this._extendExpiryDate.innerHTML = `
                 
${window.lang.strings("accountWillExpire").replace("{date}", toDateString(date))} - ${users.length > 1 ? ""+window.lang.strings("expirationBasedOn")+"" : ""} + ${users.length > 1 ? "" + window.lang.strings("expirationBasedOn") + "" : ""}
`; this._extendExpiryDate.classList.remove("unfocused"); } } - } + }; extendExpiry = (enableUser?: boolean) => { const list = this._collectUsers(); @@ -2101,8 +2342,8 @@ export class accountsList extends PaginatedList { let send: ExtendExpiryDTO = { users: applyList, timestamp: 0, - notify: this._enableExpiryNotify.checked - } + notify: this._enableExpiryNotify.checked, + }; if (this._enableExpiryNotify.checked) { send.reason = this._enableExpiryReason.value; } @@ -2118,18 +2359,24 @@ export class accountsList extends PaginatedList { send.try_extend_from_previous_expiry = true; } for (let field of ["months", "days", "hours", "minutes"]) { - send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value; + send[field] = +(document.getElementById("extend-expiry-" + field) as HTMLSelectElement).value; } } _post("/users/extend", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status != 200 && req.status != 204) { - window.notifications.customError("extendExpiryError", window.lang.notif("errorFailureCheckLogs")); + window.notifications.customError( + "extendExpiryError", + window.lang.notif("errorFailureCheckLogs"), + ); } else { - window.notifications.customSuccess("extendExpiry", window.lang.quantity("extendedExpiry", applyList.length)); + window.notifications.customSuccess( + "extendExpiry", + window.lang.quantity("extendedExpiry", applyList.length), + ); } - window.modals.extendExpiry.close() + window.modals.extendExpiry.close(); this.reload(); } }); @@ -2137,31 +2384,37 @@ export class accountsList extends PaginatedList { this._extendExpiryForm.onsubmit = (event: Event) => { event.preventDefault(); if (enableUser) { - this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify.checked ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 200 && req.status != 204) { - window.modals.extendExpiry.close(); - let errorMsg = window.lang.notif("errorFailureCheckLogs"); - if (!("error" in req.response)) { - errorMsg = window.lang.notif("errorPartialFailureCheckLogs"); + this._enableDisableUsers( + applyList, + true, + this._enableExpiryNotify.checked, + this._enableExpiryNotify.checked ? this._enableExpiryReason.value : null, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200 && req.status != 204) { + window.modals.extendExpiry.close(); + let errorMsg = window.lang.notif("errorFailureCheckLogs"); + if (!("error" in req.response)) { + errorMsg = window.lang.notif("errorPartialFailureCheckLogs"); + } + window.notifications.customError("deleteUserError", errorMsg); + return; } - window.notifications.customError("deleteUserError", errorMsg); - return; + extend(); } - extend(); - } - }); + }, + ); } else { extend(); } - } + }; this._extendExpiryTextInput.value = ""; this._usingExtendExpiryTextInput = false; this._extendExpiryDate.classList.add("unfocused"); this._displayExpiryDate(); window.modals.extendExpiry.show(); - } - + }; + private _populateAddUserProfiles = () => { this._addUserProfile.textContent = ""; let innerHTML = ``; @@ -2169,7 +2422,7 @@ export class accountsList extends PaginatedList { innerHTML += ``; } this._addUserProfile.innerHTML = innerHTML; - } + }; focusAccount = (userID: string) => { console.debug("focusing user", userID); @@ -2177,35 +2430,33 @@ export class accountsList extends PaginatedList { this._search.onSearchBoxChange(); this._search.onServerSearch(); if (userID in this.users) this.users[userID].focus(); - } + }; public static readonly _accountURLEvent = "account-url"; - registerURLListener = () => document.addEventListener(accountsList._accountURLEvent, (event: CustomEvent) => { - this.focusAccount(event.detail); - }); + registerURLListener = () => + document.addEventListener(accountsList._accountURLEvent, (event: CustomEvent) => { + this.focusAccount(event.detail); + }); isAccountURL = () => { const urlParams = new URLSearchParams(window.location.search); const userID = urlParams.get("user"); return Boolean(userID); - } + }; loadAccountURL = () => { const urlParams = new URLSearchParams(window.location.search); const userID = urlParams.get("user"); this.focusAccount(userID); - } - + }; } // An alternate view showing accounts in sub-lists grouped by group/label. -export class groupedAccountsList { +export class groupedAccountsList {} - - -} - -export const accountURLEvent = (id: string) => { return new CustomEvent(accountsList._accountURLEvent, {"detail": id}) }; +export const accountURLEvent = (id: string) => { + return new CustomEvent(accountsList._accountURLEvent, { detail: id }); +}; type GetterReturnType = Boolean | boolean | String | Number | number; type Getter = () => GetterReturnType; @@ -2262,18 +2513,22 @@ class Column { hideIcon = () => { this._header.textContent = this._headerContent; - } + }; updateHeader = () => { this._header.innerHTML = ` ${this._headerContent} - + `; + }; + + asElement = () => { + return this._card; + }; + + get ascending() { + return this._ascending; } - - asElement = () => { return this._card }; - - get ascending() { return this._ascending; } set ascending(v: boolean) { this._ascending = v; if (v) { @@ -2303,5 +2558,5 @@ class Column { }); return userIDs; - } + }; } diff --git a/ts/modules/activity.ts b/ts/modules/activity.ts index e44f716..1bce7d0 100644 --- a/ts/modules/activity.ts +++ b/ts/modules/activity.ts @@ -1,5 +1,11 @@ import { _get, _post, _delete, toDateString } from "../modules/common.js"; -import { SearchConfiguration, QueryType, SearchableItem, SearchableItems, SearchableItemDataAttribute } from "../modules/search.js"; +import { + SearchConfiguration, + QueryType, + SearchableItem, + SearchableItems, + SearchableItemDataAttribute, +} from "../modules/search.js"; import { accountURLEvent } from "../modules/accounts.js"; import { inviteURLEvent } from "../modules/invites.js"; import { PaginatedList } from "./list.js"; @@ -10,149 +16,151 @@ const ACTIVITY_DEFAULT_SORT_FIELD = "time"; const ACTIVITY_DEFAULT_SORT_ASCENDING = false; export interface activity { - id: string; - type: string; - user_id: string; - source_type: string; - source: string; - invite_code: string; - value: string; - time: number; + id: string; + type: string; + user_id: string; + source_type: string; + source: string; + invite_code: string; + value: string; + time: number; username: string; source_username: string; ip: string; } var activityTypeMoods = { - "creation": 1, - "deletion": -1, - "disabled": -1, - "enabled": 1, - "contactLinked": 1, - "contactUnlinked": -1, - "changePassword": 0, - "resetPassword": 0, - "createInvite": 1, - "deleteInvite": -1 + creation: 1, + deletion: -1, + disabled: -1, + enabled: 1, + contactLinked: 1, + contactUnlinked: -1, + changePassword: 0, + resetPassword: 0, + createInvite: 1, + deleteInvite: -1, }; // window.lang doesn't exist at page load, so I made this a function that's invoked by activityList. -const queries = (): { [field: string]: QueryType } => { return { - "id": { - name: window.lang.strings("activityID"), - getter: "id", - bool: false, - string: true, - date: false - }, - "title": { - name: window.lang.strings("title"), - getter: "title", - bool: false, - string: true, - date: false, - localOnly: true - }, - "user": { - name: window.lang.strings("usersMentioned"), - getter: "mentionedUsers", - bool: false, - string: true, - date: false - }, - "actor": { - name: window.lang.strings("actor"), - description: window.lang.strings("actorDescription"), - getter: "actor", - bool: false, - string: true, - date: false - }, - "referrer": { - name: window.lang.strings("referrer"), - getter: "referrer", - bool: true, - string: true, - date: false - }, - "time": { - name: window.lang.strings("date"), - getter: "time", - bool: false, - string: false, - date: true - }, - "account-creation": { - name: window.lang.strings("accountCreationFilter"), - getter: "accountCreation", - bool: true, - string: false, - date: false - }, - "account-deletion": { - name: window.lang.strings("accountDeletionFilter"), - getter: "accountDeletion", - bool: true, - string: false, - date: false - }, - "account-disabled": { - name: window.lang.strings("accountDisabledFilter"), - getter: "accountDisabled", - bool: true, - string: false, - date: false - }, - "account-enabled": { - name: window.lang.strings("accountEnabledFilter"), - getter: "accountEnabled", - bool: true, - string: false, - date: false - }, - "contact-linked": { - name: window.lang.strings("contactLinkedFilter"), - getter: "contactLinked", - bool: true, - string: false, - date: false - }, - "contact-unlinked": { - name: window.lang.strings("contactUnlinkedFilter"), - getter: "contactUnlinked", - bool: true, - string: false, - date: false - }, - "password-change": { - name: window.lang.strings("passwordChangeFilter"), - getter: "passwordChange", - bool: true, - string: false, - date: false - }, - "password-reset": { - name: window.lang.strings("passwordResetFilter"), - getter: "passwordReset", - bool: true, - string: false, - date: false - }, - "invite-created": { - name: window.lang.strings("inviteCreatedFilter"), - getter: "inviteCreated", - bool: true, - string: false, - date: false - }, - "invite-deleted": { - name: window.lang.strings("inviteDeletedFilter"), - getter: "inviteDeleted", - bool: true, - string: false, - date: false - } -}}; +const queries = (): { [field: string]: QueryType } => { + return { + id: { + name: window.lang.strings("activityID"), + getter: "id", + bool: false, + string: true, + date: false, + }, + title: { + name: window.lang.strings("title"), + getter: "title", + bool: false, + string: true, + date: false, + localOnly: true, + }, + user: { + name: window.lang.strings("usersMentioned"), + getter: "mentionedUsers", + bool: false, + string: true, + date: false, + }, + actor: { + name: window.lang.strings("actor"), + description: window.lang.strings("actorDescription"), + getter: "actor", + bool: false, + string: true, + date: false, + }, + referrer: { + name: window.lang.strings("referrer"), + getter: "referrer", + bool: true, + string: true, + date: false, + }, + time: { + name: window.lang.strings("date"), + getter: "time", + bool: false, + string: false, + date: true, + }, + "account-creation": { + name: window.lang.strings("accountCreationFilter"), + getter: "accountCreation", + bool: true, + string: false, + date: false, + }, + "account-deletion": { + name: window.lang.strings("accountDeletionFilter"), + getter: "accountDeletion", + bool: true, + string: false, + date: false, + }, + "account-disabled": { + name: window.lang.strings("accountDisabledFilter"), + getter: "accountDisabled", + bool: true, + string: false, + date: false, + }, + "account-enabled": { + name: window.lang.strings("accountEnabledFilter"), + getter: "accountEnabled", + bool: true, + string: false, + date: false, + }, + "contact-linked": { + name: window.lang.strings("contactLinkedFilter"), + getter: "contactLinked", + bool: true, + string: false, + date: false, + }, + "contact-unlinked": { + name: window.lang.strings("contactUnlinkedFilter"), + getter: "contactUnlinked", + bool: true, + string: false, + date: false, + }, + "password-change": { + name: window.lang.strings("passwordChangeFilter"), + getter: "passwordChange", + bool: true, + string: false, + date: false, + }, + "password-reset": { + name: window.lang.strings("passwordResetFilter"), + getter: "passwordReset", + bool: true, + string: false, + date: false, + }, + "invite-created": { + name: window.lang.strings("inviteCreatedFilter"), + getter: "inviteCreated", + bool: true, + string: false, + date: false, + }, + "invite-deleted": { + name: window.lang.strings("inviteDeletedFilter"), + getter: "inviteDeleted", + bool: true, + string: false, + date: false, + }, + }; +}; // var moodColours = ["~warning", "~neutral", "~urge"]; @@ -173,37 +181,58 @@ export class Activity implements activity, SearchableItem { _genUserText = (): string => { return `${this._act.username || this._act.user_id.substring(0, 5)}`; - } + }; _genSrcUserText = (): string => { return `${this._act.source_username || this._act.source.substring(0, 5)}`; - } + }; _genUserLink = (): string => { return `${this._genUserText()}`; - } - + }; + _genSrcUserLink = (): string => { return `${this._genSrcUserText()}`; - } + }; - private _renderInvText = (): string => { return `${this.value || this.invite_code || "???"}`; } + private _renderInvText = (): string => { + return `${this.value || this.invite_code || "???"}`; + }; private _genInvLink = (): string => { return `${this._renderInvText()}`; + }; + + get accountCreation(): boolean { + return this.type == "creation"; + } + get accountDeletion(): boolean { + return this.type == "deletion"; + } + get accountDisabled(): boolean { + return this.type == "disabled"; + } + get accountEnabled(): boolean { + return this.type == "enabled"; + } + get contactLinked(): boolean { + return this.type == "contactLinked"; + } + get contactUnlinked(): boolean { + return this.type == "contactUnlinked"; + } + get passwordChange(): boolean { + return this.type == "changePassword"; + } + get passwordReset(): boolean { + return this.type == "resetPassword"; + } + get inviteCreated(): boolean { + return this.type == "createInvite"; + } + get inviteDeleted(): boolean { + return this.type == "deleteInvite"; } - - - get accountCreation(): boolean { return this.type == "creation"; } - get accountDeletion(): boolean { return this.type == "deletion"; } - get accountDisabled(): boolean { return this.type == "disabled"; } - get accountEnabled(): boolean { return this.type == "enabled"; } - get contactLinked(): boolean { return this.type == "contactLinked"; } - get contactUnlinked(): boolean { return this.type == "contactUnlinked"; } - get passwordChange(): boolean { return this.type == "changePassword"; } - get passwordReset(): boolean { return this.type == "resetPassword"; } - get inviteCreated(): boolean { return this.type == "createInvite"; } - get inviteDeleted(): boolean { return this.type == "deleteInvite"; } get mentionedUsers(): string { return (this.username + " " + this.source_username).toLowerCase(); @@ -220,7 +249,9 @@ export class Activity implements activity, SearchableItem { return this.source_username.toLowerCase(); } - get type(): string { return this._act.type; } + get type(): string { + return this._act.type; + } set type(v: string) { this._act.type = v; @@ -229,7 +260,7 @@ export class Activity implements activity, SearchableItem { el.classList.remove("~warning"); el.classList.remove("~neutral"); el.classList.remove("~urge"); - + if (mood == -1) { el.classList.add("~warning"); } else if (mood == 0) { @@ -243,7 +274,7 @@ export class Activity implements activity, SearchableItem { if (i-1 == mood) this._card.classList.add(moodColours[i]); else this._card.classList.remove(moodColours[i]); } */ - + // lazy late addition, hide then unhide if needed this._expiryTypeBadge.classList.add("unfocused"); if (this.type == "changePassword" || this.type == "resetPassword") { @@ -309,13 +340,17 @@ export class Activity implements activity, SearchableItem { } } - get time(): number { return this._timeUnix; } + get time(): number { + return this._timeUnix; + } set time(v: number) { this._timeUnix = v; - this._time.textContent = toDateString(new Date(v*1000)); + this._time.textContent = toDateString(new Date(v * 1000)); } - get source_type(): string { return this._act.source_type; } + get source_type(): string { + return this._act.source_type; + } set source_type(v: string) { this._act.source_type = v; if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") { @@ -329,7 +364,9 @@ export class Activity implements activity, SearchableItem { } } - get ip(): string { return this._act.ip; } + get ip(): string { + return this._act.ip; + } set ip(v: string) { this._act.ip = v; if (v) { @@ -341,42 +378,68 @@ export class Activity implements activity, SearchableItem { } } - get invite_code(): string { return this._act.invite_code; } + get invite_code(): string { + return this._act.invite_code; + } set invite_code(v: string) { this._act.invite_code = v; } - get value(): string { return this._act.value; } + get value(): string { + return this._act.value; + } set value(v: string) { this._act.value = v; } - get source(): string { return this._act.source; } + get source(): string { + return this._act.source; + } set source(v: string) { this._act.source = v; if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") { this._source.innerHTML = this._genInvLink(); - } else if ((this.source_type == "admin" || this.source_type == "user") && this._act.source != "" && this._act.source_username != "") { + } else if ( + (this.source_type == "admin" || this.source_type == "user") && + this._act.source != "" && + this._act.source_username != "" + ) { this._source.innerHTML = this._genSrcUserLink(); } } - get id(): string { return this._act.id; } + get id(): string { + return this._act.id; + } set id(v: string) { this._act.id = v; this._card.setAttribute(SearchableItemDataAttribute, v); } - get user_id(): string { return this._act.user_id; } - set user_id(v: string) { this._act.user_id = v; } + get user_id(): string { + return this._act.user_id; + } + set user_id(v: string) { + this._act.user_id = v; + } - get username(): string { return this._act.username; } - set username(v: string) { this._act.username = v; } + get username(): string { + return this._act.username; + } + set username(v: string) { + this._act.username = v; + } - get source_username(): string { return this._act.source_username; } - set source_username(v: string) { this._act.source_username = v; } + get source_username(): string { + return this._act.source_username; + } + set source_username(v: string) { + this._act.source_username = v; + } - get title(): string { return this._title.textContent; } + get title(): string { + return this._title.textContent; + } matchesSearch = (query: string): boolean => { // console.log(this.title, "matches", query, ":", this.title.includes(query)); @@ -385,7 +448,7 @@ export class Activity implements activity, SearchableItem { this.username.toLowerCase().includes(query) || this.source_username.toLowerCase().includes(query) ); - } + }; constructor(act: activity) { this._card = document.createElement("div"); @@ -431,8 +494,12 @@ export class Activity implements activity, SearchableItem { this.update(act); - const pseudoUsers = this._card.getElementsByClassName("activity-pseudo-link-user") as HTMLCollectionOf; - const pseudoInvites = this._card.getElementsByClassName("activity-pseudo-link-invite") as HTMLCollectionOf; + const pseudoUsers = this._card.getElementsByClassName( + "activity-pseudo-link-user", + ) as HTMLCollectionOf; + const pseudoInvites = this._card.getElementsByClassName( + "activity-pseudo-link-invite", + ) as HTMLCollectionOf; for (let i = 0; i < pseudoUsers.length; i++) { /*const navigate = (event: Event) => { @@ -465,24 +532,27 @@ export class Activity implements activity, SearchableItem { this.time = act.time; this.source = act.source; this.value = act.value; - this.type = act.type; + this.type = act.type; this.ip = act.ip; - } + }; - delete = () => _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - if (req.status == 200) { - window.notifications.customSuccess("activityDeleted", window.lang.notif("activityDeleted")); - } - document.dispatchEvent(activityReload); - }); + delete = () => + _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + if (req.status == 200) { + window.notifications.customSuccess("activityDeleted", window.lang.notif("activityDeleted")); + } + document.dispatchEvent(activityReload); + }); - asElement = () => { return this._card; }; + asElement = () => { + return this._card; + }; } interface ActivitiesReqDTO extends PaginatedReqDTO { type: string[]; -}; +} interface ActivitiesDTO extends paginatedDTO { activities: activity[]; @@ -493,15 +563,21 @@ export class activityList extends PaginatedList { protected _sortDirection = document.getElementById("activity-sort-direction") as HTMLButtonElement; protected _ascending: boolean; - - get activities(): { [id: string]: Activity } { return this._search.items as { [id: string]: Activity }; } + + get activities(): { [id: string]: Activity } { + return this._search.items as { [id: string]: Activity }; + } // set activities(v: { [id: string]: Activity }) { this._search.items = v as SearchableItems; } - + constructor() { super({ loader: document.getElementById("activity-loader"), - loadMoreButtons: Array.from([document.getElementById("activity-load-more") as HTMLButtonElement]) as Array, - loadAllButtons: Array.from(document.getElementsByClassName("activity-load-all")) as Array, + loadMoreButtons: Array.from([ + document.getElementById("activity-load-more") as HTMLButtonElement, + ]) as Array, + loadAllButtons: Array.from( + document.getElementsByClassName("activity-load-all"), + ) as Array, refreshButton: document.getElementById("activity-refresh") as HTMLButtonElement, filterArea: document.getElementById("activity-filter-area"), searchOptionsHeader: document.getElementById("activity-search-options-header"), @@ -513,7 +589,7 @@ export class activityList extends PaginatedList { maxItemsLoadedForSearch: 200, appendNewItems: (resp: paginatedDTO) => { let ordering: string[] = this._search.ordering; - for (let act of ((resp as ActivitiesDTO).activities || [])) { + for (let act of (resp as ActivitiesDTO).activities || []) { this.activities[act.id] = new Activity(act); ordering.push(act.id); } @@ -521,7 +597,7 @@ export class activityList extends PaginatedList { }, replaceWithNewItems: (resp: paginatedDTO) => { // FIXME: Implement updates to existing elements, rather than just wiping each time. - + // Remove existing items for (let id of Object.keys(this.activities)) { delete this.activities[id]; @@ -538,10 +614,10 @@ export class activityList extends PaginatedList { window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities")); return; } - } + }, }); - - this._container = document.getElementById("activity-card-list") + + this._container = document.getElementById("activity-card-list"); document.addEventListener("activity-reload", () => this.reload()); let searchConfig: SearchConfiguration = { @@ -561,25 +637,22 @@ export class activityList extends PaginatedList { onSearchCallback: null, searchServer: null, clearServerSearch: null, - } + }; this.initSearch(searchConfig); this.ascending = this._c.defaultSortAscending; - this._sortDirection.addEventListener("click", () => this.ascending = !this.ascending); + this._sortDirection.addEventListener("click", () => (this.ascending = !this.ascending)); } reload = (callback?: (resp: paginatedDTO) => void) => { this._reload(callback); - } + }; loadMore = (loadAll: boolean = false, callback?: () => void) => { - this._loadMore( - loadAll, - callback - ); + this._loadMore(loadAll, callback); }; - + loadAll = (callback?: (resp?: paginatedDTO) => void) => { this._loadAll(callback); }; @@ -616,5 +689,4 @@ export class activityList extends PaginatedList { this._keepSearchingDescription.classList.add("unfocused"); } };*/ - } diff --git a/ts/modules/captcha.ts b/ts/modules/captcha.ts index 8c41884..c7d56d1 100644 --- a/ts/modules/captcha.ts +++ b/ts/modules/captcha.ts @@ -13,9 +13,13 @@ export class Captcha { reCAPTCHA = false; code = ""; - get value(): string { return this.input.value; } + get value(): string { + return this.input.value; + } - hasChanged = (): boolean => { return this.value != this.previous; } + hasChanged = (): boolean => { + return this.value != this.previous; + }; baseValidatorWrapper = (_baseValidator: (oncomplete: (valid: boolean) => void, captchaValid: boolean) => void) => { return (oncomplete: (valid: boolean) => void): void => { @@ -30,35 +34,47 @@ export class Captcha { }; }; - verify = (callback: () => void) => _post("/captcha/verify/" + this.code + "/" + this.captchaID + "/" + this.input.value + (this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 204) { - this.checkbox.innerHTML = ``; - this.checkbox.classList.add("~positive"); - this.checkbox.classList.remove("~critical"); - this.verified = true; - } else { - this.checkbox.innerHTML = ``; - this.checkbox.classList.add("~critical"); - this.checkbox.classList.remove("~positive"); - this.verified = false; - } - callback(); - } - }); - - generate = () => _get("/captcha/gen/"+this.code+(this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 200) { - this.captchaID = this.isPWR ? this.code : req.response["id"]; - // the Math.random() appearance below is used for PWRs, since they don't have a unique captchaID. The parameter is ignored by the server, but tells the browser to reload the image. - document.getElementById("captcha-img").innerHTML = ` + verify = (callback: () => void) => + _post( + "/captcha/verify/" + + this.code + + "/" + + this.captchaID + + "/" + + this.input.value + + (this.isPWR ? "?pwr=true" : ""), + null, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 204) { + this.checkbox.innerHTML = ``; + this.checkbox.classList.add("~positive"); + this.checkbox.classList.remove("~critical"); + this.verified = true; + } else { + this.checkbox.innerHTML = ``; + this.checkbox.classList.add("~critical"); + this.checkbox.classList.remove("~positive"); + this.verified = false; + } + callback(); + } + }, + ); + + generate = () => + _get("/captcha/gen/" + this.code + (this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200) { + this.captchaID = this.isPWR ? this.code : req.response["id"]; + // the Math.random() appearance below is used for PWRs, since they don't have a unique captchaID. The parameter is ignored by the server, but tells the browser to reload the image. + document.getElementById("captcha-img").innerHTML = ` `; - this.input.value = ""; + this.input.value = ""; + } } - } - }); + }); constructor(code: string, enabled: boolean, reCAPTCHA: boolean, isPWR: boolean) { this.code = code; @@ -69,15 +85,17 @@ export class Captcha { } export interface GreCAPTCHA { - render: (container: HTMLDivElement, parameters: { - sitekey?: string, - theme?: string, - size?: string, - tabindex?: number, - "callback"?: () => void, - "expired-callback"?: () => void, - "error-callback"?: () => void - }) => void; + render: ( + container: HTMLDivElement, + parameters: { + sitekey?: string; + theme?: string; + size?: string; + tabindex?: number; + callback?: () => void; + "expired-callback"?: () => void; + "error-callback"?: () => void; + }, + ) => void; getResponse: (opt_widget_id?: HTMLDivElement) => string; } - diff --git a/ts/modules/common.ts b/ts/modules/common.ts index ffeac57..a0a8303 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -1,6 +1,6 @@ declare var window: GlobalWindow; import dateParser from "any-date-parser"; -import { Temporal } from 'temporal-polyfill'; +import { Temporal } from "temporal-polyfill"; export function toDateString(date: Date): string { const locale = window.language || (window as any).navigator.userLanguage || window.navigator.language; @@ -9,7 +9,7 @@ export function toDateString(date: Date): string { let args1 = {}; let args2: Intl.DateTimeFormatOptions = { hour: "2-digit", - minute: "2-digit" + minute: "2-digit", }; if (t12 && t24) { if (t12.checked) { @@ -29,9 +29,9 @@ export const parseDateString = (value: string): ParsedDate => { // Used just to tell use what fields the user passed. attempt: dateParser.attempt(value), // note Date.fromString is also provided by dateParser. - date: (Date as any).fromString(value) as Date + date: (Date as any).fromString(value) as Date, }; - if (("invalid" in (out.date as any))) { + if ("invalid" in (out.date as any)) { out.invalid = true; } else { // getTimezoneOffset returns UTC - Timezone, so invert it to get distance from UTC -to- timezone. @@ -40,7 +40,7 @@ export const parseDateString = (value: string): ParsedDate => { // Month in Date objects is 0-based, so make our parsed date that way too if ("month" in out.attempt) out.attempt.month -= 1; return out; -} +}; // DateCountdown sets the given el's textContent to the time till the given date (unixSeconds), updating // every minute. It returns the timeout, so it can be later removed with clearTimeout if desired. @@ -53,14 +53,14 @@ export function DateCountdown(el: HTMLElement, unixSeconds: number): ReturnType< let diff = now.until(then).round({ largestUnit: "years", smallestUnit: "minutes", - relativeTo: nowPlain + relativeTo: nowPlain, }); // FIXME: I'd really like this to be localized, but don't know of any nice solutions. const fields = [diff.years, diff.months, diff.days, diff.hours, diff.minutes]; const abbrevs = ["y", "mo", "d", "h", "m"]; for (let i = 0; i < fields.length; i++) { if (fields[i]) { - out += ""+fields[i] + abbrevs[i] + " "; + out += "" + fields[i] + abbrevs[i] + " "; } } return out.slice(0, -1); @@ -72,13 +72,20 @@ export function DateCountdown(el: HTMLElement, unixSeconds: number): ReturnType< return setTimeout(update, 60000); } -export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => { +export const _get = ( + url: string, + data: Object, + onreadystatechange: (req: XMLHttpRequest) => void, + noConnectionError: boolean = false, +): void => { let req = new XMLHttpRequest(); - if (window.pages) { url = window.pages.Base + url; } + if (window.pages) { + url = window.pages.Base + url; + } req.open("GET", url, true); - req.responseType = 'json'; + req.responseType = "json"; req.setRequestHeader("Authorization", "Bearer " + window.token); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); req.onreadystatechange = () => { if (req.status == 0) { if (!noConnectionError) window.notifications.connectionError(); @@ -93,11 +100,13 @@ export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHtt export const _download = (url: string, fname: string): void => { let req = new XMLHttpRequest(); - if (window.pages) { url = window.pages.Base + url; } + if (window.pages) { + url = window.pages.Base + url; + } req.open("GET", url, true); - req.responseType = 'blob'; + req.responseType = "blob"; req.setRequestHeader("Authorization", "Bearer " + window.token); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); req.onload = (e: Event) => { let link = document.createElement("a") as HTMLAnchorElement; link.href = URL.createObjectURL(req.response); @@ -109,25 +118,38 @@ export const _download = (url: string, fname: string): void => { export const _upload = (url: string, formData: FormData): void => { let req = new XMLHttpRequest(); - if (window.pages) { url = window.pages.Base + url; } + if (window.pages) { + url = window.pages.Base + url; + } req.open("POST", url, true); req.setRequestHeader("Authorization", "Bearer " + window.token); // req.setRequestHeader('Content-Type', 'multipart/form-data'); req.send(formData); }; -export const _req = (method: string, url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => { +export const _req = ( + method: string, + url: string, + data: Object, + onreadystatechange: (req: XMLHttpRequest) => void, + response?: boolean, + statusHandler?: (req: XMLHttpRequest) => void, + noConnectionError: boolean = false, +): void => { let req = new XMLHttpRequest(); - if (window.pages) { url = window.pages.Base + url; } + if (window.pages) { + url = window.pages.Base + url; + } req.open(method, url, true); if (response) { - req.responseType = 'json'; + req.responseType = "json"; } req.setRequestHeader("Authorization", "Bearer " + window.token); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); req.onreadystatechange = () => { - if (statusHandler) { statusHandler(req); } - else if (req.status == 0) { + if (statusHandler) { + statusHandler(req); + } else if (req.status == 0) { if (!noConnectionError) window.notifications.connectionError(); return; } else if (req.status == 401) { @@ -138,18 +160,46 @@ export const _req = (method: string, url: string, data: Object, onreadystatechan req.send(JSON.stringify(data)); }; -export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("POST", url, data, onreadystatechange, response, statusHandler, noConnectionError); +export const _post = ( + url: string, + data: Object, + onreadystatechange: (req: XMLHttpRequest) => void, + response?: boolean, + statusHandler?: (req: XMLHttpRequest) => void, + noConnectionError: boolean = false, +): void => _req("POST", url, data, onreadystatechange, response, statusHandler, noConnectionError); -export const _put = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("PUT", url, data, onreadystatechange, response, statusHandler, noConnectionError); +export const _put = ( + url: string, + data: Object, + onreadystatechange: (req: XMLHttpRequest) => void, + response?: boolean, + statusHandler?: (req: XMLHttpRequest) => void, + noConnectionError: boolean = false, +): void => _req("PUT", url, data, onreadystatechange, response, statusHandler, noConnectionError); -export const _patch = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("PATCH", url, data, onreadystatechange, response, statusHandler, noConnectionError); +export const _patch = ( + url: string, + data: Object, + onreadystatechange: (req: XMLHttpRequest) => void, + response?: boolean, + statusHandler?: (req: XMLHttpRequest) => void, + noConnectionError: boolean = false, +): void => _req("PATCH", url, data, onreadystatechange, response, statusHandler, noConnectionError); -export function _delete(url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void { +export function _delete( + url: string, + data: Object, + onreadystatechange: (req: XMLHttpRequest) => void, + noConnectionError: boolean = false, +): void { let req = new XMLHttpRequest(); - if (window.pages) { url = window.pages.Base + url; } + if (window.pages) { + url = window.pages.Base + url; + } req.open("DELETE", url, true); req.setRequestHeader("Authorization", "Bearer " + window.token); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); req.onreadystatechange = () => { if (req.status == 0) { if (!noConnectionError) window.notifications.connectionError(); @@ -162,8 +212,8 @@ export function _delete(url: string, data: Object, onreadystatechange: (req: XML req.send(JSON.stringify(data)); } -export function toClipboard (str: string) { - const el = document.createElement('textarea') as HTMLTextAreaElement; +export function toClipboard(str: string) { + const el = document.createElement("textarea") as HTMLTextAreaElement; el.value = str; el.readOnly = true; el.style.position = "absolute"; @@ -193,45 +243,50 @@ export class notificationBox implements NotificationBox { static baseClasses = ["aside", "flex", "flex-row", "justify-between", "gap-4"]; private _error = (message: string): HTMLElement => { - const noti = document.createElement('aside'); + const noti = document.createElement("aside"); noti.classList.add(...notificationBox.baseClasses, "~critical", "@low", "notification-error"); let error = ""; if (window.lang) { - error = window.lang.strings("error") + ":" + error = window.lang.strings("error") + ":"; } noti.innerHTML = `
${error} ${message}
`; - const closeButton = document.createElement('span') as HTMLSpanElement; + const closeButton = document.createElement("span") as HTMLSpanElement; closeButton.classList.add("button", "~critical", "@low"); closeButton.innerHTML = ``; closeButton.onclick = () => this._close(noti); noti.classList.add("animate-slide-in"); noti.appendChild(closeButton); return noti; - } - + }; + private _positive = (bold: string, message: string): HTMLElement => { - const noti = document.createElement('aside'); + const noti = document.createElement("aside"); noti.classList.add(...notificationBox.baseClasses, "~positive", "@low", "notification-positive"); noti.innerHTML = `
${bold} ${message}
`; - const closeButton = document.createElement('span') as HTMLSpanElement; + const closeButton = document.createElement("span") as HTMLSpanElement; closeButton.classList.add("button", "~positive", "@low"); closeButton.innerHTML = ``; - closeButton.onclick = () => this._close(noti); + closeButton.onclick = () => this._close(noti); noti.classList.add("animate-slide-in"); noti.appendChild(closeButton); return noti; - } - + }; + private _close = (noti: HTMLElement) => { noti.classList.remove("animate-slide-in"); noti.classList.add("animate-slide-out"); - noti.addEventListener(window.animationEvent, () => { - this._box.removeChild(noti); - }, false); - } + noti.addEventListener( + window.animationEvent, + () => { + this._box.removeChild(noti); + }, + false, + ); + }; - - connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); } + connectionError = () => { + this.customError("connectionError", window.lang.notif("errorConnection")); + }; customError = (type: string, message: string) => { this._errorTypes[type] = this._errorTypes[type] || false; @@ -245,9 +300,14 @@ export class notificationBox implements NotificationBox { } this._box.appendChild(noti); this._errorTypes[type] = true; - setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._errorTypes[type] = false; } }, this.timeout*1000); - } - + setTimeout(() => { + if (this._box.contains(noti)) { + this._close(noti); + this._errorTypes[type] = false; + } + }, this.timeout * 1000); + }; + customPositive = (type: string, bold: string, message: string) => { this._positiveTypes[type] = this._positiveTypes[type] || false; const noti = this._positive(bold, message); @@ -260,10 +320,16 @@ export class notificationBox implements NotificationBox { } this._box.appendChild(noti); this._positiveTypes[type] = true; - setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._positiveTypes[type] = false; } }, this.timeout*1000); - } + setTimeout(() => { + if (this._box.contains(noti)) { + this._close(noti); + this._positiveTypes[type] = false; + } + }, this.timeout * 1000); + }; - customSuccess = (type: string, message: string) => this.customPositive(type, window.lang.strings("success") + ":", message) + customSuccess = (type: string, message: string) => + this.customPositive(type, window.lang.strings("success") + ":", message); } export const whichAnimationEvent = () => { @@ -272,19 +338,23 @@ export const whichAnimationEvent = () => { return "animationend"; } return "webkitAnimationEnd"; -} +}; export function toggleLoader(el: HTMLElement, small: boolean = true) { if (el.classList.contains("loader")) { el.classList.remove("loader"); el.classList.remove("loader-sm"); const dot = el.querySelector("span.dot"); - if (dot) { dot.remove(); } + if (dot) { + dot.remove(); + } } else { el.classList.add("loader"); - if (small) { el.classList.add("loader-sm"); } + if (small) { + el.classList.add("loader-sm"); + } const dot = document.createElement("span") as HTMLSpanElement; - dot.classList.add("dot") + dot.classList.add("dot"); el.appendChild(dot); } } @@ -293,9 +363,11 @@ export function addLoader(el: HTMLElement, small: boolean = true, relative: bool if (el.classList.contains("loader")) return; el.classList.add("loader"); if (relative) el.classList.add("rel"); - if (small) { el.classList.add("loader-sm"); } + if (small) { + el.classList.add("loader-sm"); + } const dot = document.createElement("span") as HTMLSpanElement; - dot.classList.add("dot") + dot.classList.add("dot"); el.appendChild(dot); } @@ -305,7 +377,9 @@ export function removeLoader(el: HTMLElement, small: boolean = true) { el.classList.remove("loader-sm"); el.classList.remove("rel"); const dot = el.querySelector("span.dot"); - if (dot) { dot.remove(); } + if (dot) { + dot.remove(); + } } } @@ -329,7 +403,9 @@ export function insertText(textarea: HTMLTextAreaElement, text: string) { } export function bindManualDropdowns() { - const buttons = Array.from(document.getElementsByClassName("dropdown-manual-toggle") as HTMLCollectionOf); + const buttons = Array.from( + document.getElementsByClassName("dropdown-manual-toggle") as HTMLCollectionOf, + ); for (let button of buttons) { const parent = button.closest(".dropdown.manual"); const display = parent.querySelector(".dropdown-display"); @@ -337,7 +413,7 @@ export function bindManualDropdowns() { const mouseout = () => parent.classList.remove("selected"); button.addEventListener("mouseover", mousein); button.addEventListener("mouseout", mouseout); - display.addEventListener("mouseover", mousein); + display.addEventListener("mouseover", mousein); display.addEventListener("mouseout", mouseout); button.onclick = () => { parent.classList.add("selected"); @@ -346,7 +422,12 @@ export function bindManualDropdowns() { display.removeEventListener("mouseout", mouseout); }; const outerClickListener = (event: Event) => { - if (!(event.target instanceof HTMLElement && (display.contains(event.target) || button.contains(event.target)))) { + if ( + !( + event.target instanceof HTMLElement && + (display.contains(event.target) || button.contains(event.target)) + ) + ) { parent.classList.remove("selected"); document.removeEventListener("click", outerClickListener); button.addEventListener("mouseout", mouseout); @@ -372,20 +453,28 @@ export function unicodeB64Encode(s: string): string { // Only allow running a function every n milliseconds. // Source: Clément Prévost at https://stackoverflow.com/questions/27078285/simple-throttle-in-javascript // function foo(bar: T): T { -export function throttle (callback: () => void, limitMilliseconds: number): () => void { - var waiting = false; // Initially, we're not waiting - return function () { // We return a throttled function - if (!waiting) { // If we're not waiting - callback.apply(this, arguments); // Execute users function - waiting = true; // Prevent future invocations - setTimeout(function () { // After a period of time - waiting = false; // And allow future invocations +export function throttle(callback: () => void, limitMilliseconds: number): () => void { + var waiting = false; // Initially, we're not waiting + return function () { + // We return a throttled function + if (!waiting) { + // If we're not waiting + callback.apply(this, arguments); // Execute users function + waiting = true; // Prevent future invocations + setTimeout(function () { + // After a period of time + waiting = false; // And allow future invocations }, limitMilliseconds); } - } + }; } -export function SetupCopyButton(button: HTMLButtonElement, text: string | (() => string), baseClass?: string, notif?: string) { +export function SetupCopyButton( + button: HTMLButtonElement, + text: string | (() => string), + baseClass?: string, + notif?: string, +) { if (!notif) notif = window.lang.strings("copied"); if (!baseClass) baseClass = "~info"; // script will probably turn this into multiple @@ -395,8 +484,8 @@ export function SetupCopyButton(button: HTMLButtonElement, text: string | (() => button.title = window.lang.strings("copy"); const icon = document.createElement("i"); icon.classList.add("icon", "ri-file-copy-line"); - button.appendChild(icon) - button.onclick = () => { + button.appendChild(icon); + button.onclick = () => { if (typeof text === "string") { toClipboard(text); } else { diff --git a/ts/modules/discord.ts b/ts/modules/discord.ts index 84e4812..224ee4a 100644 --- a/ts/modules/discord.ts +++ b/ts/modules/discord.ts @@ -1,4 +1,4 @@ -import {addLoader, removeLoader, _get} from "../modules/common.js"; +import { addLoader, removeLoader, _get } from "../modules/common.js"; declare var window: GlobalWindow; @@ -12,7 +12,12 @@ var listeners: { [buttonText: string]: (event: CustomEvent) => void } = {}; export type DiscordSearch = (passData: string) => void; -export function newDiscordSearch(title: string, description: string, buttonText: string, buttonFunction: (user: DiscordUser, passData: string) => void): DiscordSearch { +export function newDiscordSearch( + title: string, + description: string, + buttonText: string, + buttonFunction: (user: DiscordUser, passData: string) => void, +): DiscordSearch { if (!window.discordEnabled) { return () => {}; } @@ -62,7 +67,7 @@ export function newDiscordSearch(title: string, description: string, buttonText: } }); }, 750); - } + }; return (passData: string) => { const input = document.getElementById("discord-search") as HTMLInputElement; @@ -79,5 +84,5 @@ export function newDiscordSearch(title: string, description: string, buttonText: input.addEventListener("keyup", listeners[buttonText].bind(null, { detail: passData })); window.modals.discord.show(); - } + }; } diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts index df607b5..335bd7c 100644 --- a/ts/modules/invites.ts +++ b/ts/modules/invites.ts @@ -1,6 +1,18 @@ -import { _get, _post, _delete, _patch, toClipboard, toggleLoader, toDateString, SetupCopyButton, addLoader, removeLoader, DateCountdown } from "../modules/common.js"; +import { + _get, + _post, + _delete, + _patch, + toClipboard, + toggleLoader, + toDateString, + SetupCopyButton, + addLoader, + removeLoader, + DateCountdown, +} from "../modules/common.js"; import { DiscordSearch, DiscordUser, newDiscordSearch } from "../modules/discord.js"; -import { reloadProfileNames } from "../modules/profiles.js"; +import { reloadProfileNames } from "../modules/profiles.js"; import { HiddenInputField } from "./ui.js"; declare var window: GlobalWindow; @@ -11,38 +23,45 @@ export const generateCodeLink = (code: string): string => { // let codeLink = window.pages.Base + window.pages.Form + "/" + code; let codeLink = window.pages.ExternalURI + window.pages.Form + "/" + code; return codeLink; -} +}; class DOMInvite implements Invite { updateNotify = (checkbox: HTMLInputElement) => { let state = { code: this.code, notify_expiry: this.notify_expiry, - notify_creation: this.notify_creation + notify_creation: this.notify_creation, }; let revertChanges: () => void; if (checkbox.classList.contains("inv-notify-expiry")) { - revertChanges = () => { this.notify_expiry = !this.notify_expiry }; + revertChanges = () => { + this.notify_expiry = !this.notify_expiry; + }; } else { - revertChanges = () => { this.notify_creation = !this.notify_creation }; + revertChanges = () => { + this.notify_creation = !this.notify_creation; + }; } _patch("/invites/edit", state, (req: XMLHttpRequest) => { if (req.readyState == 4 && !(req.status == 200 || req.status == 204)) { revertChanges(); } }); - } + }; - delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => { - if (req.readyState == 4 && (req.status == 200 || req.status == 204)) { - this.remove(); - const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { detail: this.code }); - document.dispatchEvent(inviteDeletedEvent); - } - }) + delete = () => + _delete("/invites", { code: this.code }, (req: XMLHttpRequest) => { + if (req.readyState == 4 && (req.status == 200 || req.status == 204)) { + this.remove(); + const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { detail: this.code }); + document.dispatchEvent(inviteDeletedEvent); + } + }); private _userLabel: string = ""; - get user_label(): string { return this._userLabel; } + get user_label(): string { + return this._userLabel; + } set user_label(label: string) { this._userLabel = label; const labelLabel = this._middle.querySelector(".user-label-label"); @@ -59,7 +78,9 @@ class DOMInvite implements Invite { } private _label: string = ""; - get label(): string { return this._label; } + get label(): string { + return this._label; + } set label(label: string) { this._label = label; if (label == "") { @@ -70,12 +91,14 @@ class DOMInvite implements Invite { } private _code: string = "None"; - get code(): string { return this._code; } + get code(): string { + return this._code; + } set code(code: string) { this._code = code; this._codeLink = generateCodeLink(code); if (this.label == "") { - this._labelEditor.value = code.replace(/-/g, '-'); + this._labelEditor.value = code.replace(/-/g, "-"); } this._linkEl.href = this._codeLink; } @@ -83,7 +106,9 @@ class DOMInvite implements Invite { private _validTill: number; private _validTillUpdater: ReturnType = null; - get valid_till(): number { return this._validTill; } + get valid_till(): number { + return this._validTill; + } set valid_till(v: number) { this._validTill = v; if (this._validTillUpdater) clearTimeout(this._validTillUpdater); @@ -91,14 +116,26 @@ class DOMInvite implements Invite { } private _userExpiryEnabled: boolean; - get user_expiry(): boolean { return this._userExpiryEnabled; } - set user_expiry(v: boolean) { this._userExpiryEnabled = v; } + get user_expiry(): boolean { + return this._userExpiryEnabled; + } + set user_expiry(v: boolean) { + this._userExpiryEnabled = v; + } private _userExpiry = { months: 0, days: 0, hours: 0, minutes: 0 }; private _userExpiryString: string; - get user_months(): number { return this._userExpiry.months; } - get user_days(): number { return this._userExpiry.days; } - get user_hours(): number { return this._userExpiry.hours; } - get user_minutes(): number { return this._userExpiry.minutes; } + get user_months(): number { + return this._userExpiry.months; + } + get user_days(): number { + return this._userExpiry.days; + } + get user_hours(): number { + return this._userExpiry.hours; + } + get user_minutes(): number { + return this._userExpiry.minutes; + } set user_months(v: number) { this._userExpiry.months = v; this._updateUserExpiry(); @@ -115,9 +152,9 @@ class DOMInvite implements Invite { this._userExpiry.minutes = v; this._updateUserExpiry(); } - set user_expiry_time(v: { months: number, days: number, hours: number, minutes: number }) { + set user_expiry_time(v: { months: number; days: number; hours: number; minutes: number }) { this._userExpiry = v; - this._updateUserExpiry() + this._updateUserExpiry(); } private _updateUserExpiry() { const expiry = this._middle.querySelector("span.user-expiry") as HTMLSpanElement; @@ -132,34 +169,40 @@ class DOMInvite implements Invite { const abbrevs = ["mo", "d", "h", "m"]; for (let i = 0; i < fields.length; i++) { if (fields[i]) { - this._userExpiryString += ""+fields[i] + abbrevs[i] + " "; + this._userExpiryString += "" + fields[i] + abbrevs[i] + " "; } } this._userExpiryString = this._userExpiryString.slice(0, -1); } this._middle.querySelector("strong.user-expiry-time").textContent = this._userExpiryString; } - + private _noLimit: boolean = false; - get no_limit(): boolean { return this._noLimit; } + get no_limit(): boolean { + return this._noLimit; + } set no_limit(v: boolean) { this._noLimit = v; const remaining = this._middle.querySelector("strong.inv-remaining") as HTMLElement; - if (!this.no_limit) remaining.textContent = ""+this._remainingUses; + if (!this.no_limit) remaining.textContent = "" + this._remainingUses; else remaining.textContent = INF; } private _remainingUses: number = 1; - get remaining_uses(): number { return this._remainingUses; } + get remaining_uses(): number { + return this._remainingUses; + } set remaining_uses(v: number) { this._remainingUses = v; const remaining = this._middle.querySelector("strong.inv-remaining") as HTMLElement; - if (!this.no_limit) remaining.textContent = ""+this._remainingUses; + if (!this.no_limit) remaining.textContent = "" + this._remainingUses; else remaining.textContent = INF; } private _send_to: string = ""; - get send_to(): string { return this._send_to }; + get send_to(): string { + return this._send_to; + } set send_to(address: string | null) { this._send_to = address; const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement; @@ -191,44 +234,53 @@ class DOMInvite implements Invite { // innerHTML as the newer sent_to re-uses this with HTML. tooltip.innerHTML = address; } - private _sendToDialog: SendToDialog; + private _sendToDialog: SendToDialog; private _sent_to: SentToList; - get sent_to(): SentToList { return this._sent_to; } + get sent_to(): SentToList { + return this._sent_to; + } set sent_to(v: SentToList) { this._sent_to = v; if (!v || !(v.success || v.failed)) return; let text = ""; if (v.success && v.success.length > 0) { - text += window.lang.strings("sentTo") + ": " + v.success.join(", ") + "
" + text += window.lang.strings("sentTo") + ": " + v.success.join(", ") + "
"; } if (v.failed && v.failed.length > 0) { - text += window.lang.strings("failed") + ": " + v.failed.map((el: SendFailure) => { - let err: string; - switch (el.reason) { - case "CheckLogs": - err = window.lang.notif("errorCheckLogs"); - break; - case "NoUser": - err = window.lang.notif("errorNoUser"); - break; - case "MultiUser": - err = window.lang.notif("errorMultiUser"); - break; - case "InvalidAddress": - err = window.lang.notif("errorInvalidAddress"); - break; - default: - err = el.reason; - break; - } - return el.address + " (" + err + ")"; - }).join(", "); + text += + window.lang.strings("failed") + + ": " + + v.failed + .map((el: SendFailure) => { + let err: string; + switch (el.reason) { + case "CheckLogs": + err = window.lang.notif("errorCheckLogs"); + break; + case "NoUser": + err = window.lang.notif("errorNoUser"); + break; + case "MultiUser": + err = window.lang.notif("errorMultiUser"); + break; + case "InvalidAddress": + err = window.lang.notif("errorInvalidAddress"); + break; + default: + err = el.reason; + break; + } + return el.address + " (" + err + ")"; + }) + .join(", "); } if (text.length != 0) this.send_to = text; } private _usedBy: { [name: string]: number }; - get used_by(): { [name: string]: number } { return this._usedBy; } + get used_by(): { [name: string]: number } { + return this._usedBy; + } set used_by(uB: { [name: string]: number } | null) { this._usedBy = uB; if (!uB || Object.keys(uB).length == 0) { @@ -265,45 +317,55 @@ class DOMInvite implements Invite { } private _createdUnix: number; - get created(): number { return this._createdUnix; } + get created(): number { + return this._createdUnix; + } set created(unix: number) { this._createdUnix = unix; const el = this._middle.querySelector("strong.inv-created"); if (unix == 0) { el.textContent = window.lang.strings("unknown"); } else { - el.textContent = toDateString(new Date(unix*1000)); + el.textContent = toDateString(new Date(unix * 1000)); } } - + private _notifyExpiry: boolean = false; - get notify_expiry(): boolean { return this._notifyExpiry } + get notify_expiry(): boolean { + return this._notifyExpiry; + } set notify_expiry(state: boolean) { this._notifyExpiry = state; (this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement).checked = state; } private _notifyCreation: boolean = false; - get notify_creation(): boolean { return this._notifyCreation } + get notify_creation(): boolean { + return this._notifyCreation; + } set notify_creation(state: boolean) { this._notifyCreation = state; (this._left.querySelector("input.inv-notify-creation") as HTMLInputElement).checked = state; } private _profile: string; - get profile(): string { return this._profile; } - set profile(profile: string) { this.loadProfiles(profile); } + get profile(): string { + return this._profile; + } + set profile(profile: string) { + this.loadProfiles(profile); + } loadProfiles = (selected?: string) => { const select = this._left.querySelector("select") as HTMLSelectElement; let noProfile = false; if (selected === "") { - noProfile = true; + noProfile = true; } else { selected = selected || select.value; } let innerHTML = ``; for (let profile of window.availableProfiles) { - innerHTML += ``; + innerHTML += ``; } select.innerHTML = innerHTML; this._profile = selected; @@ -312,8 +374,10 @@ class DOMInvite implements Invite { const select = this._left.querySelector("select") as HTMLSelectElement; const previous = this.profile; let profile = select.value; - let state = {code: this.code}; - if (profile != "noProfile") { state["profile"] = profile }; + let state = { code: this.code }; + if (profile != "noProfile") { + state["profile"] = profile; + } _patch("/invites/edit", state, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (!(req.status == 200 || req.status == 204)) { @@ -323,7 +387,7 @@ class DOMInvite implements Invite { } } }); - } + }; private _setLabel = () => { const newLabel = this._labelEditor.value.trim(); @@ -331,7 +395,7 @@ class DOMInvite implements Invite { this.label = newLabel; let state = { code: this.code, - label: newLabel + label: newLabel, }; _patch("/invites/edit", state, (req: XMLHttpRequest) => { if (req.readyState != 4) return; @@ -339,7 +403,7 @@ class DOMInvite implements Invite { this.label = old; } }); - } + }; private _container: HTMLDivElement; @@ -360,7 +424,9 @@ class DOMInvite implements Invite { private _detailsToggle: HTMLInputElement; private _gap: number; - get gap(): number { return this._gap; } + get gap(): number { + return this._gap; + } set gap(v: number) { // Do it this way to ensure the class is included by tailwind let gapClass: string; @@ -378,10 +444,10 @@ class DOMInvite implements Invite { gapClass = "gap-4"; break; default: - gapClass = "gap-"+v; + gapClass = "gap-" + v; break; } - this._container.classList.remove("gap-"+this._gap); + this._container.classList.remove("gap-" + this._gap); this._container.classList.add(gapClass); this._gap = v; } @@ -395,13 +461,14 @@ class DOMInvite implements Invite { if (state) { this._detailsToggle.previousElementSibling.classList.add("rotated"); this._detailsToggle.previousElementSibling.classList.remove("not-rotated"); - + const mainTransitionStart = () => { this._details.removeEventListener("transitionend", mainTransitionStart); this._details.style.transitionDuration = ""; this._details.addEventListener("transitionend", mainTransitionEnd); this._details.style.opacity = "100%"; - this._details.style.maxHeight = "calc(" + (1*this._details.scrollHeight)+"px" + " + " + (0.125 * 8 * this.gap) + "rem)"; // Compensate for the margin and padding (ugly) + this._details.style.maxHeight = + "calc(" + 1 * this._details.scrollHeight + "px" + " + " + 0.125 * 8 * this.gap + "rem)"; // Compensate for the margin and padding (ugly) this._details.style.marginTop = "0"; this._details.style.marginBottom = "0"; this._details.style.paddingTop = ""; @@ -416,8 +483,8 @@ class DOMInvite implements Invite { this._details.style.transitionDuration = "1ms"; // Add negative y margin to cancel out "gap-x" when we unhide (and are initially height: 0) // perhaps not great assuming --spacing == 0.25rem - this._details.style.marginTop = (-0.125 * this.gap)+"rem"; - this._details.style.marginBottom = (-0.125 * this.gap)+"rem"; + this._details.style.marginTop = -0.125 * this.gap + "rem"; + this._details.style.marginBottom = -0.125 * this.gap + "rem"; this._details.style.paddingTop = "0"; this._details.style.paddingBottom = "0"; mainTransitionStart(); @@ -443,12 +510,12 @@ class DOMInvite implements Invite { this._details.style.opacity = "0"; // Add negative y margin to cancel out "gap-x" when we finish hiding (and end up height:0) // perhaps not great assuming --spacing == 0.25rem - this._details.style.marginTop = (-0.125 * this.gap)+"rem"; - this._details.style.marginBottom = (-0.125 * this.gap)+"rem"; + this._details.style.marginTop = -0.125 * this.gap + "rem"; + this._details.style.marginBottom = -0.125 * this.gap + "rem"; }; this._details.style.transitionDuration = "1ms"; this._details.addEventListener("transitionend", mainTransitionStart); - this._details.style.maxHeight = (1*this._details.scrollHeight)+"px"; + this._details.style.maxHeight = 1 * this._details.scrollHeight + "px"; } } @@ -457,7 +524,7 @@ class DOMInvite implements Invite { if (state) { this._detailsToggle.previousElementSibling.classList.add("rotated"); this._detailsToggle.previousElementSibling.classList.remove("not-rotated"); - + this._details.classList.remove("unfocused"); this._details.classList.add("focused"); this._details.style.maxHeight = "9999px"; @@ -476,25 +543,45 @@ class DOMInvite implements Invite { constructor(invite: Invite) { // first create the invite structure, then use our setter methods to fill in the data. - this._container = document.createElement('div') as HTMLDivElement; + this._container = document.createElement("div") as HTMLDivElement; this._container.classList.add("inv", "overflow-visible", "flex", "flex-col"); - + // Stores gap-x so we can cancel it out for transitions this.gap = 2; - this._header = document.createElement('div') as HTMLDivElement; + this._header = document.createElement("div") as HTMLDivElement; this._container.appendChild(this._header); - this._header.classList.add("card", "dark:~d_neutral", "@low", "inv-header", "flex", "flex-row", "justify-between", "overflow-visible", "gap-2", "z-[1]"); + this._header.classList.add( + "card", + "dark:~d_neutral", + "@low", + "inv-header", + "flex", + "flex-row", + "justify-between", + "overflow-visible", + "gap-2", + "z-[1]", + ); - this._codeArea = document.createElement('div') as HTMLDivElement; + this._codeArea = document.createElement("div") as HTMLDivElement; this._header.appendChild(this._codeArea); - this._codeArea.classList.add("flex", "flex-row", "flex-wrap", "justify-between", "w-full", "items-center", "gap-2", "truncate"); + this._codeArea.classList.add( + "flex", + "flex-row", + "flex-wrap", + "justify-between", + "w-full", + "items-center", + "gap-2", + "truncate", + ); this._codeArea.innerHTML = `
- ${window.lang.var("strings", "inviteExpiresInTime", "")} + ${window.lang.var("strings", "inviteExpiresInTime", '')} `; this._linkContainer = this._codeArea.getElementsByClassName("invite-link-container")[0] as HTMLElement; @@ -503,14 +590,16 @@ class DOMInvite implements Invite { buttonOnLeft: false, customContainerHTML: ``, clickAwayShouldSave: true, - onSet: this._setLabel + onSet: this._setLabel, }); this._linkEl = this._linkContainer.getElementsByClassName("invite-link")[0] as HTMLAnchorElement; const copyButton = this._codeArea.getElementsByClassName("invite-copy-button")[0] as HTMLButtonElement; - SetupCopyButton(copyButton, () => { return this._codeLink; }); + SetupCopyButton(copyButton, () => { + return this._codeLink; + }); - this._infoArea = document.createElement('div') as HTMLDivElement; + this._infoArea = document.createElement("div") as HTMLDivElement; this._header.appendChild(this._infoArea); this._infoArea.classList.add("inv-infoarea", "flex", "flex-row", "items-center", "gap-2"); this._infoArea.innerHTML = ` @@ -524,32 +613,39 @@ class DOMInvite implements Invite { `; - + (this._infoArea.querySelector(".inv-delete") as HTMLSpanElement).onclick = this.delete; - this._detailsToggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement); + this._detailsToggle = this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement; this._detailsToggle.onclick = () => { this.expanded = this.expanded; }; - const toggleDetails = (event: Event) => { + const toggleDetails = (event: Event) => { if (event.target == this._header || event.target == this._codeArea || event.target == this._infoArea) { - this.expanded = !this.expanded; + this.expanded = !this.expanded; } }; this._header.onclick = toggleDetails; - - this._details = document.createElement('div') as HTMLDivElement; + this._details = document.createElement("div") as HTMLDivElement; this._container.appendChild(this._details); this._details.classList.add("card", "~neutral", "@low", "inv-details", "transition-all", "unfocused"); this._details.style.maxHeight = "0"; this._details.style.opacity = "0"; - const detailsInner = document.createElement('div') as HTMLDivElement; + const detailsInner = document.createElement("div") as HTMLDivElement; this._details.appendChild(detailsInner); detailsInner.classList.add("inv-row", "flex", "flex-row", "flex-wrap", "justify-between", "gap-4"); - this._left = document.createElement('div') as HTMLDivElement; - this._left.classList.add("flex", "flex-row", "flex-wrap", "gap-4", "min-w-full", "sm:min-w-fit", "whitespace-nowrap"); + this._left = document.createElement("div") as HTMLDivElement; + this._left.classList.add( + "flex", + "flex-row", + "flex-wrap", + "gap-4", + "min-w-full", + "sm:min-w-fit", + "whitespace-nowrap", + ); detailsInner.appendChild(this._left); const leftLeft = document.createElement("div") as HTMLDivElement; this._left.appendChild(leftLeft); @@ -581,16 +677,22 @@ class DOMInvite implements Invite { } leftLeft.innerHTML = innerHTML; (this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile; - + if (window.notificationsEnabled) { const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement; - notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); }; + notifyExpiry.onchange = () => { + this._notifyExpiry = notifyExpiry.checked; + this.updateNotify(notifyExpiry); + }; const notifyCreation = this._left.querySelector("input.inv-notify-creation") as HTMLInputElement; - notifyCreation.onchange = () => { this._notifyCreation = notifyCreation.checked; this.updateNotify(notifyCreation); }; + notifyCreation.onchange = () => { + this._notifyCreation = notifyCreation.checked; + this.updateNotify(notifyCreation); + }; } - this._middle = document.createElement('div') as HTMLDivElement; + this._middle = document.createElement("div") as HTMLDivElement; this._left.appendChild(this._middle); this._middle.classList.add("flex", "flex-col", "grow", "gap-4"); this._middle.innerHTML = ` @@ -601,18 +703,32 @@ class DOMInvite implements Invite {
`; - this._right = document.createElement('div') as HTMLDivElement; + this._right = document.createElement("div") as HTMLDivElement; detailsInner.appendChild(this._right); - this._right.classList.add("card", "~neutral", "@low", "inv-created-users", "min-w-full", "sm:min-w-fit", "whitespace-nowrap"); + this._right.classList.add( + "card", + "~neutral", + "@low", + "inv-created-users", + "min-w-full", + "sm:min-w-fit", + "whitespace-nowrap", + ); this._right.innerHTML = `${window.lang.strings("inviteUsersCreated")}`; - this._userTable = document.createElement('div') as HTMLDivElement; - this._userTable.classList.add("text-sm", "mt-1", ); + this._userTable = document.createElement("div") as HTMLDivElement; + this._userTable.classList.add("text-sm", "mt-1"); this._right.appendChild(this._userTable); this.setExpandedWithoutAnimation(false); this.update(invite); - document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); + document.addEventListener( + "profileLoadEvent", + () => { + this.loadProfiles(); + }, + false, + ); document.addEventListener("timefmt-change", () => { this.created = this.created; this.used_by = this.used_by; @@ -628,7 +744,7 @@ class DOMInvite implements Invite { months: invite.user_months, days: invite.user_days, hours: invite.user_hours, - minutes: invite.user_minutes + minutes: invite.user_minutes, }; } this.created = invite.created; @@ -648,15 +764,23 @@ class DOMInvite implements Invite { if (invite.user_label) { this.user_label = invite.user_label; } - this._sendToDialog = new SendToDialog(this._middle.getElementsByClassName("invite-send-to-dialog")[0] as HTMLElement, invite, () => { - const needsUpdatingEvent = new CustomEvent("inviteNeedsUpdating", { detail: this.code }); - document.dispatchEvent(needsUpdatingEvent); - }); - } + this._sendToDialog = new SendToDialog( + this._middle.getElementsByClassName("invite-send-to-dialog")[0] as HTMLElement, + invite, + () => { + const needsUpdatingEvent = new CustomEvent("inviteNeedsUpdating", { detail: this.code }); + document.dispatchEvent(needsUpdatingEvent); + }, + ); + }; - asElement = (): HTMLDivElement => { return this._container; } + asElement = (): HTMLDivElement => { + return this._container; + }; - remove = () => { this._container.remove(); } + remove = () => { + this._container.remove(); + }; } export class DOMInviteList implements InviteList { @@ -675,43 +799,62 @@ export class DOMInviteList implements InviteList { }; public static readonly _inviteURLEvent = "invite-url"; - registerURLListener = () => document.addEventListener(DOMInviteList._inviteURLEvent, (event: CustomEvent) => { - this.focusInvite(event.detail); - }) + registerURLListener = () => + document.addEventListener(DOMInviteList._inviteURLEvent, (event: CustomEvent) => { + this.focusInvite(event.detail); + }); isInviteURL = () => { const urlParams = new URLSearchParams(window.location.search); const inviteCode = urlParams.get("invite"); return Boolean(inviteCode); - } + }; loadInviteURL = () => { const urlParams = new URLSearchParams(window.location.search); const inviteCode = urlParams.get("invite"); this.focusInvite(inviteCode, window.lang.notif("errorInviteNotFound")); - } + }; constructor() { - this._list = document.getElementById('invites') as HTMLDivElement; + this._list = document.getElementById("invites") as HTMLDivElement; this.empty = true; this.invites = {}; // FIXME: Do this better, take advantage of getting the code in e.detail - document.addEventListener("inviteNeedsUpdating", () => { this.reload(); }, false); + document.addEventListener( + "inviteNeedsUpdating", + () => { + this.reload(); + }, + false, + ); - document.addEventListener("newInviteEvent", () => { this.reload(); }, false); - document.addEventListener("inviteDeletedEvent", (event: CustomEvent) => { - const code = event.detail; - const length = Object.keys(this.invites).length - 1; // store prior as Object.keys is undefined when there are no keys - delete this.invites[code]; - if (length == 0) { - this.empty = true; - } - }, false); + document.addEventListener( + "newInviteEvent", + () => { + this.reload(); + }, + false, + ); + document.addEventListener( + "inviteDeletedEvent", + (event: CustomEvent) => { + const code = event.detail; + const length = Object.keys(this.invites).length - 1; // store prior as Object.keys is undefined when there are no keys + delete this.invites[code]; + if (length == 0) { + this.empty = true; + } + }, + false, + ); this.registerURLListener(); } - get empty(): boolean { return this._empty; } + get empty(): boolean { + return this._empty; + } set empty(state: boolean) { this._empty = state; if (state) { @@ -729,7 +872,7 @@ export class DOMInviteList implements InviteList { } else { this._list.classList.remove("empty"); if (this._list.querySelector(".inv-empty")) { - this._list.textContent = ''; + this._list.textContent = ""; } } } @@ -737,48 +880,57 @@ export class DOMInviteList implements InviteList { add = (invite: Invite) => { let domInv = new DOMInvite(invite); this.invites[invite.code] = domInv; - if (this.empty) { this.empty = false; } - this._list.appendChild(domInv.asElement()); - } - - reload = (callback?: () => void) => reloadProfileNames(() => _get("/invites", null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - let data = req.response; - if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) { - this.empty = true; - return; - } - // get a list of all current inv codes on dom - // every time we find a match in resp, delete from list - // at end delete all remaining in list from dom - let invitesOnDOM: { [code: string]: boolean } = {}; - for (let code in this.invites) { invitesOnDOM[code] = true; } - for (let invite of (data["invites"] as Array)) { - if (invite.code in this.invites) { - this.invites[invite.code].update(invite); - delete invitesOnDOM[invite.code]; - } else { - this.add(invite); - } - } - for (let code in invitesOnDOM) { - this.invites[code].remove(); - delete this.invites[code]; - } - - if (callback) callback(); + if (this.empty) { + this.empty = false; } - })); + this._list.appendChild(domInv.asElement()); + }; + + reload = (callback?: () => void) => + reloadProfileNames(() => + _get("/invites", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + let data = req.response; + if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) { + this.empty = true; + return; + } + // get a list of all current inv codes on dom + // every time we find a match in resp, delete from list + // at end delete all remaining in list from dom + let invitesOnDOM: { [code: string]: boolean } = {}; + for (let code in this.invites) { + invitesOnDOM[code] = true; + } + for (let invite of data["invites"] as Array) { + if (invite.code in this.invites) { + this.invites[invite.code].update(invite); + delete invitesOnDOM[invite.code]; + } else { + this.add(invite); + } + } + for (let code in invitesOnDOM) { + this.invites[code].remove(); + delete this.invites[code]; + } + + if (callback) callback(); + } + }), + ); } -export const inviteURLEvent = (id: string) => { return new CustomEvent(DOMInviteList._inviteURLEvent, {"detail": id}) }; +export const inviteURLEvent = (id: string) => { + return new CustomEvent(DOMInviteList._inviteURLEvent, { detail: id }); +}; export class createInvite { private _sendTo: SendToDialog; private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement; - private _uses = document.getElementById('create-uses') as HTMLInputElement; + private _uses = document.getElementById("create-uses") as HTMLInputElement; private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement; - private _infUsesWarning = document.getElementById('create-inf-uses-warning') as HTMLParagraphElement; + private _infUsesWarning = document.getElementById("create-inf-uses-warning") as HTMLParagraphElement; private _createButton = document.getElementById("create-submit") as HTMLSpanElement; private _profile = document.getElementById("create-profile") as HTMLSelectElement; private _label = document.getElementById("create-label") as HTMLInputElement; @@ -793,10 +945,10 @@ export class createInvite { private _userHours = document.getElementById("user-hours") as HTMLSelectElement; private _userMinutes = document.getElementById("user-minutes") as HTMLSelectElement; - private _invDurationButton = document.getElementById('radio-inv-duration') as HTMLInputElement; - private _userExpiryButton = document.getElementById('radio-user-expiry') as HTMLInputElement; - private _invDuration = document.getElementById('inv-duration'); - private _userExpiry = document.getElementById('user-expiry'); + private _invDurationButton = document.getElementById("radio-inv-duration") as HTMLInputElement; + private _userExpiryButton = document.getElementById("radio-user-expiry") as HTMLInputElement; + private _invDuration = document.getElementById("inv-duration"); + private _userExpiry = document.getElementById("user-expiry"); private _sendToDiscord: (passData: string) => void; @@ -809,24 +961,32 @@ export class createInvite { const fieldIDs = ["months", "days", "hours", "minutes"]; const prefixes = ["create-", "user-"]; for (let i = 0; i < fieldIDs.length; i++) { - for (let j = 0; j < prefixes.length; j++) { + for (let j = 0; j < prefixes.length; j++) { const field = document.getElementById(prefixes[j] + fieldIDs[i]); - field.textContent = ''; + field.textContent = ""; for (let n = 0; n <= this._count; n++) { - const opt = document.createElement("option") as HTMLOptionElement; - opt.textContent = ""+n; - opt.value = ""+n; - field.appendChild(opt); + const opt = document.createElement("option") as HTMLOptionElement; + opt.textContent = "" + n; + opt.value = "" + n; + field.appendChild(opt); } } } + }; + + get label(): string { + return this._label.value; + } + set label(label: string) { + this._label.value = label; } - get label(): string { return this._label.value; } - set label(label: string) { this._label.value = label; } - - get user_label(): string { return this._userLabel.value; } - set user_label(label: string) { this._userLabel.value = label; } + get user_label(): string { + return this._userLabel.value; + } + set user_label(label: string) { + this._userLabel.value = label; + } get infiniteUses(): boolean { return this._infUses.checked; @@ -844,9 +1004,13 @@ export class createInvite { this._infUsesWarning.classList.add("unfocused"); } } - - get uses(): number { return this._uses.valueAsNumber; } - set uses(n: number) { this._uses.valueAsNumber = n; } + + get uses(): number { + return this._uses.valueAsNumber; + } + set uses(n: number) { + this._uses.valueAsNumber = n; + } private _checkDurationValidity = () => { if (this.months + this.days + this.hours + this.minutes == 0) { @@ -856,34 +1020,34 @@ export class createInvite { this._createButton.removeAttribute("disabled"); this._createButton.onclick = this.create; } - } + }; get months(): number { return +this._months.value; } set months(n: number) { - this._months.value = ""+n; + this._months.value = "" + n; this._checkDurationValidity(); } get days(): number { return +this._days.value; } set days(n: number) { - this._days.value = ""+n; + this._days.value = "" + n; this._checkDurationValidity(); } get hours(): number { return +this._hours.value; } set hours(n: number) { - this._hours.value = ""+n; + this._hours.value = "" + n; this._checkDurationValidity(); } get minutes(): number { return +this._minutes.value; } set minutes(n: number) { - this._minutes.value = ""+n; + this._minutes.value = "" + n; this._checkDurationValidity(); } get userExpiry(): boolean { @@ -908,36 +1072,40 @@ export class createInvite { return +this._userMonths.value; } set userMonths(n: number) { - this._userMonths.value = ""+n; + this._userMonths.value = "" + n; } get userDays(): number { return +this._userDays.value; } set userDays(n: number) { - this._userDays.value = ""+n; + this._userDays.value = "" + n; } get userHours(): number { return +this._userHours.value; } set userHours(n: number) { - this._userHours.value = ""+n; + this._userHours.value = "" + n; } get userMinutes(): number { return +this._userMinutes.value; } set userMinutes(n: number) { - this._userMinutes.value = ""+n; + this._userMinutes.value = "" + n; } get sendTo(): string { - if (!(this._sendTo)) return ""; - if (this._sendTo.addresses.length > 1) console.error("FIXME: SendToDialog has collected more than one address, make them usable or fix it!"); + if (!this._sendTo) return ""; + if (this._sendTo.addresses.length > 1) + console.error("FIXME: SendToDialog has collected more than one address, make them usable or fix it!"); if (this._sendTo.addresses.length > 0) return this._sendTo.addresses[0]; else return ""; } - set sendTo(address: string) { if (!(this._sendTo)) return; this._sendTo.addresses = [address]; } + set sendTo(address: string) { + if (!this._sendTo) return; + this._sendTo.addresses = [address]; + } - get profile(): string { + get profile(): string { const val = this._profile.value; if (val == "noProfile") { return ""; @@ -945,7 +1113,9 @@ export class createInvite { return val; } set profile(p: string) { - if (p == "") { p = "noProfile"; } + if (p == "") { + p = "noProfile"; + } this._profile.value = p; } @@ -962,7 +1132,7 @@ export class createInvite { } else { this.profile = selected; } - } + }; create = () => { toggleLoader(this._createButton); @@ -971,22 +1141,22 @@ export class createInvite { userExpiry = false; } let send = { - "months": this.months, - "days": this.days, - "hours": this.hours, - "minutes": this.minutes, + months: this.months, + days: this.days, + hours: this.hours, + minutes: this.minutes, "user-expiry": userExpiry, "user-months": this.userMonths, "user-days": this.userDays, "user-hours": this.userHours, "user-minutes": this.userMinutes, - "multiple-uses": (this.uses > 1 || this.infiniteUses), + "multiple-uses": this.uses > 1 || this.infiniteUses, "no-limit": this.infiniteUses, "remaining-uses": this.uses, "send-to": this.sendTo, - "profile": this.profile, - "label": this.label, - "user_label": this.user_label + profile: this.profile, + label: this.label, + user_label: this.user_label, }; _post("/invites", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { @@ -996,7 +1166,7 @@ export class createInvite { toggleLoader(this._createButton); } }); - } + }; constructor() { this._populateNumbers(); @@ -1004,10 +1174,14 @@ export class createInvite { this.days = 0; this.hours = 0; this.minutes = 30; - this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; }; + this._infUses.onchange = () => { + this.infiniteUses = this.infiniteUses; + }; this.infiniteUses = false; this.userExpiry = false; - this._userExpiryToggle.onchange = () => { this.userExpiry = this._userExpiryToggle.checked; } + this._userExpiryToggle.onchange = () => { + this.userExpiry = this._userExpiryToggle.checked; + }; this._userMonths.disabled = true; this._userDays.disabled = true; this._userHours.disabled = true; @@ -1046,12 +1220,18 @@ export class createInvite { this._months.onchange = this._checkDurationValidity; this._hours.onchange = this._checkDurationValidity; this._minutes.onchange = this._checkDurationValidity; - document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); + document.addEventListener( + "profileLoadEvent", + () => { + this.loadProfiles(); + }, + false, + ); const sendToContainer = document.getElementById("create-send-to-container"); if (window.emailEnabled || window.discordEnabled) { this._sendTo = new SendToDialog(sendToContainer); - } else { + } else { sendToContainer.classList.add("unfocused"); } } @@ -1070,12 +1250,11 @@ class SendToDialog { set addresses(v: string[]) { if (v.length > 0) this._input.value = v[0]; // this._addresses = v; - }; + } private _search?: HTMLButtonElement; private _discordSearch?: DiscordSearch; - constructor(container: HTMLElement, invite?: Invite, onSuccess?: () => void) { this._container = container; this._container.classList.add("flex", "flex-col", "gap-2"); @@ -1106,7 +1285,7 @@ class SendToDialog { // this.addresses.push(user.name); window.modals.discord.close(); - } + }, ); // FIXME: Check why we're passing an empty string rather than the input value this._search.onclick = () => this._discordSearch(""); @@ -1123,9 +1302,9 @@ class SendToDialog { const icon = this._submit.children[0] as HTMLElement; addLoader(icon, true); if (this.addresses.length == 0) return; - _post("/invites/send", {"invite": invite.code, "send-to": this.addresses[0]}, (req: XMLHttpRequest) => { + _post("/invites/send", { invite: invite.code, "send-to": this.addresses[0] }, (req: XMLHttpRequest) => { if (req.readyState != 4) return; - removeLoader(icon, true) + removeLoader(icon, true); if (req.status != 200 && req.status != 204) { window.notifications.customError("errorSendInvite", window.lang.notif("errorFailureCheckLogs")); return; @@ -1140,7 +1319,7 @@ class SendToDialog { e.preventDefault(); this._submit.click(); } - }) + }); } } } diff --git a/ts/modules/lang.ts b/ts/modules/lang.ts index 625f7f4..989b1f3 100644 --- a/ts/modules/lang.ts +++ b/ts/modules/lang.ts @@ -24,37 +24,45 @@ export class lang implements Lang { } get = (sect: string, key: string): string => { - if (sect == "quantityStrings" || sect == "meta") { return ""; } + if (sect == "quantityStrings" || sect == "meta") { + return ""; + } return this._lang[sect][key]; - } + }; - strings = (key: string): string => this.get("strings", key) - notif = (key: string): string => this.get("notifications", key) + strings = (key: string): string => this.get("strings", key); + notif = (key: string): string => this.get("notifications", key); var = (sect: string, key: string, ...subs: string[]): string => { - if (sect == "quantityStrings" || sect == "meta") { return ""; } + if (sect == "quantityStrings" || sect == "meta") { + return ""; + } let str = this._lang[sect][key]; for (let sub of subs) { str = str.replace("{n}", sub); } return str; - } + }; template = (sect: string, key: string, subs: { [key: string]: any }): string => { - if (sect == "quantityStrings" || sect == "meta") { return ""; } + if (sect == "quantityStrings" || sect == "meta") { + return ""; + } const map = new Map(); - for (let key of Object.keys(subs)) { map.set(key, subs[key]); } + for (let key of Object.keys(subs)) { + map.set(key, subs[key]); + } const [out, err] = Template(this._lang[sect][key], map); if (err != null) throw err; return out; - } + }; quantity = (key: string, number: number): string => { if (number == 1) { - return this._lang.quantityStrings[key].singular.replace("{n}", ""+number) + return this._lang.quantityStrings[key].singular.replace("{n}", "" + number); } - return this._lang.quantityStrings[key].plural.replace("{n}", ""+number); - } + return this._lang.quantityStrings[key].plural.replace("{n}", "" + number); + }; } export var TimeFmtChange = new CustomEvent("timefmt-change"); @@ -66,7 +74,7 @@ export const loadLangSelector = (page: string) => { localStorage.setItem("timefmt", fmt); }; const t12 = document.getElementById("lang-12h") as HTMLInputElement; - if (typeof(t12) !== "undefined" && t12 != null) { + if (typeof t12 !== "undefined" && t12 != null) { t12.onchange = () => setTimefmt("12h"); const t24 = document.getElementById("lang-24h") as HTMLInputElement; t24.onchange = () => setTimefmt("24h"); @@ -83,21 +91,26 @@ export const loadLangSelector = (page: string) => { } let queryString = new URLSearchParams(window.location.search); if (queryString.has("lang")) queryString.delete("lang"); - _get("/lang/" + page, null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 200) { - document.getElementById("lang-dropdown").remove(); - return; + _get( + "/lang/" + page, + null, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) { + document.getElementById("lang-dropdown").remove(); + return; + } + const list = document.getElementById("lang-list") as HTMLDivElement; + let innerHTML = ""; + for (let code in req.response) { + if (!code || !req.response[code]) continue; + queryString.set("lang", code); + innerHTML += `${req.response[code]}`; + queryString.delete("lang"); + } + list.innerHTML = innerHTML; } - const list = document.getElementById("lang-list") as HTMLDivElement; - let innerHTML = ''; - for (let code in req.response) { - if (!code || !(req.response[code])) continue; - queryString.set("lang", code); - innerHTML += `${req.response[code]}`; - queryString.delete("lang"); - } - list.innerHTML = innerHTML; - } - }, true); + }, + true, + ); }; diff --git a/ts/modules/list.ts b/ts/modules/list.ts index 7197ffa..823c469 100644 --- a/ts/modules/list.ts +++ b/ts/modules/list.ts @@ -6,7 +6,7 @@ declare var window: GlobalWindow; export interface ListItem { asElement: () => HTMLElement; -}; +} export class RecordCounter { private _container: HTMLElement; @@ -50,25 +50,33 @@ export class RecordCounter { }); } - get total(): number { return this._total; } + get total(): number { + return this._total; + } set total(v: number) { this._total = v; this._totalRecords.textContent = window.lang.var("strings", "totalRecords", `${v}`); } - - get loaded(): number { return this._loaded; } + + get loaded(): number { + return this._loaded; + } set loaded(v: number) { this._loaded = v; this._loadedRecords.textContent = window.lang.var("strings", "loadedRecords", `${v}`); } - - get shown(): number { return this._shown; } + + get shown(): number { + return this._shown; + } set shown(v: number) { this._shown = v; this._shownRecords.textContent = window.lang.var("strings", "shownRecords", `${v}`); } - - get selected(): number { return this._selected; } + + get selected(): number { + return this._selected; + } set selected(v: number) { this._selected = v; if (v == 0) this._selectedRecords.textContent = ``; @@ -98,7 +106,7 @@ export interface PaginatedListConfig { export abstract class PaginatedList { protected _c: PaginatedListConfig; - + // Container to append items to. protected _container: HTMLElement; // List of visible IDs (i.e. those set with setVisibility). @@ -119,14 +127,16 @@ export abstract class PaginatedList { }; protected _search: Search; - + protected _counter: RecordCounter; - + protected _hasLoaded: boolean; protected _lastLoad: number; protected _page: number = 0; protected _lastPage: boolean; - get lastPage(): boolean { return this._lastPage }; + get lastPage(): boolean { + return this._lastPage; + } set lastPage(v: boolean) { this._lastPage = v; if (v) { @@ -156,15 +166,15 @@ export abstract class PaginatedList { limit: 0, page: 0, sortByField: "", - ascending: false + ascending: false, }; - } + }; constructor(c: PaginatedListConfig) { this._c = c; this._counter = new RecordCounter(this._c.recordCounter); this._hasLoaded = false; - + this._c.loadMoreButtons.forEach((v) => { v.onclick = () => this.loadMore(false); }); @@ -180,7 +190,10 @@ export abstract class PaginatedList { } autoSetServerSearchButtonsDisabled = () => { - const serverSearchSortChanged = this._search.inServerSearch && (this._searchParams.sortByField != this._search.sortField || this._searchParams.ascending != this._search.ascending); + const serverSearchSortChanged = + this._search.inServerSearch && + (this._searchParams.sortByField != this._search.sortField || + this._searchParams.ascending != this._search.ascending); if (this._search.inServerSearch) { if (serverSearchSortChanged) { this._search.setServerSearchButtonsDisabled(false); @@ -189,28 +202,42 @@ export abstract class PaginatedList { } return; } - if (!this._search.inSearch && this._search.sortField == this._c.defaultSortField && this._search.ascending == this._c.defaultSortAscending) { + if ( + !this._search.inSearch && + this._search.sortField == this._c.defaultSortField && + this._search.ascending == this._c.defaultSortAscending + ) { this._search.setServerSearchButtonsDisabled(true); return; } this._search.setServerSearchButtonsDisabled(false); - } + }; initSearch = (searchConfig: SearchConfiguration) => { const previousCallback = searchConfig.onSearchCallback; - searchConfig.onSearchCallback = (newItems: boolean, loadAll: boolean, callback?: (resp: paginatedDTO) => void) => { + searchConfig.onSearchCallback = ( + newItems: boolean, + loadAll: boolean, + callback?: (resp: paginatedDTO) => void, + ) => { // if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused"); // else this._c.loadAllButton.classList.add("unfocused"); this.autoSetServerSearchButtonsDisabled(); // FIXME: Figure out why this makes sense and make it clearer. - if ((this._visible.length < this._c.itemsPerPage && this._counter.loaded < this._c.maxItemsLoadedForSearch && !this.lastPage) || loadAll) { - if (!newItems || + if ( + (this._visible.length < this._c.itemsPerPage && + this._counter.loaded < this._c.maxItemsLoadedForSearch && + !this.lastPage) || + loadAll + ) { + if ( + !newItems || this._previousVisibleItemCount != this._visible.length || (this._visible.length == 0 && !this.lastPage) || loadAll - ) { + ) { this.loadMore(loadAll, callback); } } @@ -229,7 +256,7 @@ export abstract class PaginatedList { console.trace("Clearing server search"); this._page = 0; this.reload(); - } + }; searchConfig.setVisibility = this.setVisibility; this._search = new Search(searchConfig); this._search.generateFilterList(); @@ -258,7 +285,7 @@ export abstract class PaginatedList { setVisibility = (elements: string[], visible: boolean, appendedItems: boolean = false) => { let timer = this._search.timeSearches ? performance.now() : null; if (visible) this._visible = elements; - else this._visible = this._search.ordering.filter(v => !elements.includes(v)); + else this._visible = this._search.ordering.filter((v) => !elements.includes(v)); // console.log(elements.length, visible, this._visible.length); this._counter.shown = this._visible.length; if (this._visible.length == 0) { @@ -268,56 +295,56 @@ export abstract class PaginatedList { if (!appendedItems) { // Wipe old elements and render 1 new one, so we can take the element height. - this._container.replaceChildren(this._search.items[this._visible[0]].asElement()) + this._container.replaceChildren(this._search.items[this._visible[0]].asElement()); } this._computeScrollInfo(); // Initial render of min(_visible.length, max(rowsOnPage*renderNExtraScreensWorth, itemsPerPage)), skipping 1 as we already did it. - this._scroll.initialRenderCount = Math.floor(Math.min( - this._visible.length, - Math.max( - ((this._scroll.renderNExtraScreensWorth+1)*this._scroll.screenHeight)/this._scroll.rowHeight, - this._c.itemsPerPage) - )); + this._scroll.initialRenderCount = Math.floor( + Math.min( + this._visible.length, + Math.max( + ((this._scroll.renderNExtraScreensWorth + 1) * this._scroll.screenHeight) / this._scroll.rowHeight, + this._c.itemsPerPage, + ), + ), + ); let baseIndex = 1; if (appendedItems) { baseIndex = this._scroll.rendered; } - const frag = document.createDocumentFragment() + const frag = document.createDocumentFragment(); for (let i = baseIndex; i < this._scroll.initialRenderCount; i++) { - frag.appendChild(this._search.items[this._visible[i]].asElement()) + frag.appendChild(this._search.items[this._visible[i]].asElement()); } this._scroll.rendered = Math.max(baseIndex, this._scroll.initialRenderCount); - // appendChild over replaceChildren because there's already elements on the DOM + // appendChild over replaceChildren because there's already elements on the DOM this._container.appendChild(frag); if (this._search.timeSearches) { const totalTime = performance.now() - timer; console.debug(`setVisibility took ${totalTime}ms`); } - } + }; // Computes required scroll info, requiring one on-DOM item. Should be computed on page resize and this._visible change. _computeScrollInfo = () => { if (this._visible.length == 0) return; - this._scroll.screenHeight = Math.max( - document.documentElement.clientHeight, - window.innerHeight || 0 - ); + this._scroll.screenHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); this._scroll.rowHeight = this._search.items[this._visible[0]].asElement().offsetHeight; - } + }; // returns the item index to render up to for the given scroll position. // might return a value greater than this._visible.length, indicating a need for a page load. maximumItemsToRender = (scrollY: number): number => { - const bottomScroll = scrollY + ((this._scroll.renderNExtraScreensWorth+1)*this._scroll.screenHeight); + const bottomScroll = scrollY + (this._scroll.renderNExtraScreensWorth + 1) * this._scroll.screenHeight; const bottomIdx = Math.floor(bottomScroll / this._scroll.rowHeight); return bottomIdx; - } + }; private _load = ( itemLimit: number, @@ -325,7 +352,7 @@ export abstract class PaginatedList { appendFunc: (resp: paginatedDTO) => void, // Function to append/put items in storage. pre?: (resp: paginatedDTO) => void, post?: (resp: paginatedDTO) => void, - failCallback?: (req: XMLHttpRequest) => void + failCallback?: (req: XMLHttpRequest) => void, ) => { this._lastLoad = Date.now(); let params = this._search.inServerSearch ? this._searchParams : this.defaultParams(); @@ -336,30 +363,35 @@ export abstract class PaginatedList { params.ascending = this._c.defaultSortAscending; } - _post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - if (req.status != 200) { + _post( + this._c.getPageEndpoint, + params, + (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + if (req.status != 200) { + if (this._c.pageLoadCallback) this._c.pageLoadCallback(req); + if (failCallback) failCallback(req); + return; + } + this._hasLoaded = true; + + let resp = req.response as paginatedDTO; + if (pre) pre(resp); + + this.lastPage = resp.last_page; + + appendFunc(resp); + + this._counter.loaded = this._search.ordering.length; + + if (post) post(resp); + if (this._c.pageLoadCallback) this._c.pageLoadCallback(req); - if (failCallback) failCallback(req); - return; - } - this._hasLoaded = true; + }, + true, + ); + }; - let resp = req.response as paginatedDTO; - if (pre) pre(resp); - - this.lastPage = resp.last_page; - - appendFunc(resp); - - this._counter.loaded = this._search.ordering.length; - - if (post) post(resp); - - if (this._c.pageLoadCallback) this._c.pageLoadCallback(req); - }, true); - } - // Removes all elements, and reloads the first page. public abstract reload: (callback?: (resp: paginatedDTO) => void) => void; protected _reload = (callback?: (resp: paginatedDTO) => void) => { @@ -369,7 +401,7 @@ export abstract class PaginatedList { // Reload all currently visible elements, i.e. Load a new page of size (limit*(page+1)). let limit = this._c.itemsPerPage; if (this._page != 0) { - limit *= this._page+1; + limit *= this._page + 1; } this._load( limit, @@ -378,7 +410,7 @@ export abstract class PaginatedList { (_0: paginatedDTO) => { // Allow refreshes every 15s this._c.refreshButton.disabled = true; - setTimeout(() => this._c.refreshButton.disabled = false, 15000); + setTimeout(() => (this._c.refreshButton.disabled = false), 15000); }, (resp: paginatedDTO) => { this._search.onSearchBoxChange(true, false, false); @@ -392,14 +424,14 @@ export abstract class PaginatedList { if (callback) callback(resp); }, ); - } + }; // Loads the next page. If "loadAll", all pages will be loaded until the last is reached. public abstract loadMore: (loadAll?: boolean, callback?: (resp?: paginatedDTO) => void) => void; protected _loadMore = (loadAll: boolean = false, callback?: (resp: paginatedDTO) => void) => { - this._c.loadMoreButtons.forEach((v) => v.disabled = true); + this._c.loadMoreButtons.forEach((v) => (v.disabled = true)); const timeout = setTimeout(() => { - this._c.loadMoreButtons.forEach((v) => v.disabled = false); + this._c.loadMoreButtons.forEach((v) => (v.disabled = false)); }, 1000); this._page += 1; @@ -430,11 +462,13 @@ export abstract class PaginatedList { if (callback) callback(resp); }, ); - } + }; public abstract loadAll: (callback?: (resp?: paginatedDTO) => void) => void; - protected _loadAll = (callback?: (resp?: paginatedDTO) => void) => { - this._c.loadAllButtons.forEach((v) => { addLoader(v, true); }); + protected _loadAll = (callback?: (resp?: paginatedDTO) => void) => { + this._c.loadAllButtons.forEach((v) => { + addLoader(v, true); + }); this.loadMore(true, callback); }; @@ -442,17 +476,16 @@ export abstract class PaginatedList { const cb = () => { if (this._counter.loaded > n) return; this.loadMore(false, cb); - } + }; cb(); - } + }; // As reloading can disrupt long-scrolling, this function will only do it if you're at the top of the page, essentially. public reloadIfNotInScroll = () => { if (this._visible.length == 0 || this.maximumItemsToRender(window.scrollY) < this._scroll.initialRenderCount) { return this.reload(); } - } - + }; _detectScroll = () => { if (!this._hasLoaded || this._scroll.scrollLoading || this._visible.length == 0) return; @@ -462,15 +495,15 @@ export abstract class PaginatedList { // If you've scrolled back up, do nothing if (scrollSpeed < 0) return; let endIdx = this.maximumItemsToRender(scrollY); - + // Throttling this function means we might not catch up in time if the user scrolls fast, // so we calculate the scroll speed (in rows/call) from the previous scrollY value. // This still might not be enough, so hackily we'll just scale it up. // With onscrollend, this is less necessary, but with both I wasn't able to hit the bottom of the page on my mouse. - const rowsPerScroll = Math.round((scrollSpeed / this._scroll.rowHeight)); + const rowsPerScroll = Math.round(scrollSpeed / this._scroll.rowHeight); // Render extra pages depending on scroll speed - endIdx += rowsPerScroll*2; - + endIdx += rowsPerScroll * 2; + const realEndIdx = Math.min(endIdx, this._visible.length); const frag = document.createDocumentFragment(); for (let i = this._scroll.rendered; i < realEndIdx; i++) { @@ -495,7 +528,7 @@ export abstract class PaginatedList { cb(); return; } - } + }; detectScroll = throttle(this._detectScroll, 200); @@ -515,7 +548,5 @@ export abstract class PaginatedList { window.removeEventListener("scroll", this.detectScroll); window.removeEventListener("scrollend", this.detectScroll); window.removeEventListener("resize", this.redrawScroll); - } + }; } - - diff --git a/ts/modules/login.ts b/ts/modules/login.ts index eb9fff5..c7ebd73 100644 --- a/ts/modules/login.ts +++ b/ts/modules/login.ts @@ -17,7 +17,7 @@ export class Login { constructor(modal: Modal, endpoint: string, appearance: string) { this._endpoint = endpoint; this._url = window.pages.Base + endpoint; - if (this._url[this._url.length-1] != '/') this._url += "/"; + if (this._url[this._url.length - 1] != "/") this._url += "/"; this._modal = modal; if (appearance == "opaque") { @@ -45,29 +45,39 @@ export class Login { this._logoutButton = button; this._logoutButton.classList.add("unfocused"); const logoutFunc = (url: string, tryAgain: boolean) => { - _post(url + "logout", null, (req: XMLHttpRequest): boolean => { - if (req.readyState == 4 && req.status == 200) { - window.token = ""; - location.reload(); - return false; - } - }, false, (req: XMLHttpRequest) => { - if (req.readyState == 4 && req.status == 404 && tryAgain) { - console.warn("logout failed, trying without URL Base..."); - logoutFunc(this._endpoint, false); - } - }); + _post( + url + "logout", + null, + (req: XMLHttpRequest): boolean => { + if (req.readyState == 4 && req.status == 200) { + window.token = ""; + location.reload(); + return false; + } + }, + false, + (req: XMLHttpRequest) => { + if (req.readyState == 4 && req.status == 404 && tryAgain) { + console.warn("logout failed, trying without URL Base..."); + logoutFunc(this._endpoint, false); + } + }, + ); }; this._logoutButton.onclick = () => logoutFunc(this._url, true); }; - get onLogin() { return this._onLogin; } - set onLogin(f: (username: string, password: string) => void) { this._onLogin = f; } + get onLogin() { + return this._onLogin; + } + set onLogin(f: (username: string, password: string) => void) { + this._onLogin = f; + } login = (username: string, password: string, run?: (state?: number) => void) => { const req = new XMLHttpRequest(); - req.responseType = 'json'; - const refresh = (username == "" && password == ""); + req.responseType = "json"; + const refresh = username == "" && password == ""; req.open("GET", this._url + (refresh ? "token/refresh" : "token/login"), true); if (!refresh) { req.setRequestHeader("Authorization", "Basic " + unicodeB64Encode(username + ":" + password)); @@ -100,13 +110,13 @@ export class Login { } if (this._hasOpacityWall) this._wall.remove(); this._modal.close(); - if (this._logoutButton != null) - this._logoutButton.classList.remove("unfocused"); + if (this._logoutButton != null) this._logoutButton.classList.remove("unfocused"); + } + if (run) { + run(+req.status); } - if (run) { run(+req.status); } } }).bind(this, req); req.send(); }; } - diff --git a/ts/modules/modal.ts b/ts/modules/modal.ts index 92e3435..a609928 100644 --- a/ts/modules/modal.ts +++ b/ts/modules/modal.ts @@ -9,14 +9,16 @@ export class Modal implements Modal { this.modal = modal; this.openEvent = new CustomEvent("modal-open-" + modal.id); this.closeEvent = new CustomEvent("modal-close-" + modal.id); - const closeButton = this.modal.querySelector('span.modal-close'); + const closeButton = this.modal.querySelector("span.modal-close"); if (closeButton !== null) { this.closeButton = closeButton as HTMLSpanElement; this.closeButton.onclick = this.close; } if (!important) { - window.addEventListener('click', (event: Event) => { - if (event.target == this.modal) { this.close(); } + window.addEventListener("click", (event: Event) => { + if (event.target == this.modal) { + this.close(); + } }); } } @@ -26,36 +28,38 @@ export class Modal implements Modal { if (event) { event.preventDefault(); } - this.modal.classList.add('animate-fade-out'); + this.modal.classList.add("animate-fade-out"); this.modal.classList.remove("animate-fade-in"); const modal = this.modal; const listenerFunc = () => { - modal.classList.remove('block'); - modal.classList.remove('animate-fade-out'); - modal.removeEventListener(window.animationEvent, listenerFunc) + modal.classList.remove("block"); + modal.classList.remove("animate-fade-out"); + modal.removeEventListener(window.animationEvent, listenerFunc); if (!noDispatch) document.dispatchEvent(this.closeEvent); }; this.modal.addEventListener(window.animationEvent, listenerFunc, false); - } + }; set onopen(f: () => void) { - document.addEventListener("modal-open-"+this.modal.id, f); + document.addEventListener("modal-open-" + this.modal.id, f); } set onclose(f: () => void) { - document.addEventListener("modal-close-"+this.modal.id, f); + document.addEventListener("modal-close-" + this.modal.id, f); } show = () => { - this.modal.classList.add('block', 'animate-fade-in'); + this.modal.classList.add("block", "animate-fade-in"); document.dispatchEvent(this.openEvent); - } + }; toggle = () => { - if (this.modal.classList.contains('animate-fade-in')) { + if (this.modal.classList.contains("animate-fade-in")) { this.close(); } else { this.show(); } - } + }; - asElement = () => { return this.modal; } + asElement = () => { + return this.modal; + }; } diff --git a/ts/modules/pages.ts b/ts/modules/pages.ts index 35ceab7..2af2edc 100644 --- a/ts/modules/pages.ts +++ b/ts/modules/pages.ts @@ -6,7 +6,7 @@ export interface Page { hide: () => boolean; shouldSkip: () => boolean; index?: number; -}; +} export interface PageConfig { hideOthersOnPageShow: boolean; @@ -29,7 +29,7 @@ export class PageManager { let ev = { state: data as string } as PopStateEvent; window.onpopstate(ev); }; - } + }; private _onpopstate = (event: PopStateEvent) => { let name = event.state; @@ -42,13 +42,13 @@ export class PageManager { } } if (!this.pages.has(name)) { - name = this.pageList[0] + name = this.pageList[0]; } let success = this.pages.get(name).show(); if (!success) { return; } - if (!(this.hideOthers)) { + if (!this.hideOthers) { return; } for (let k of this.pageList) { @@ -56,15 +56,15 @@ export class PageManager { this.pages.get(k).hide(); } } - } + }; constructor(c: PageConfig) { - this.pages = new Map; + this.pages = new Map(); this.pageList = []; this.hideOthers = c.hideOthersOnPageShow; this.defaultName = c.defaultName; this.defaultTitle = c.defaultTitle; - + this._overridePushState(); window.onpopstate = this._onpopstate; } @@ -77,12 +77,12 @@ export class PageManager { load(name: string = "") { name = decodeURI(name); - if (!this.pages.has(name)) return window.history.pushState(name || this.defaultName, this.defaultTitle, "") + if (!this.pages.has(name)) return window.history.pushState(name || this.defaultName, this.defaultTitle, ""); const p = this.pages.get(name); this.loadPage(p); } - loadPage (p: Page) { + loadPage(p: Page) { let url = p.url; // Fix ordering of query params and hash if (url.includes("#")) { @@ -99,20 +99,20 @@ export class PageManager { let p = this.pages.get(name); let shouldSkip = true; while (shouldSkip && p.index > 0) { - p = this.pages.get(this.pageList[p.index-1]); + p = this.pages.get(this.pageList[p.index - 1]); shouldSkip = p.shouldSkip(); } this.loadPage(p); - } - + } + next(name: string = "") { if (!this.pages.has(name)) return console.error(`previous page ${name} not found`); let p = this.pages.get(name); let shouldSkip = true; while (shouldSkip && p.index < this.pageList.length) { - p = this.pages.get(this.pageList[p.index+1]); + p = this.pages.get(this.pageList[p.index + 1]); shouldSkip = p.shouldSkip(); } this.loadPage(p); - } -}; + } +} diff --git a/ts/modules/profiles.ts b/ts/modules/profiles.ts index 480bb24..8cb07c2 100644 --- a/ts/modules/profiles.ts +++ b/ts/modules/profiles.ts @@ -1,24 +1,23 @@ import { _get, _post, _delete, toggleLoader, _put } from "../modules/common.js"; import hljs from "highlight.js/lib/core"; -import json from 'highlight.js/lib/languages/json'; +import json from "highlight.js/lib/languages/json"; import codeInput, { CodeInput } from "@webcoder49/code-input/code-input.mjs"; import Template from "@webcoder49/code-input/templates/hljs.mjs"; import Indent from "@webcoder49/code-input/plugins/indent.mjs"; hljs.registerLanguage("json", json); -codeInput.registerTemplate("json-highlighted", - new Template(hljs, [new Indent()]) - ); +codeInput.registerTemplate("json-highlighted", new Template(hljs, [new Indent()])); declare var window: GlobalWindow; export const profileLoadEvent = new CustomEvent("profileLoadEvent"); -export const reloadProfileNames = (then?: () => void) => _get("/profiles/names", null, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - window.availableProfiles = req.response["profiles"]; - document.dispatchEvent(profileLoadEvent); - if (then) then(); -}); +export const reloadProfileNames = (then?: () => void) => + _get("/profiles/names", null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + window.availableProfiles = req.response["profiles"]; + document.dispatchEvent(profileLoadEvent); + if (then) then(); + }); interface Profile { admin: boolean; @@ -44,10 +43,16 @@ class profile implements Profile { private _referralsEnabled: boolean; private _editButton: HTMLButtonElement; - get name(): string { return this._name.textContent; } - set name(v: string) { this._name.textContent = v; } + get name(): string { + return this._name.textContent; + } + set name(v: string) { + this._name.textContent = v; + } - get admin(): boolean { return this._adminChip.classList.contains("chip"); } + get admin(): boolean { + return this._adminChip.classList.contains("chip"); + } set admin(state: boolean) { if (state) { this._adminChip.classList.remove("unfocused"); @@ -60,10 +65,16 @@ class profile implements Profile { } } - get libraries(): string { return this._libraries.textContent; } - set libraries(v: string) { this._libraries.textContent = v; } + get libraries(): string { + return this._libraries.textContent; + } + set libraries(v: string) { + this._libraries.textContent = v; + } - get ombi(): boolean { return this._ombi; } + get ombi(): boolean { + return this._ombi; + } set ombi(v: boolean) { if (!window.ombiEnabled) return; this._ombi = v; @@ -77,8 +88,10 @@ class profile implements Profile { this._ombiButton.classList.remove("~critical"); } } - - get jellyseerr(): boolean { return this._jellyseerr; } + + get jellyseerr(): boolean { + return this._jellyseerr; + } set jellyseerr(v: boolean) { if (!window.jellyseerrEnabled) return; this._jellyseerr = v; @@ -93,10 +106,16 @@ class profile implements Profile { } } - get fromUser(): string { return this._fromUser.textContent; } - set fromUser(v: string) { this._fromUser.textContent = v; } - - get referrals_enabled(): boolean { return this._referralsEnabled; } + get fromUser(): string { + return this._fromUser.textContent; + } + set fromUser(v: string) { + this._fromUser.textContent = v; + } + + get referrals_enabled(): boolean { + return this._referralsEnabled; + } set referrals_enabled(v: boolean) { if (!window.referralsEnabled) return; this._referralsEnabled = v; @@ -111,8 +130,12 @@ class profile implements Profile { } } - get default(): boolean { return this._defaultRadio.checked; } - set default(v: boolean) { this._defaultRadio.checked = v; } + get default(): boolean { + return this._defaultRadio.checked; + } + set default(v: boolean) { + this._defaultRadio.checked = v; + } constructor(name: string, p: Profile) { this._row = document.createElement("tr") as HTMLTableRowElement; @@ -120,13 +143,16 @@ class profile implements Profile {
`; - if (window.ombiEnabled) innerHTML += ` + if (window.ombiEnabled) + innerHTML += ` `; - if (window.jellyseerrEnabled) innerHTML += ` + if (window.jellyseerrEnabled) + innerHTML += ` `; - if (window.referralsEnabled) innerHTML += ` + if (window.referralsEnabled) + innerHTML += ` `; innerHTML += ` @@ -139,8 +165,7 @@ class profile implements Profile { this._name = this._row.querySelector("b.profile-name"); this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement; this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement; - if (window.ombiEnabled) - this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement; + if (window.ombiEnabled) this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement; if (window.jellyseerrEnabled) this._jellyseerrButton = this._row.querySelector("span.profile-jellyseerr") as HTMLSpanElement; if (window.referralsEnabled) @@ -148,12 +173,13 @@ class profile implements Profile { this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement; this._editButton = this._row.querySelector(".profile-edit") as HTMLButtonElement; this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement; - this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name })); + this._defaultRadio.onclick = () => + document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name })); (this._row.querySelector("span.\\~critical") as HTMLSpanElement).onclick = this.delete; this.update(name, p); } - + update = (name: string, p: Profile) => { this.name = name; this.admin = p.admin; @@ -162,26 +188,43 @@ class profile implements Profile { this.ombi = p.ombi; this.jellyseerr = p.jellyseerr; this.referrals_enabled = p.referrals_enabled; - } + }; - setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); } - setJellyseerrFunc = (jellyseerrFunc: (jellyseerr: boolean) => void) => { this._jellyseerrButton.onclick = () => jellyseerrFunc(this._jellyseerr); } - setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); } - setEditFunc = (editFunc: (name: string) => void) => { this._editButton.onclick = () => editFunc(this.name); } + setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { + this._ombiButton.onclick = () => ombiFunc(this._ombi); + }; + setJellyseerrFunc = (jellyseerrFunc: (jellyseerr: boolean) => void) => { + this._jellyseerrButton.onclick = () => jellyseerrFunc(this._jellyseerr); + }; + setReferralFunc = (referralFunc: (enabled: boolean) => void) => { + this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); + }; + setEditFunc = (editFunc: (name: string) => void) => { + this._editButton.onclick = () => editFunc(this.name); + }; - remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); } + remove = () => { + document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); + this._row.remove(); + }; - delete = () => _delete("/profiles", { "name": this.name }, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 200 || req.status == 204) { - this.remove(); - } else { - window.notifications.customError("profileDelete", window.lang.var("notifications", "errorDeleteProfile", `"${this.name}"`)); + delete = () => + _delete("/profiles", { name: this.name }, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + this.remove(); + } else { + window.notifications.customError( + "profileDelete", + window.lang.var("notifications", "errorDeleteProfile", `"${this.name}"`), + ); + } } - } - }) + }); - asElement = (): HTMLTableRowElement => { return this._row; } + asElement = (): HTMLTableRowElement => { + return this._row; + }; } interface profileResp { @@ -201,101 +244,119 @@ export class ProfileEditor { private _profileName = document.getElementById("add-profile-name") as HTMLInputElement; private _userSelect = document.getElementById("add-profile-user") as HTMLSelectElement; private _storeHomescreen = document.getElementById("add-profile-homescreen") as HTMLInputElement; - private _createJellyseerrProfile = window.jellyseerrEnabled ? document.getElementById("add-profile-jellyseerr") as HTMLInputElement : null; + private _createJellyseerrProfile = window.jellyseerrEnabled + ? (document.getElementById("add-profile-jellyseerr") as HTMLInputElement) + : null; - get empty(): boolean { return (Object.keys(this._table.children).length == 0) } + get empty(): boolean { + return Object.keys(this._table.children).length == 0; + } set empty(state: boolean) { if (state) { - this._table.innerHTML = `${window.lang.strings("inviteNoInvites")}` + this._table.innerHTML = `${window.lang.strings("inviteNoInvites")}`; } else if (this._table.querySelector("td.empty")) { this._table.textContent = ``; } } - get default(): string { return this._default; } + get default(): string { + return this._default; + } set default(v: string) { this._default = v; - if (v != "") { this._profiles[v].default = true; } + if (v != "") { + this._profiles[v].default = true; + } for (let name in this._profiles) { - if (name != v) { this._profiles[name].default = false; } + if (name != v) { + this._profiles[name].default = false; + } } } - load = () => _get("/profiles", null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 200) { - let resp = req.response as profileResp; - if (Object.keys(resp.profiles).length == 0) { - this.empty = true; - } else { - this.empty = false; - for (let name in resp.profiles) { - if (name in this._profiles) { - this._profiles[name].update(name, resp.profiles[name]); - } else { - this._profiles[name] = new profile(name, resp.profiles[name]); - if (window.ombiEnabled) { - this._profiles[name].setOmbiFunc((ombi: boolean) => { - if (ombi) { - this._ombiProfiles.delete(name, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 204) { - window.notifications.customError("errorDeleteOmbi", window.lang.notif("errorUnknown")); - return; + load = () => + _get("/profiles", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200) { + let resp = req.response as profileResp; + if (Object.keys(resp.profiles).length == 0) { + this.empty = true; + } else { + this.empty = false; + for (let name in resp.profiles) { + if (name in this._profiles) { + this._profiles[name].update(name, resp.profiles[name]); + } else { + this._profiles[name] = new profile(name, resp.profiles[name]); + if (window.ombiEnabled) { + this._profiles[name].setOmbiFunc((ombi: boolean) => { + if (ombi) { + this._ombiProfiles.delete(name, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 204) { + window.notifications.customError( + "errorDeleteOmbi", + window.lang.notif("errorUnknown"), + ); + return; + } + this._profiles[name].ombi = false; } - this._profiles[name].ombi = false; - } - }); - } else { - window.modals.profiles.close(); - this._ombiProfiles.load(name); - } - }); - } - if (window.jellyseerrEnabled) { - this._profiles[name].setJellyseerrFunc((jellyseerr: boolean) => { - if (jellyseerr) { - this._jellyseerrProfiles.delete(name, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 204) { - window.notifications.customError("errorDeleteJellyseerr", window.lang.notif("errorUnknown")); - return; + }); + } else { + window.modals.profiles.close(); + this._ombiProfiles.load(name); + } + }); + } + if (window.jellyseerrEnabled) { + this._profiles[name].setJellyseerrFunc((jellyseerr: boolean) => { + if (jellyseerr) { + this._jellyseerrProfiles.delete(name, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 204) { + window.notifications.customError( + "errorDeleteJellyseerr", + window.lang.notif("errorUnknown"), + ); + return; + } + this._profiles[name].jellyseerr = false; } - this._profiles[name].jellyseerr = false; - } - }); - } else { - window.modals.profiles.close(); - this._jellyseerrProfiles.load(name); - } - }); + }); + } else { + window.modals.profiles.close(); + this._jellyseerrProfiles.load(name); + } + }); + } + if (window.referralsEnabled) { + this._profiles[name].setReferralFunc((enabled: boolean) => { + if (enabled) { + this.disableReferrals(name); + } else { + this.enableReferrals(name); + } + }); + } + this._profiles[name].setEditFunc(this._loadProfileEditor); + this._table.appendChild(this._profiles[name].asElement()); } - if (window.referralsEnabled) { - this._profiles[name].setReferralFunc((enabled: boolean) => { - if (enabled) { - this.disableReferrals(name); - } else { - this.enableReferrals(name); - } - }); - } - this._profiles[name].setEditFunc(this._loadProfileEditor); - this._table.appendChild(this._profiles[name].asElement()); } } + this.default = resp.default_profile; + window.modals.profiles.show(); + } else { + window.notifications.customError("profileEditor", window.lang.notif("errorLoadProfiles")); } - this.default = resp.default_profile; - window.modals.profiles.show(); - } else { - window.notifications.customError("profileEditor", window.lang.notif("errorLoadProfiles")); } - } - }) + }); - disableReferrals = (name: string) => _delete("/profiles/referral/" + name, null, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - this.load(); - }); + disableReferrals = (name: string) => + _delete("/profiles/referral/" + name, null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + this.load(); + }); enableReferrals = (name: string) => { const referralsInviteSelect = document.getElementById("enable-referrals-profile-invites") as HTMLSelectElement; @@ -316,7 +377,7 @@ export class ProfileEditor { } else { innerHTML += ``; } - + referralsInviteSelect.innerHTML = innerHTML; }); @@ -327,22 +388,34 @@ export class ProfileEditor { toggleLoader(button); let send = { - "profile": name, - "invite": referralsInviteSelect.value + profile: name, + invite: referralsInviteSelect.value, }; - - _post("/profiles/referral/" + send["profile"] + "/" + send["invite"] + "/" + (referralsExpiry.checked ? "with-expiry" : "none"), send, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - toggleLoader(button); - if (req.status == 400) { - window.notifications.customError("unknownError", window.lang.notif("errorUnknown")); - } else if (req.status == 200 || req.status == 204) { - window.notifications.customSuccess("enableReferralsSuccess", window.lang.notif("referralsEnabled")); + + _post( + "/profiles/referral/" + + send["profile"] + + "/" + + send["invite"] + + "/" + + (referralsExpiry.checked ? "with-expiry" : "none"), + send, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 400) { + window.notifications.customError("unknownError", window.lang.notif("errorUnknown")); + } else if (req.status == 200 || req.status == 204) { + window.notifications.customSuccess( + "enableReferralsSuccess", + window.lang.notif("referralsEnabled"), + ); + } + window.modals.enableReferralsProfile.close(); + this.load(); } - window.modals.enableReferralsProfile.close(); - this.load(); - } - }); + }, + ); }; referralsExpiry.checked = false; window.modals.profiles.close(); @@ -372,7 +445,7 @@ export class ProfileEditor { let send: any; try { send = JSON.parse(editor.value); - } catch(e: any) { + } catch (e: any) { submit.classList.add("~critical"); submit.classList.remove("~urge"); window.notifications.customError("errorInvalidJSON", window.lang.notif("errorInvalidJSON")); @@ -389,7 +462,7 @@ export class ProfileEditor { // a 201 implies the profile was renamed. Since reloading profiles doesn't delete missing ones, // we should delete the old one ourselves. if (req.status == 201) { - this._profiles[name].remove() + this._profiles[name].remove(); delete this._profiles[name]; } } else { @@ -403,16 +476,15 @@ export class ProfileEditor { window.modals.profiles.close(); window.modals.editProfile.show(); - }) - - } + }); + }; constructor() { - (document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load; + (document.getElementById("setting-profiles") as HTMLSpanElement).onclick = this.load; document.addEventListener("profiles-default", (event: CustomEvent) => { const prevDefault = this.default; const newDefault = event.detail; - _post("/profiles/default", { "name": newDefault }, (req: XMLHttpRequest) => { + _post("/profiles/default", { name: newDefault }, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200 || req.status == 204) { this.default = newDefault; @@ -428,38 +500,37 @@ export class ProfileEditor { this.load(); }); - if (window.ombiEnabled) - this._ombiProfiles = new ombiProfiles(); - if (window.jellyseerrEnabled) - this._jellyseerrProfiles = new jellyseerrProfiles(); + if (window.ombiEnabled) this._ombiProfiles = new ombiProfiles(); + if (window.jellyseerrEnabled) this._jellyseerrProfiles = new jellyseerrProfiles(); - this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 200 || req.status == 204) { - let innerHTML = ``; - for (let user of req.response["users"]) { - innerHTML += ``; + this._createButton.onclick = () => + _get("/users", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + let innerHTML = ``; + for (let user of req.response["users"]) { + innerHTML += ``; + } + this._userSelect.innerHTML = innerHTML; + this._storeHomescreen.checked = true; + if (this._createJellyseerrProfile) this._createJellyseerrProfile.checked = true; + window.modals.profiles.close(); + window.modals.addProfile.show(); + } else { + window.notifications.customError("loadUsers", window.lang.notif("errorLoadUsers")); } - this._userSelect.innerHTML = innerHTML; - this._storeHomescreen.checked = true; - if (this._createJellyseerrProfile) this._createJellyseerrProfile.checked = true; - window.modals.profiles.close(); - window.modals.addProfile.show(); - } else { - window.notifications.customError("loadUsers", window.lang.notif("errorLoadUsers")); } - } - }); + }); this._createForm.onsubmit = (event: SubmitEvent) => { event.preventDefault(); const button = this._createForm.querySelector("span.submit") as HTMLSpanElement; toggleLoader(button); let send = { - "homescreen": this._storeHomescreen.checked, - "id": this._userSelect.value, - "name": this._profileName.value - } + homescreen: this._storeHomescreen.checked, + id: this._userSelect.value, + name: this._profileName.value, + }; if (this._createJellyseerrProfile) send["jellyseerr"] = this._createJellyseerrProfile.checked; _post("/profiles", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { @@ -467,15 +538,20 @@ export class ProfileEditor { window.modals.addProfile.close(); if (req.status == 200 || req.status == 204) { this.load(); - window.notifications.customSuccess("createProfile", window.lang.var("notifications", "createProfile", `"${send['name']}"`)); + window.notifications.customSuccess( + "createProfile", + window.lang.var("notifications", "createProfile", `"${send["name"]}"`), + ); } else { - window.notifications.customError("createProfile", window.lang.var("notifications", "errorCreateProfile", `"${send['name']}"`)); + window.notifications.customError( + "createProfile", + window.lang.var("notifications", "errorCreateProfile", `"${send["name"]}"`), + ); } window.modals.profiles.show(); } - }) + }); }; - } } @@ -501,27 +577,32 @@ export class ombiProfiles { let resp = {} as ombiUser; resp.id = this._select.value; resp.name = this._users[resp.id]; - _post("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)), resp, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - toggleLoader(button); - if (req.status == 200 || req.status == 204) { - window.notifications.customSuccess("ombiDefaults", window.lang.notif("setOmbiProfile")); - } else { - window.notifications.customError("ombiDefaults", window.lang.notif("errorSetOmbiProfile")); + _post( + "/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)), + resp, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 200 || req.status == 204) { + window.notifications.customSuccess("ombiDefaults", window.lang.notif("setOmbiProfile")); + } else { + window.notifications.customError("ombiDefaults", window.lang.notif("errorSetOmbiProfile")); + } + window.modals.ombiProfile.close(); } - window.modals.ombiProfile.close(); - } - }); - } + }, + ); + }; - delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post); + delete = (profile: string, post?: (req: XMLHttpRequest) => void) => + _delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post); load = (profile: string) => { this._currentProfile = profile; _get("/ombi/users", null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200 && "users" in req.response) { - const users = req.response["users"] as ombiUser[]; + const users = req.response["users"] as ombiUser[]; let innerHTML = ""; for (let user of users) { this._users[user.id] = user.name; @@ -530,11 +611,11 @@ export class ombiProfiles { this._select.innerHTML = innerHTML; window.modals.ombiProfile.show(); } else { - window.notifications.customError("ombiLoadError", window.lang.notif("errorLoadOmbiUsers")) + window.notifications.customError("ombiLoadError", window.lang.notif("errorLoadOmbiUsers")); } } }); - } + }; } export class jellyseerrProfiles { @@ -563,16 +644,17 @@ export class jellyseerrProfiles { window.modals.jellyseerrProfile.close(); } }); - } + }; - delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/jellyseerr/" + encodeURIComponent(encodeURIComponent(profile)), null, post); + delete = (profile: string, post?: (req: XMLHttpRequest) => void) => + _delete("/profiles/jellyseerr/" + encodeURIComponent(encodeURIComponent(profile)), null, post); load = (profile: string) => { this._currentProfile = profile; _get("/jellyseerr/users", null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200 && "users" in req.response) { - const users = req.response["users"] as ombiUser[]; + const users = req.response["users"] as ombiUser[]; let innerHTML = ""; for (let user of users) { this._users[user.id] = user.name; @@ -581,9 +663,9 @@ export class jellyseerrProfiles { this._select.innerHTML = innerHTML; window.modals.jellyseerrProfile.show(); } else { - window.notifications.customError("jellyseerrLoadError", window.lang.notif("errorLoadUsers")) + window.notifications.customError("jellyseerrLoadError", window.lang.notif("errorLoadUsers")); } } }); - } + }; } diff --git a/ts/modules/search.ts b/ts/modules/search.ts index a0b8418..590040b 100644 --- a/ts/modules/search.ts +++ b/ts/modules/search.ts @@ -6,7 +6,7 @@ declare var window: GlobalWindow; export enum QueryOperator { Greater = ">", Lower = "<", - Equal = "=" + Equal = "=", } export function QueryOperatorToDateText(op: QueryOperator): string { @@ -29,7 +29,7 @@ export interface QueryType { date: boolean; dependsOnElement?: string; // Format for querySelector show?: boolean; - localOnly?: boolean // Indicates can't be performed server-side. + localOnly?: boolean; // Indicates can't be performed server-side. } export interface SearchConfiguration { @@ -62,7 +62,7 @@ export interface QueryDTO { field: string; operator: QueryOperator; value: boolean | string | DateAttempt; -}; +} export abstract class Query { protected _subject: QueryType; @@ -83,8 +83,10 @@ export abstract class Query { this._card.addEventListener("click", v); } - asElement(): HTMLElement { return this._card; } - + asElement(): HTMLElement { + return this._card; + } + public abstract compare(subjectValue: any): boolean; asDTO(): QueryDTO | null { @@ -95,7 +97,9 @@ export abstract class Query { return out; } - get subject(): QueryType { return this._subject; } + get subject(): QueryType { + return this._subject; + } getValueFromItem(item: SearchableItem): any { return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(item), this.subject.getter).get.call(item); @@ -105,7 +109,9 @@ export abstract class Query { return this.compare(this.getValueFromItem(item)); } - get localOnly(): boolean { return this._subject.localOnly ? true : false; } + get localOnly(): boolean { + return this._subject.localOnly ? true : false; + } } export class BoolQuery extends Query { @@ -122,7 +128,7 @@ export class BoolQuery extends Query { } this._card.innerHTML = ` ${subject.name} - + `; } @@ -136,14 +142,16 @@ export class BoolQuery extends Query { isBool = true; boolState = false; } - return [boolState, isBool] + return [boolState, isBool]; } - get value(): boolean { return this._value; } + get value(): boolean { + return this._value; + } // Ripped from old code. Why it's like this, I don't know public compare(subjectBool: boolean): boolean { - return ((subjectBool && this._value) || (!subjectBool && !this._value)) + return (subjectBool && this._value) || (!subjectBool && !this._value); } asDTO(): QueryDTO | null { @@ -167,12 +175,14 @@ export class StringQuery extends Query { `; } - get value(): string { return this._value; } + get value(): string { + return this._value; + } public compare(subjectString: string): boolean { return subjectString.toLowerCase().includes(this._value); } - + asDTO(): QueryDTO | null { let out = super.asDTO(); if (out === null) return null; @@ -211,7 +221,7 @@ export class DateQuery extends Query { this._card.classList.add("button", "~neutral", "@low", "center", "flex", "flex-row", "gap-2"); let dateText = QueryOperatorToDateText(operator); this._card.innerHTML = ` - ${subject.name}: ${dateText != "" ? dateText+" " : ""}${value.text} + ${subject.name}: ${dateText != "" ? dateText + " " : ""}${value.text} `; } @@ -225,11 +235,13 @@ export class DateQuery extends Query { let out = parseDateString(valueString); let isValid = true; if (out.invalid) isValid = false; - + return [out, op, isValid]; } - get value(): ParsedDate { return this._value; } + get value(): ParsedDate { + return this._value; + } public compare(subjectDate: Date): boolean { // We want to compare only the fields given in this._value, @@ -237,10 +249,7 @@ export class DateQuery extends Query { const temp = new Date(subjectDate.valueOf()); for (let [field] of dateGetters) { if (field in this._value.attempt) { - dateSetters.get(field).call( - temp, - dateGetters.get(field).call(this._value.date) - ); + dateSetters.get(field).call(temp, dateGetters.get(field).call(this._value.date)); } } @@ -251,7 +260,7 @@ export class DateQuery extends Query { } return subjectDate > temp; } - + asDTO(): QueryDTO | null { let out = super.asDTO(); if (out === null) return null; @@ -272,7 +281,7 @@ export type SearchableItems = { [id: string]: SearchableItem }; export class Search { private _c: SearchConfiguration; private _sortField: string = ""; - private _ascending: boolean = true; + private _ascending: boolean = true; private _ordering: string[] = []; private _items: SearchableItems = {}; // Search queries (filters) @@ -281,7 +290,9 @@ export class Search { private _searchTerms: string[] = []; inSearch: boolean = false; private _inServerSearch: boolean = false; - get inServerSearch(): boolean { return this._inServerSearch; } + get inServerSearch(): boolean { + return this._inServerSearch; + } set inServerSearch(v: boolean) { const previous = this._inServerSearch; this._inServerSearch = v; @@ -316,14 +327,14 @@ export class Search { } } - if (query[i] == " " || i == query.length-1) { + if (query[i] == " " || i == query.length - 1) { if (lastQuote != -1) { continue; } else { - let end = i+1; + let end = i + 1; if (query[i] == " ") { end = i; - while (i+1 < query.length && query[i+1] == " ") { + while (i + 1 < query.length && query[i + 1] == " ") { i += 1; } } @@ -333,7 +344,7 @@ export class Search { } } return words; - } + }; parseTokens = (tokens: string[]): [string[], Query[]] => { let queries: Query[] = []; @@ -346,8 +357,8 @@ export class Search { continue; } // 2. A filter query of some sort. - const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)]; - + const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":") + 1)]; + if (!(split[0] in this._c.queries)) continue; const queryFormat = this._c.queries[split[0]]; @@ -360,9 +371,12 @@ export class Search { q = new BoolQuery(queryFormat, boolState); q.onclick = () => { for (let quote of [`"`, `'`, ``]) { - this._c.search.value = this._c.search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); + this._c.search.value = this._c.search.value.replace( + split[0] + ":" + quote + split[1] + quote, + "", + ); } - this._c.search.oninput((null as Event)); + this._c.search.oninput(null as Event); }; queries.push(q); continue; @@ -376,8 +390,8 @@ export class Search { let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); this._c.search.value = this._c.search.value.replace(regex, ""); } - this._c.search.oninput((null as Event)); - } + this._c.search.oninput(null as Event); + }; queries.push(q); continue; } @@ -385,23 +399,23 @@ export class Search { let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]); if (!isDate) continue; q = new DateQuery(queryFormat, op, parsedDate); - + q.onclick = () => { for (let quote of [`"`, `'`, ``]) { let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); this._c.search.value = this._c.search.value.replace(regex, ""); } - - this._c.search.oninput((null as Event)); - } + + this._c.search.oninput(null as Event); + }; queries.push(q); continue; } // if (q != null) queries.push(q); } return [searchTerms, queries]; - } - + }; + // Returns a list of identifiers (used as keys in items, values in ordering). searchParsed = (searchTerms: string[], queries: Query[]): string[] => { let result: string[] = [...this._ordering]; @@ -432,7 +446,7 @@ export class Search { for (let q of queries) { this._c.filterArea.appendChild(q.asElement()); // Skip if this query has already been performed by the server. - if (this.inServerSearch && !(q.localOnly)) continue; + if (this.inServerSearch && !q.localOnly) continue; let cachedResult = [...result]; if (q.type == "bool") { @@ -463,7 +477,7 @@ export class Search { result.splice(result.indexOf(id), 1); continue; } - let value = new Date(unixValue*1000); + let value = new Date(unixValue * 1000); if (!q.compare(value)) { result.splice(result.indexOf(id), 1); @@ -472,17 +486,17 @@ export class Search { } } return result; - } + }; // Returns a list of identifiers (used as keys in items, values in ordering). search = (query: string): string[] => { let timer = this.timeSearches ? performance.now() : null; this._c.filterArea.textContent = ""; - + const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query)); let result = this.searchParsed(searchTerms, queries); - + this._queries = queries; this._searchTerms = searchTerms; @@ -491,44 +505,57 @@ export class Search { console.debug(`Search took ${totalTime}ms`); } return result; - } + }; // postServerSearch performs local-only queries after a server search if necessary. postServerSearch = () => { this.searchParsed(this._searchTerms, this._queries); }; - + showHideSearchOptionsHeader = () => { let sortingBy = false; - if (this._c.sortingByButton) sortingBy = !(this._c.sortingByButton.classList.contains("hidden")); + if (this._c.sortingByButton) sortingBy = !this._c.sortingByButton.classList.contains("hidden"); const hasFilters = this._c.filterArea.textContent != ""; if (sortingBy || hasFilters) { this._c.searchOptionsHeader.classList.remove("hidden"); } else { this._c.searchOptionsHeader.classList.add("hidden"); } - } + }; // -all- elements. - get items(): { [id: string]: SearchableItem } { return this._items; } + get items(): { [id: string]: SearchableItem } { + return this._items; + } // set items(v: { [id: string]: SearchableItem }) { // this._items = v; // } // The order of -all- elements (even those hidden), by their identifier. - get ordering(): string[] { return this._ordering; } + get ordering(): string[] { + return this._ordering; + } // Specifically dis-allow setting ordering itself, so that setOrdering is used instead (for the field and ascending params). // set ordering(v: string[]) { this._ordering = v; } setOrdering = (v: string[], field: string, ascending: boolean) => { this._ordering = v; this._sortField = field; this._ascending = ascending; + }; + + get sortField(): string { + return this._sortField; + } + get ascending(): boolean { + return this._ascending; } - get sortField(): string { return this._sortField; } - get ascending(): boolean { return this._ascending; } - - onSearchBoxChange = (newItems: boolean = false, appendedItems: boolean = false, loadAll: boolean = false, callback?: (resp: paginatedDTO) => void) => { + onSearchBoxChange = ( + newItems: boolean = false, + appendedItems: boolean = false, + loadAll: boolean = false, + callback?: (resp: paginatedDTO) => void, + ) => { const query = this._c.search.value; if (!query) { this.inSearch = false; @@ -554,7 +581,7 @@ export class Search { this.showHideSearchOptionsHeader(); this.setNotFoundPanelVisibility(results.length == 0); if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0); - } + }; setNotFoundPanelVisibility = (visible: boolean) => { if (this._inServerSearch || !this.inSearch) { @@ -567,14 +594,13 @@ export class Search { } else { this._c.notFoundPanel.classList.add("unfocused"); } - } + }; fillInFilter = (name: string, value: string, offset?: number) => { this._c.search.value = name + ":" + value + " " + this._c.search.value; this._c.search.focus(); let newPos = name.length + 1 + value.length; - if (typeof offset !== 'undefined') - newPos += offset; + if (typeof offset !== "undefined") newPos += offset; this._c.search.setSelectionRange(newPos, newPos); this._c.search.oninput(null as any); }; @@ -592,7 +618,16 @@ export class Search { } const container = document.createElement("span") as HTMLSpanElement; - container.classList.add("button", "button-xl", "~neutral", "@low", "align-bottom", "flex", "flex-row", "gap-2"); + container.classList.add( + "button", + "button-xl", + "~neutral", + "@low", + "align-bottom", + "flex", + "flex-row", + "gap-2", + ); container.innerHTML = `
${query.name} @@ -624,7 +659,7 @@ export class Search { // Position cursor between quotes button.addEventListener("click", () => this.fillInFilter(queryName, `""`, -1)); - + container.appendChild(button); } if (query.date) { @@ -645,27 +680,26 @@ export class Search { afterDate.classList.add("button", "~urge", "flex", "flex-row", "gap-2"); afterDate.innerHTML = `After Date`; afterDate.addEventListener("click", () => this.fillInFilter(queryName, `">"`, -1)); - + container.appendChild(onDate); container.appendChild(beforeDate); container.appendChild(afterDate); } - + filterListContainer.appendChild(container); } - this._c.filterList.appendChild(filterListContainer) - } + this._c.filterList.appendChild(filterListContainer); + }; onServerSearch = () => { const newServerSearch = !this.inServerSearch; this.inServerSearch = true; this.searchServer(newServerSearch); - } + }; searchServer = (newServerSearch: boolean) => { this._c.searchServer(this.serverSearchParams(this._searchTerms, this._queries), newServerSearch); - } - + }; serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => { let req: ServerSearchReqDTO = { @@ -674,18 +708,18 @@ export class Search { limit: -1, page: 0, sortByField: this.sortField, - ascending: this.ascending + ascending: this.ascending, }; for (const q of queries) { const dto = q.asDTO(); if (dto !== null) req.queries.push(dto); } return req; - } + }; setServerSearchButtonsDisabled = (disabled: boolean) => { - this._serverSearchButtons.forEach((v: HTMLButtonElement) => v.disabled = disabled); - } + this._serverSearchButtons.forEach((v: HTMLButtonElement) => (v.disabled = disabled)); + }; constructor(c: SearchConfiguration) { this._c = c; @@ -693,14 +727,16 @@ export class Search { this._c.search.oninput = () => { this.inServerSearch = false; this.onSearchBoxChange(); - } + }; this._c.search.addEventListener("keyup", (ev: KeyboardEvent) => { if (ev.key == "Enter") { this.onServerSearch(); } }); - const clearSearchButtons = Array.from(document.querySelectorAll(this._c.clearSearchButtonSelector)) as Array; + const clearSearchButtons = Array.from( + document.querySelectorAll(this._c.clearSearchButtonSelector), + ) as Array; for (let b of clearSearchButtons) { b.addEventListener("click", () => { this._c.search.value = ""; @@ -708,8 +744,10 @@ export class Search { this.onSearchBoxChange(); }); } - - this._serverSearchButtons = Array.from(document.querySelectorAll(this._c.serverSearchButtonSelector)) as Array; + + this._serverSearchButtons = Array.from( + document.querySelectorAll(this._c.serverSearchButtonSelector), + ) as Array; for (let b of this._serverSearchButtons) { b.addEventListener("click", () => { this.onServerSearch(); diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index 32715dc..0cfcf5d 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -1,4 +1,17 @@ -import { _get, _post, _delete, _download, _upload, toggleLoader, addLoader, removeLoader, insertText, toClipboard, toDateString, SetupCopyButton } from "../modules/common.js"; +import { + _get, + _post, + _delete, + _download, + _upload, + toggleLoader, + addLoader, + removeLoader, + insertText, + toClipboard, + toDateString, + SetupCopyButton, +} from "../modules/common.js"; import { Marked } from "@ts-stack/markdown"; import { stripMarkdown } from "../modules/stripmd.js"; import { PDT } from "src/data/timezoneNames"; @@ -7,7 +20,7 @@ declare var window: GlobalWindow; const toBool = (s: string): boolean => { return s == "false" ? false : Boolean(s); -} +}; interface BackupDTO { size: string; @@ -17,7 +30,7 @@ interface BackupDTO { commit: string; } -interface settingsChangedEvent extends Event { +interface settingsChangedEvent extends Event { detail: { value: string; hidden: boolean; @@ -28,11 +41,13 @@ interface advancedEvent extends Event { detail: boolean; } -const changedEvent = (section: string, setting: string, value: string, hidden: boolean = false) => { - return new CustomEvent(`settings-${section}-${setting}`, { detail: { - value: value, - hidden: hidden - }}); +const changedEvent = (section: string, setting: string, value: string, hidden: boolean = false) => { + return new CustomEvent(`settings-${section}-${setting}`, { + detail: { + value: value, + hidden: hidden, + }, + }); }; type SettingType = string; @@ -85,7 +100,7 @@ const splitDependant = (section: string, dep: string): string[] => { if (parts.length == 1) { parts = [section, dep]; } - return parts + return parts; }; let RestartRequiredBadge: HTMLElement; @@ -103,7 +118,9 @@ class DOMSetting { protected _s: Setting; setting: string; - get hidden(): boolean { return this._hideEl.classList.contains("unfocused"); } + get hidden(): boolean { + return this._hideEl.classList.contains("unfocused"); + } set hidden(v: boolean) { if (v) { this._hideEl.classList.add("unfocused"); @@ -115,10 +132,12 @@ class DOMSetting { } private _advancedListener = (event: advancedEvent) => { - this.hidden = !(event.detail); - } + this.hidden = !event.detail; + }; - get advanced(): boolean { return this._advanced; } + get advanced(): boolean { + return this._advanced; + } set advanced(advanced: boolean) { this._advanced = advanced; if (advanced) { @@ -128,10 +147,16 @@ class DOMSetting { } } - get name(): string { return this._container.querySelector("span.setting-label").textContent; } - set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; } + get name(): string { + return this._container.querySelector("span.setting-label").textContent; + } + set name(n: string) { + this._container.querySelector("span.setting-label").textContent = n; + } - get description(): string { return this._tooltip.querySelector("span.content").textContent; } + get description(): string { + return this._tooltip.querySelector("span.content").textContent; + } set description(d: string) { const content = this._tooltip.querySelector("span.content") as HTMLSpanElement; content.textContent = d; @@ -142,7 +167,9 @@ class DOMSetting { } } - get required(): boolean { return !(this._required.classList.contains("unfocused")); } + get required(): boolean { + return !this._required.classList.contains("unfocused"); + } set required(state: boolean) { if (state) { this._required.classList.remove("unfocused"); @@ -152,8 +179,10 @@ class DOMSetting { this._required.textContent = ``; } } - - get requires_restart(): boolean { return !(this._restart.classList.contains("unfocused")); } + + get requires_restart(): boolean { + return !this._restart.classList.contains("unfocused"); + } set requires_restart(state: boolean) { if (state) { this._restart.classList.remove("unfocused"); @@ -164,37 +193,49 @@ class DOMSetting { } } - get depends_true(): string { return this._s.depends_true; } + get depends_true(): string { + return this._s.depends_true; + } set depends_true(v: string) { this._s.depends_true = v; this._registerDependencies(); } - - get depends_false(): string { return this._s.depends_false; } + + get depends_false(): string { + return this._s.depends_false; + } set depends_false(v: string) { this._s.depends_false = v; this._registerDependencies(); } - get aliases(): string[] { return this._s.aliases; } + get aliases(): string[] { + return this._s.aliases; + } protected _registerDependencies() { // Doesn't re-register dependencies, but that isn't important in this application if (!(this._s.depends_true || this._s.depends_false)) return; let [sect, dependant] = splitDependant(this._section, this._s.depends_true || this._s.depends_false); - let state = !(Boolean(this._s.depends_false)); + let state = !Boolean(this._s.depends_false); document.addEventListener(`settings-${sect}-${dependant}`, (event: settingsChangedEvent) => { - this.hidden = event.detail.hidden || (toBool(event.detail.value) !== state); + this.hidden = event.detail.hidden || toBool(event.detail.value) !== state; }); } - valueAsString = (): string => { return ""+this.value; }; + valueAsString = (): string => { + return "" + this.value; + }; onValueChange = () => { document.dispatchEvent(changedEvent(this._section, this.setting, this.valueAsString(), this.hidden)); - const setEvent = new CustomEvent(`settings-set-${this._section}-${this.setting}`, { "detail": this.valueAsString() }) + const setEvent = new CustomEvent(`settings-set-${this._section}-${this.setting}`, { + detail: this.valueAsString(), + }); document.dispatchEvent(setEvent); - if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); } + if (this.requires_restart) { + document.dispatchEvent(new CustomEvent("settings-requires-restart")); + } }; constructor(input: string, setting: Setting, section: string, name: string, inputOnTop: boolean = false) { @@ -228,8 +269,12 @@ class DOMSetting { this._hideEl = this._container; } - get value(): any { return this._input.value; } - set value(v: any) { this._input.value = v; } + get value(): any { + return this._input.value; + } + set value(v: any) { + this._input.value = v; + } update(s: Setting) { this.name = s.name; @@ -238,22 +283,21 @@ class DOMSetting { this.requires_restart = s.requires_restart; this.value = s.value; this.advanced = s.advanced; - if (!(this._s) || s.depends_true != this._s.depends_true || s.depends_false != this._s.depends_false) { + if (!this._s || s.depends_true != this._s.depends_true || s.depends_false != this._s.depends_false) { this._s = s; this._registerDependencies(); } this._s = s; } - - asElement = (): HTMLDivElement => { return this._container; } + + asElement = (): HTMLDivElement => { + return this._container; + }; } class DOMInput extends DOMSetting { constructor(inputType: string, setting: Setting, section: string, name: string) { - super( - ``, - setting, section, name, - ); + super(``, setting, section, name); // this._hideEl = this._input.parentElement; this.update(setting); } @@ -263,40 +307,64 @@ interface SText extends Setting { value: string; } class DOMText extends DOMInput implements SText { - constructor(setting: Setting, section: string, name: string) { super("text", setting, section, name); } + constructor(setting: Setting, section: string, name: string) { + super("text", setting, section, name); + } type: SettingType = TextType; - get value(): string { return this._input.value } - set value(v: string) { this._input.value = v; } + get value(): string { + return this._input.value; + } + set value(v: string) { + this._input.value = v; + } } interface SPassword extends Setting { value: string; } class DOMPassword extends DOMInput implements SPassword { - constructor(setting: Setting, section: string, name: string) { super("password", setting, section, name); } + constructor(setting: Setting, section: string, name: string) { + super("password", setting, section, name); + } type: SettingType = PasswordType; - get value(): string { return this._input.value } - set value(v: string) { this._input.value = v; } + get value(): string { + return this._input.value; + } + set value(v: string) { + this._input.value = v; + } } interface SEmail extends Setting { value: string; } class DOMEmail extends DOMInput implements SEmail { - constructor(setting: Setting, section: string, name: string) { super("email", setting, section, name); } + constructor(setting: Setting, section: string, name: string) { + super("email", setting, section, name); + } type: SettingType = EmailType; - get value(): string { return this._input.value } - set value(v: string) { this._input.value = v; } + get value(): string { + return this._input.value; + } + set value(v: string) { + this._input.value = v; + } } interface SNumber extends Setting { value: number; } class DOMNumber extends DOMInput implements SNumber { - constructor(setting: Setting, section: string, name: string) { super("number", setting, section, name); } + constructor(setting: Setting, section: string, name: string) { + super("number", setting, section, name); + } type: SettingType = NumberType; - get value(): number { return +this._input.value; } - set value(v: number) { this._input.value = ""+v; } + get value(): number { + return +this._input.value; + } + set value(v: number) { + this._input.value = "" + v; + } } interface SList extends Setting { @@ -305,8 +373,10 @@ interface SList extends Setting { class DOMList extends DOMSetting implements SList { protected _inputs: HTMLDivElement; type: SettingType = ListType; - - valueAsString = (): string => { return this.value.join("|"); }; + + valueAsString = (): string => { + return this.value.join("|"); + }; get value(): string[] { let values = []; @@ -327,12 +397,12 @@ class DOMList extends DOMSetting implements SList { const input = dummyRow.querySelector("input") as HTMLInputElement; input.placeholder = window.lang.strings("add"); const onDummyChange = () => { - if (!(input.value)) return; + if (!input.value) return; addDummy(); input.removeEventListener("change", onDummyChange); input.removeEventListener("keyup", onDummyChange); input.placeholder = ``; - } + }; input.addEventListener("change", onDummyChange); input.addEventListener("keyup", onDummyChange); this._input.appendChild(dummyRow); @@ -354,18 +424,15 @@ class DOMList extends DOMSetting implements SList { input.onchange = this.onValueChange; const removeRow = container.querySelector("button") as HTMLButtonElement; removeRow.onclick = () => { - if (!(container.nextElementSibling)) return; + if (!container.nextElementSibling) return; container.remove(); this.onValueChange(); - } + }; return container; } - + constructor(setting: Setting, section: string, name: string) { - super( - `
`, - setting, section, name, - ); + super(`
`, setting, section, name); // this._hideEl = this._input.parentElement; this.update(setting); } @@ -377,14 +444,15 @@ interface SBool extends Setting { class DOMBool extends DOMSetting implements SBool { type: SettingType = BoolType; - get value(): boolean { return this._input.checked; } - set value(state: boolean) { this._input.checked = state; } - + get value(): boolean { + return this._input.checked; + } + set value(state: boolean) { + this._input.checked = state; + } + constructor(setting: SBool, section: string, name: string) { - super( - ``, - setting, section, name, true, - ); + super(``, setting, section, name, true); const label = this._container.getElementsByTagName("LABEL")[0]; label.classList.remove("flex-col"); label.classList.add("flex-row"); @@ -401,7 +469,9 @@ class DOMSelect extends DOMSetting implements SSelect { type: SettingType = SelectType; private _options: string[][]; - get options(): string[][] { return this._options; } + get options(): string[][] { + return this._options; + } set options(opt: string[][]) { this._options = opt; let innerHTML = ""; @@ -414,14 +484,16 @@ class DOMSelect extends DOMSetting implements SSelect { update(s: SSelect) { this.options = s.options; super.update(s); - }; + } constructor(setting: SSelect, section: string, name: string) { super( `
`, - setting, section, name, + setting, + section, + name, ); this._options = []; // this._hideEl = this._container; @@ -440,7 +512,9 @@ class DOMNote extends DOMSetting implements SNote { private _style: string; // We're a note, no one depends on us so we don't need to broadcast a state change. - get hidden(): boolean { return this._container.classList.contains("unfocused"); } + get hidden(): boolean { + return this._container.classList.contains("unfocused"); + } set hidden(v: boolean) { if (v) { this._container.classList.add("unfocused"); @@ -449,32 +523,54 @@ class DOMNote extends DOMSetting implements SNote { } } - get name(): string { return this._nameEl.textContent; } - set name(n: string) { this._nameEl.textContent = n; } + get name(): string { + return this._nameEl.textContent; + } + set name(n: string) { + this._nameEl.textContent = n; + } - get description(): string { return this._description.textContent; } + get description(): string { + return this._description.textContent; + } set description(d: string) { this._description.innerHTML = d; } - valueAsString = (): string => { return ""; }; + valueAsString = (): string => { + return ""; + }; - get value(): string { return ""; } - set value(_: string) { return; } - - get required(): boolean { return false; } - set required(_: boolean) { return; } - - get requires_restart(): boolean { return false; } - set requires_restart(_: boolean) { return; } + get value(): string { + return ""; + } + set value(_: string) { + return; + } - get style(): string { return this._style; } + get required(): boolean { + return false; + } + set required(_: boolean) { + return; + } + + get requires_restart(): boolean { + return false; + } + set requires_restart(_: boolean) { + return; + } + + get style(): string { + return this._style; + } set style(s: string) { this._input.classList.remove("~" + this._style); this._style = s; this._input.classList.add("~" + this._style); } - + constructor(setting: SNote, section: string) { super( ` @@ -482,7 +578,10 @@ class DOMNote extends DOMSetting implements SNote { - `, setting, section, "", + `, + setting, + section, + "", ); // this._hideEl = this._container; this._nameEl = this._container.querySelector(".setting-name"); @@ -493,10 +592,12 @@ class DOMNote extends DOMSetting implements SNote { update(s: SNote) { this.name = s.name; this.description = s.description; - this.style = ("style" in s && s.style) ? s.style : "info"; + this.style = "style" in s && s.style ? s.style : "info"; + } + + asElement = (): HTMLDivElement => { + return this._container; }; - - asElement = (): HTMLDivElement => { return this._container; } } interface Group { @@ -508,10 +609,18 @@ interface Group { abstract class groupableItem { protected _el: HTMLElement; - asElement = () => { return this._el; } - remove = () => { this._el.remove(); }; - inGroup = (): string|null => { return this._el.parentElement.getAttribute("data-group"); } - get hidden(): boolean { return this._el.classList.contains("unfocused"); } + asElement = () => { + return this._el; + }; + remove = () => { + this._el.remove(); + }; + inGroup = (): string | null => { + return this._el.parentElement.getAttribute("data-group"); + }; + get hidden(): boolean { + return this._el.classList.contains("unfocused"); + } set hidden(v: boolean) { if (v) { this._el.classList.add("unfocused"); @@ -541,12 +650,16 @@ class groupButton extends groupableItem { private _indentClasses = ["h-11", "h-10", "h-9"]; private _indentClass = () => { const classes = [["h-10"], ["h-9"]]; - return classes[Math.min(this.indent, classes.length-1)]; + return classes[Math.min(this.indent, classes.length - 1)]; }; - asElement = () => { return this._el; }; + asElement = () => { + return this._el; + }; - remove = () => { this._el.remove(); }; + remove = () => { + this._el.remove(); + }; update = (g: Group) => { this._group = g; @@ -555,7 +668,7 @@ class groupButton extends groupableItem { this.description = g.description; }; - append(item: HTMLElement|groupButton) { + append(item: HTMLElement | groupButton) { if (item instanceof groupButton) { item.button.classList.remove(...this._indentClasses); item.button.classList.add(...this._indentClass()); @@ -567,13 +680,17 @@ class groupButton extends groupableItem { } } - get name(): string { return this._group.name; } + get name(): string { + return this._group.name; + } set name(v: string) { this._group.name = v; this.button.querySelector(".group-button-name").textContent = v; } - get group(): string { return this._group.group; } + get group(): string { + return this._group.group; + } set group(v: string) { document.removeEventListener(`settings-group-${this.group}-child-visible`, this._childVisible); document.removeEventListener(`settings-group-${this.group}-child-hidden`, this._childHidden); @@ -586,10 +703,16 @@ class groupButton extends groupableItem { this._dropdown.setAttribute("data-group", v); } - get description(): string { return this._group.description; } - set description(v: string) { this._group.description = v; } + get description(): string { + return this._group.description; + } + set description(v: string) { + this._group.description = v; + } - get indent(): number { return this._indent; } + get indent(): number { + return this._indent; + } set indent(v: number) { this._dropdown.classList.remove(groupButton._margin); this._indent = v; @@ -597,10 +720,12 @@ class groupButton extends groupableItem { for (let child of this._dropdown.children) { child.classList.remove(...this._indentClasses); child.classList.add(...this._indentClass()); - }; + } } - get open(): boolean { return this._check.checked; } + get open(): boolean { + return this._check.checked; + } set open(v: boolean) { this.openCloseWithAnimation(v); } @@ -610,7 +735,7 @@ class groupButton extends groupableItem { // When groups are nested, the outer group's scrollHeight will obviously change when an // inner group is opened/closed. Instead of traversing the tree and adjusting the maxHeight property // each open/close, just set the maxHeight to 9999px once the animation is completed. - // On close, quickly set maxHeight back to ~scrollHeight, then animate to 0. + // On close, quickly set maxHeight back to ~scrollHeight, then animate to 0. if (this._check.checked) { this._icon.classList.add("rotated"); this._icon.classList.remove("not-rotated"); @@ -624,7 +749,7 @@ class groupButton extends groupableItem { this._parentSidebar.style.overflowY = ""; }; this._dropdown.addEventListener("transitionend", fullHeight); - this._dropdown.style.maxHeight = (1.2*this._dropdown.scrollHeight)+"px"; + this._dropdown.style.maxHeight = 1.2 * this._dropdown.scrollHeight + "px"; this._dropdown.style.opacity = "100%"; } else { this._icon.classList.add("not-rotated"); @@ -636,7 +761,7 @@ class groupButton extends groupableItem { this._parentSidebar.style.overflowY = ""; }; const mainTransitionStart = () => { - this._dropdown.removeEventListener("transitionend", mainTransitionStart) + this._dropdown.removeEventListener("transitionend", mainTransitionStart); this._dropdown.style.transitionDuration = ""; this._dropdown.addEventListener("transitionend", mainTransitionEnd); this._dropdown.style.maxHeight = "0"; @@ -648,7 +773,7 @@ class groupButton extends groupableItem { // so instead just make the transition duration really short. this._dropdown.style.transitionDuration = "1ms"; this._dropdown.addEventListener("transitionend", mainTransitionStart); - this._dropdown.style.maxHeight = (1.2*this._dropdown.scrollHeight)+"px"; + this._dropdown.style.maxHeight = 1.2 * this._dropdown.scrollHeight + "px"; } } @@ -669,17 +794,17 @@ class groupButton extends groupableItem { private _childVisible = () => { this.hidden = false; - } + }; private _childHidden = () => { for (let el of this._dropdown.children) { - if (!(el.classList.contains("unfocused"))) { + if (!el.classList.contains("unfocused")) { return; } } // All children are hidden, so hide ourself this.hidden = true; - } + }; // Takes sidebar as we need to disable scrolling on it when animation starts. constructor(parentSidebar: HTMLElement) { @@ -699,7 +824,7 @@ class groupButton extends groupableItem { `; - + this._dropdown = document.createElement("div") as HTMLDivElement; this._el.appendChild(this._dropdown); this._dropdown.style.maxHeight = "0"; @@ -714,11 +839,11 @@ class groupButton extends groupableItem { }; this._check.onclick = () => { this.open = this.open; - } + }; this.openCloseWithoutAnimation(false); } -}; +} interface Section { section: string; @@ -787,22 +912,27 @@ class sectionPanel { break; } if (setting.type != "note") { - this.values[setting.setting] = ""+setting.value; + this.values[setting.setting] = "" + setting.value; // settings-section-name: Implies the setting changed or was shown/hidden. // settings-set-section-name: Implies the setting changed. - document.addEventListener(`settings-set-${this._sectionName}-${setting.setting}`, (event: CustomEvent) => { - // const oldValue = this.values[name]; - this.values[setting.setting] = event.detail; - document.dispatchEvent(new CustomEvent("settings-section-changed")); - }); + document.addEventListener( + `settings-set-${this._sectionName}-${setting.setting}`, + (event: CustomEvent) => { + // const oldValue = this.values[name]; + this.values[setting.setting] = event.detail; + document.dispatchEvent(new CustomEvent("settings-section-changed")); + }, + ); } this._section.appendChild(setting.asElement()); this._settings[setting.setting] = setting; } } + }; + + get visible(): boolean { + return !this._section.classList.contains("unfocused"); } - - get visible(): boolean { return !this._section.classList.contains("unfocused"); } set visible(s: boolean) { if (s) { this._section.classList.remove("unfocused"); @@ -811,7 +941,9 @@ class sectionPanel { } } - asElement = (): HTMLDivElement => { return this._section; } + asElement = (): HTMLDivElement => { + return this._section; + }; } type Member = { group: string } | { section: string }; @@ -830,28 +962,40 @@ class sectionButton extends groupableItem { this._registerDependencies(); }; - get subButton(): HTMLElement { return this._subButton.children[0] as HTMLElement; } - set subButton(v: HTMLElement) { this._subButton.replaceChildren(v); } + get subButton(): HTMLElement { + return this._subButton.children[0] as HTMLElement; + } + set subButton(v: HTMLElement) { + this._subButton.replaceChildren(v); + } - get name(): string { return this._meta.name; } + get name(): string { + return this._meta.name; + } set name(v: string) { this._meta.name = v; this._name.textContent = v; - }; + } - get depends_true(): string { return this._meta.depends_true; } + get depends_true(): string { + return this._meta.depends_true; + } set depends_true(v: string) { this._meta.depends_true = v; this._registerDependencies(); } - - get depends_false(): string { return this._meta.depends_false; } + + get depends_false(): string { + return this._meta.depends_false; + } set depends_false(v: string) { this._meta.depends_false = v; this._registerDependencies(); } - get selected(): boolean { return this._el.classList.contains("selected"); } + get selected(): boolean { + return this._el.classList.contains("selected"); + } set selected(v: boolean) { if (v) this._el.classList.add("selected"); else this._el.classList.remove("selected"); @@ -859,17 +1003,19 @@ class sectionButton extends groupableItem { select = () => { document.dispatchEvent(new CustomEvent("settings-show-panel", { detail: this.section })); - } + }; private _registerDependencies() { // Doesn't re-register dependencies, but that isn't important in this application if (!(this._meta.depends_true || this._meta.depends_false)) return; let [sect, dependant] = splitDependant(this.section, this._meta.depends_true || this._meta.depends_false); - let state = !(Boolean(this._meta.depends_false)); + let state = !Boolean(this._meta.depends_false); document.addEventListener(`settings-${sect}-${dependant}`, (event: settingsChangedEvent) => { - console.log(`recieved settings-${sect}-${dependant} = ${event.detail.value} = ${toBool(event.detail.value)} / ${event.detail.hidden}`); - const hide = event.detail.hidden || (toBool(event.detail.value) !== state); + console.log( + `recieved settings-${sect}-${dependant} = ${event.detail.value} = ${toBool(event.detail.value)} / ${event.detail.hidden}`, + ); + const hide = event.detail.hidden || toBool(event.detail.value) !== state; this.hidden = hide; document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: !hide })); }); @@ -878,19 +1024,21 @@ class sectionButton extends groupableItem { this.hidden = true; document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false })); } - }); + }); } private _advancedListener = (event: advancedEvent) => { - if (!(event.detail)) { + if (!event.detail) { this._el.classList.add("unfocused"); } else { this._el.classList.remove("unfocused"); } document.dispatchEvent(new CustomEvent("settings-re-search")); - } + }; - get advanced(): boolean { return this._meta.advanced } + get advanced(): boolean { + return this._meta.advanced; + } set advanced(v: boolean) { this._meta.advanced = v; if (v) document.addEventListener("settings-advancedState", this._advancedListener); @@ -917,7 +1065,7 @@ class sectionButton extends groupableItem { interface Settings { groups: Group[]; sections: Section[]; - order?: Member[]; + order?: Member[]; } export class settingsList { @@ -926,13 +1074,13 @@ export class settingsList { private _saveRestart = document.getElementById("settings-apply-restart") as HTMLSpanElement; private _loader = document.getElementById("settings-loader") as HTMLDivElement; - + private _panel = document.getElementById("settings-panel") as HTMLDivElement; private _sidebar = document.getElementById("settings-sidebar-items") as HTMLDivElement; private _visibleSection: string; private _sections: { [name: string]: sectionPanel }; private _buttons: { [name: string]: sectionButton }; - + private _groups: { [name: string]: Group }; private _groupButtons: { [name: string]: groupButton }; @@ -942,7 +1090,9 @@ export class settingsList { private _advanced: boolean = false; private _searchbox = document.getElementById("settings-search") as HTMLInputElement; - private _clearSearchboxButtons = Array.from(document.getElementsByClassName("settings-search-clear")) as Array; + private _clearSearchboxButtons = Array.from( + document.getElementsByClassName("settings-search-clear"), + ) as Array; private _noResultsPanel: HTMLElement = document.getElementById("settings-not-found"); @@ -955,7 +1105,9 @@ export class settingsList { // Must be called -after- all section have been added. // Takes all groups at once since members might contain each other. addGroups = (groups: Group[]) => { - groups.forEach((g) => { this._groups[g.group] = g }); + groups.forEach((g) => { + this._groups[g.group] = g; + }); const addGroup = (g: Group, indent: number = 0): groupButton => { if (g.group in this._groupButtons) return null; @@ -965,7 +1117,7 @@ export class settingsList { for (const member of g.members) { if ("group" in member) { - let subgroup = addGroup(this._groups[member.group], indent+1); + let subgroup = addGroup(this._groups[member.group], indent + 1); if (!subgroup) { subgroup = this._groupButtons[member.group]; // Remove from page @@ -979,10 +1131,10 @@ export class settingsList { container.append(subsection.asElement()); } } - + this._groupButtons[g.group] = container; return container; - } + }; for (let g of groups) { const container = addGroup(g); if (container) { @@ -990,7 +1142,7 @@ export class settingsList { container.openCloseWithoutAnimation(false); } } - } + }; addSection = (name: string, s: Section, subButton?: HTMLElement) => { const section = new sectionPanel(s, name); @@ -1000,7 +1152,7 @@ export class settingsList { if (subButton) button.subButton = subButton; this._buttons[name] = button; this._sidebar.appendChild(button.asElement()); - } + }; private _traverseMemberList = (list: Member[], func: (sect: string) => void) => { for (const member of list) { @@ -1015,7 +1167,7 @@ export class settingsList { func(member.section); } } - } + }; setUIOrder(order: Member[]) { this._sidebar.textContent = ``; @@ -1041,50 +1193,54 @@ export class settingsList { this._visibleSection = name; } } - } + }; private _save = () => { let config = {}; for (let name in this._sections) { config[name] = this._sections[name].values; } - if (this._needsRestart) { + if (this._needsRestart) { this._saveRestart.onclick = () => { config["restart-program"] = true; this._send(config, () => { - window.modals.settingsRestart.close(); + window.modals.settingsRestart.close(); window.modals.settingsRefresh.show(); }); }; this._saveNoRestart.onclick = () => { config["restart-program"] = false; - this._send(config, window.modals.settingsRestart.close); - } - window.modals.settingsRestart.show(); + this._send(config, window.modals.settingsRestart.close); + }; + window.modals.settingsRestart.show(); } else { this._send(config); } // console.log(config); - } + }; - private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 200 || req.status == 204) { - window.notifications.customSuccess("settingsSaved", window.lang.notif("saveSettings")); - } else { - window.notifications.customError("settingsSaved", window.lang.notif("errorSaveSettings")); + private _send = (config: Object, run?: () => void) => + _post("/config", config, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + window.notifications.customSuccess("settingsSaved", window.lang.notif("saveSettings")); + } else { + window.notifications.customError("settingsSaved", window.lang.notif("errorSaveSettings")); + } + this.reload(); + if (run) { + run(); + } } - this.reload(); - if (run) { run(); } - } - }); + }); - private _showLogs = () => _get("/logs", null, (req: XMLHttpRequest) => { - if (req.readyState == 4 && req.status == 200) { - (document.getElementById("log-area") as HTMLPreElement).textContent = req.response["log"] as string; - window.modals.logs.show(); - } - }); + private _showLogs = () => + _get("/logs", null, (req: XMLHttpRequest) => { + if (req.readyState == 4 && req.status == 200) { + (document.getElementById("log-area") as HTMLPreElement).textContent = req.response["log"] as string; + window.modals.logs.show(); + } + }); setBackupSort = (ascending: boolean) => { this._backupSortAscending = ascending; @@ -1092,40 +1248,52 @@ export class settingsList { this._getBackups(); }; - private _backup = () => _post("/backups", null, (req: XMLHttpRequest) => { - if (req.readyState != 4 || req.status != 200) return; - const backupDTO = req.response as BackupDTO; - if (backupDTO.path == "") { - window.notifications.customError("backupError", window.lang.strings("errorFailureCheckLogs")); - return; - } - const location = document.getElementById("settings-backed-up-location"); - const download = document.getElementById("settings-backed-up-download"); - location.innerHTML = window.lang.strings("backupCanBeFound").replace("{filepath}", `"`+backupDTO.path+`"`); - download.innerHTML = ` + private _backup = () => + _post( + "/backups", + null, + (req: XMLHttpRequest) => { + if (req.readyState != 4 || req.status != 200) return; + const backupDTO = req.response as BackupDTO; + if (backupDTO.path == "") { + window.notifications.customError("backupError", window.lang.strings("errorFailureCheckLogs")); + return; + } + const location = document.getElementById("settings-backed-up-location"); + const download = document.getElementById("settings-backed-up-download"); + location.innerHTML = window.lang + .strings("backupCanBeFound") + .replace( + "{filepath}", + `"` + backupDTO.path + `"`, + ); + download.innerHTML = ` ${window.lang.strings("download")} ${backupDTO.size} `; - - download.parentElement.onclick = () => _download("/backups/" + backupDTO.name, backupDTO.name); - window.modals.backedUp.show(); - }, true); - private _getBackups = () => _get("/backups", null, (req: XMLHttpRequest) => { - if (req.readyState != 4 || req.status != 200) return; - const backups = req.response["backups"] as BackupDTO[]; - const table = document.getElementById("backups-list"); - table.textContent = ``; - if (!this._backupSortAscending) { - backups.reverse(); - } - for (let b of backups) { - const tr = document.createElement("tr") as HTMLTableRowElement; - tr.classList.add("align-middle"); - tr.innerHTML = ` + download.parentElement.onclick = () => _download("/backups/" + backupDTO.name, backupDTO.name); + window.modals.backedUp.show(); + }, + true, + ); + + private _getBackups = () => + _get("/backups", null, (req: XMLHttpRequest) => { + if (req.readyState != 4 || req.status != 200) return; + const backups = req.response["backups"] as BackupDTO[]; + const table = document.getElementById("backups-list"); + table.textContent = ``; + if (!this._backupSortAscending) { + backups.reverse(); + } + for (let b of backups) { + const tr = document.createElement("tr") as HTMLTableRowElement; + tr.classList.add("align-middle"); + tr.innerHTML = ` ${b.name} - ${toDateString(new Date(b.date*1000))} + ${toDateString(new Date(b.date * 1000))} ${b.commit || "?"}
@@ -1135,17 +1303,20 @@ export class settingsList {
`; - SetupCopyButton(tr.querySelector(".backup-copy"), b.path, null, window.lang.notif("pathCopied")); - tr.querySelector(".backup-download").addEventListener("click", () => _download("/backups/" + b.name, b.name)); - tr.querySelector(".backup-restore").addEventListener("click", () => { - _post("/backups/restore/"+b.name, null, () => {}); - window.modals.backups.close(); - window.modals.settingsRefresh.modal.querySelector("span.heading").textContent = window.lang.strings("settingsRestarting"); - window.modals.settingsRefresh.show(); - }); - table.appendChild(tr); - } - }); + SetupCopyButton(tr.querySelector(".backup-copy"), b.path, null, window.lang.notif("pathCopied")); + tr.querySelector(".backup-download").addEventListener("click", () => + _download("/backups/" + b.name, b.name), + ); + tr.querySelector(".backup-restore").addEventListener("click", () => { + _post("/backups/restore/" + b.name, null, () => {}); + window.modals.backups.close(); + window.modals.settingsRefresh.modal.querySelector("span.heading").textContent = + window.lang.strings("settingsRestarting"); + window.modals.settingsRefresh.show(); + }); + table.appendChild(tr); + } + }); constructor() { this._groups = {}; @@ -1155,11 +1326,14 @@ export class settingsList { document.addEventListener("settings-section-changed", () => this._saveButton.classList.remove("unfocused")); document.getElementById("settings-restart").onclick = () => { _post("/restart", null, () => {}); - window.modals.settingsRefresh.modal.querySelector("span.heading").textContent = window.lang.strings("settingsRestarting"); + window.modals.settingsRefresh.modal.querySelector("span.heading").textContent = + window.lang.strings("settingsRestarting"); window.modals.settingsRefresh.show(); }; this._saveButton.onclick = this._save; - document.addEventListener("settings-requires-restart", () => { this._needsRestart = true; }); + document.addEventListener("settings-requires-restart", () => { + this._needsRestart = true; + }); document.getElementById("settings-logs").onclick = this._showLogs; document.getElementById("settings-backups-backup").onclick = () => { window.modals.backups.close(); @@ -1177,12 +1351,12 @@ export class settingsList { this.setBackupSort(this._backupSortAscending); window.modals.backups.show(); }; - this._backupSortDirection.onclick = () => this.setBackupSort(!(this._backupSortAscending)); + this._backupSortDirection.onclick = () => this.setBackupSort(!this._backupSortAscending); const advancedEnableToggle = document.getElementById("settings-advanced-enabled") as HTMLInputElement; const filedlg = document.getElementById("backups-file") as HTMLInputElement; document.getElementById("settings-backups-upload").onclick = () => { - filedlg.click(); + filedlg.click(); }; filedlg.addEventListener("change", () => { if (filedlg.files.length == 0) return; @@ -1190,7 +1364,8 @@ export class settingsList { form.append("backups-file", filedlg.files[0], filedlg.files[0].name); _upload("/backups/restore", form); window.modals.backups.close(); - window.modals.settingsRefresh.modal.querySelector("span.heading").textContent = window.lang.strings("settingsRestarting"); + window.modals.settingsRefresh.modal.querySelector("span.heading").textContent = + window.lang.strings("settingsRestarting"); window.modals.settingsRefresh.show(); }); @@ -1216,7 +1391,7 @@ export class settingsList { this._searchbox.oninput = () => { this.search(this._searchbox.value); }; - + document.addEventListener("settings-re-search", () => { this._searchbox.oninput(null); }); @@ -1226,8 +1401,8 @@ export class settingsList { this._searchbox.value = ""; this._searchbox.oninput(null); }; - }; - + } + // Create (restart)required badges (can't do on load as window.lang is unset) RestartRequiredBadge = (() => { const rr = document.createElement("span"); @@ -1261,29 +1436,40 @@ export class settingsList { let send = { homeserver: (document.getElementById("matrix-homeserver") as HTMLInputElement).value, username: (document.getElementById("matrix-user") as HTMLInputElement).value, - password: (document.getElementById("matrix-password") as HTMLInputElement).value - } - _post("/matrix/login", send, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - removeLoader(button); - if (req.status == 400) { - window.notifications.customError("errorUnknown", window.lang.notif(req.response["error"] as string)); - return; - } else if (req.status == 401) { - window.notifications.customError("errorUnauthorized", req.response["error"] as string); - return; - } else if (req.status == 500) { - window.notifications.customError("errorAddMatrix", window.lang.notif("errorFailureCheckLogs")); - return; + password: (document.getElementById("matrix-password") as HTMLInputElement).value, + }; + _post( + "/matrix/login", + send, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + removeLoader(button); + if (req.status == 400) { + window.notifications.customError( + "errorUnknown", + window.lang.notif(req.response["error"] as string), + ); + return; + } else if (req.status == 401) { + window.notifications.customError("errorUnauthorized", req.response["error"] as string); + return; + } else if (req.status == 500) { + window.notifications.customError( + "errorAddMatrix", + window.lang.notif("errorFailureCheckLogs"), + ); + return; + } + window.modals.matrix.close(); + _post("/restart", null, () => {}); + window.location.reload(); } - window.modals.matrix.close(); - _post("/restart", null, () => {}); - window.location.reload(); - } - }, true); + }, + true, + ); }; window.modals.matrix.show(); - } + }; reload = () => { for (let i = 0; i < this._loader.children.length; i++) { @@ -1374,10 +1560,9 @@ export class settingsList { document.dispatchEvent(new CustomEvent("settings-advancedState", { detail: false })); this._saveButton.classList.add("unfocused"); this._needsRestart = false; - }) + }); }; - private _query: string; // FIXME: Fix searching groups // FIXME: Search "About" & "User profiles", pseudo-search "User profiles" for things like "Ombi", "Referrals", etc. @@ -1388,7 +1573,7 @@ export class settingsList { const noChange = query == this._query; let firstVisibleSection = ""; - + // Close and hide all groups to start with for (const groupButton of Object.values(this._groupButtons)) { // Leave these opened/closed if the query didn't change @@ -1397,9 +1582,11 @@ export class settingsList { // changed like advanced settings being enabled). if (noChange && query == "") continue; groupButton.openCloseWithoutAnimation(false); - groupButton.hidden = !(groupButton.group.toLowerCase().includes(query) || - groupButton.name.toLowerCase().includes(query) || - groupButton.description.toLowerCase().includes(query)); + groupButton.hidden = !( + groupButton.group.toLowerCase().includes(query) || + groupButton.name.toLowerCase().includes(query) || + groupButton.description.toLowerCase().includes(query) + ); } const searchSection = (section: Section) => { @@ -1422,7 +1609,7 @@ export class settingsList { let matchedGroup = false; if (parentGroup) { parentGroupButton = this._groupButtons[parentGroup]; - matchedGroup = !(parentGroupButton.hidden); + matchedGroup = !parentGroupButton.hidden; } const show = () => { @@ -1430,18 +1617,20 @@ export class settingsList { if (parentGroupButton) { if (query != "") parentGroupButton.openCloseWithoutAnimation(true); } - } + }; const hide = () => { button.hidden = true; - } + }; + + let matchedSection = + matchedGroup || + section.section.toLowerCase().includes(query) || + section.meta.name.toLowerCase().includes(query) || + section.meta.description.toLowerCase().includes(query); + if (section.meta.aliases) + section.meta.aliases.forEach((term: string) => (matchedSection ||= term.toLowerCase().includes(query))); + matchedSection &&= (section.meta.advanced && this._advanced) || !section.meta.advanced; - let matchedSection = matchedGroup || - section.section.toLowerCase().includes(query) || - section.meta.name.toLowerCase().includes(query) || - section.meta.description.toLowerCase().includes(query); - if (section.meta.aliases) section.meta.aliases.forEach((term: string) => matchedSection ||= term.toLowerCase().includes(query)); - matchedSection &&= ((section.meta.advanced && this._advanced) || !(section.meta.advanced)); - if (matchedSection) { show(); firstVisibleSection = firstVisibleSection || section.section; @@ -1464,24 +1653,27 @@ export class settingsList { element.classList.add("opacity-50", "pointer-events-none"); element.setAttribute("aria-disabled", "true"); - let matchedSetting = setting.setting.toLowerCase().includes(query) || - setting.name.toLowerCase().includes(query) || - setting.description.toLowerCase().includes(query) || - String(setting.value).toLowerCase().includes(query); - if (setting.aliases) setting.aliases.forEach((term: string) => matchedSetting ||= term.toLowerCase().includes(query)); + let matchedSetting = + setting.setting.toLowerCase().includes(query) || + setting.name.toLowerCase().includes(query) || + setting.description.toLowerCase().includes(query) || + String(setting.value).toLowerCase().includes(query); + if (setting.aliases) + setting.aliases.forEach((term: string) => (matchedSetting ||= term.toLowerCase().includes(query))); if (matchedSetting) { - if ((section.meta.advanced && this._advanced) || !(section.meta.advanced)) { + if ((section.meta.advanced && this._advanced) || !section.meta.advanced) { show(); firstVisibleSection = firstVisibleSection || section.section; } - const shouldShow = (query != "" && - ((setting.advanced && this._advanced) || - !(setting.advanced))); + const shouldShow = query != "" && ((setting.advanced && this._advanced) || !setting.advanced); if (shouldShow || query == "") { element.classList.remove("opacity-50", "pointer-events-none"); element.setAttribute("aria-disabled", "false"); } - if (query != "" && ((shouldShow && element.querySelector("label").classList.contains("unfocused")) || (!shouldShow))) { + if ( + query != "" && + ((shouldShow && element.querySelector("label").classList.contains("unfocused")) || !shouldShow) + ) { // Add a note explaining why the setting is hidden if (!dependencyCard) { dependencyCard = document.createElement("aside"); @@ -1493,9 +1685,18 @@ export class settingsList {
    `; - dependencyList = dependencyCard.querySelector(".settings-dependency-list") as HTMLUListElement; + dependencyList = dependencyCard.querySelector( + ".settings-dependency-list", + ) as HTMLUListElement; // Insert it right after the description - this._sections[section.section].asElement().insertBefore(dependencyCard, this._sections[section.section].asElement().querySelector(".settings-section-description").nextElementSibling); + this._sections[section.section] + .asElement() + .insertBefore( + dependencyCard, + this._sections[section.section] + .asElement() + .querySelector(".settings-section-description").nextElementSibling, + ); } const li = document.createElement("li"); if (shouldShow) { @@ -1507,9 +1708,14 @@ export class settingsList { depName = this._settings.sections[dep[0]].meta.name + " > " + depName; } - li.textContent = window.lang.strings("settingsDependsOn").replace("{setting}", `"`+setting.name+`"`).replace("{dependency}", `"`+depName+`"`); + li.textContent = window.lang + .strings("settingsDependsOn") + .replace("{setting}", `"` + setting.name + `"`) + .replace("{dependency}", `"` + depName + `"`); } else { - li.textContent = window.lang.strings("settingsAdvancedMode").replace("{setting}", `"`+setting.name+`"`); + li.textContent = window.lang + .strings("settingsAdvancedMode") + .replace("{setting}", `"` + setting.name + `"`); } dependencyList.appendChild(li); } @@ -1520,7 +1726,7 @@ export class settingsList { for (let section of this._settings.sections) { searchSection(section); } - + if (firstVisibleSection && (query != "" || this._visibleSection == "")) { this._buttons[firstVisibleSection].select(); this._noResultsPanel.classList.add("unfocused"); @@ -1535,7 +1741,7 @@ export class settingsList { // We can use this later to tell if we should leave groups expanded/closed as they were. this._query = query; - } + }; } export interface templateEmail { @@ -1581,7 +1787,7 @@ class MessageEditor { } if (this._names[id] !== undefined) { this._header.textContent = this._names[id].name; - } + } this._aside.classList.add("unfocused"); if (this._names[id].description != "") { this._aside.textContent = this._names[id].description; @@ -1599,59 +1805,63 @@ class MessageEditor { this.loadPreview(); this._content = this._templ.content; const colors = ["info", "urge", "positive", "neutral"]; - let innerHTML = ''; + let innerHTML = ""; for (let i = 0; i < this._templ.variables.length; i++) { let ci = i % colors.length; - innerHTML += '' + innerHTML += ''; } if (this._templ.variables.length == 0) { this._variablesLabel.classList.add("unfocused"); } else { this._variablesLabel.classList.remove("unfocused"); } - this._variables.innerHTML = innerHTML + this._variables.innerHTML = innerHTML; let buttons = this._variables.querySelectorAll("span.button") as NodeListOf; for (let i = 0; i < this._templ.variables.length; i++) { - buttons[i].innerHTML = `` + "{" + this._templ.variables[i] + "}" + ``; + buttons[i].innerHTML = + `` + "{" + this._templ.variables[i] + "}" + ``; buttons[i].onclick = () => { insertText(this._textArea, "{" + this._templ.variables[i] + "}"); this.loadPreview(); // this._timeout = setTimeout(this.loadPreview, this._finishInterval); - } + }; } - innerHTML = ''; + innerHTML = ""; if (this._templ.conditionals == null || this._templ.conditionals.length == 0) { this._conditionalsLabel.classList.add("unfocused"); this._conditionals.textContent = ``; } else { - for (let i = this._templ.conditionals.length-1; i >= 0; i--) { + for (let i = this._templ.conditionals.length - 1; i >= 0; i--) { let ci = i % colors.length; // FIXME: Store full color strings (with ~) so tailwind sees them. - innerHTML += '' + innerHTML += ''; } this._conditionalsLabel.classList.remove("unfocused"); - this._conditionals.innerHTML = innerHTML + this._conditionals.innerHTML = innerHTML; buttons = this._conditionals.querySelectorAll("span.button") as NodeListOf; for (let i = 0; i < this._templ.conditionals.length; i++) { - buttons[i].innerHTML = `{if ` + this._templ.conditionals[i] + "}" + ``; + buttons[i].innerHTML = + `{if ` + this._templ.conditionals[i] + "}" + ``; buttons[i].onclick = () => { insertText(this._textArea, "{if " + this._templ.conditionals[i] + "}" + "{endif}"); this.loadPreview(); // this._timeout = setTimeout(this.loadPreview, this._finishInterval); - } + }; } } window.modals.editor.show(); } - }) - } + }); + }; loadPreview = () => { let content = this._textArea.value; if (this._templ.variables) { for (let variable of this._templ.variables) { let value = this._templ.values[variable]; - if (value === undefined) { value = "{" + variable + "}"; } + if (value === undefined) { + value = "{" + variable + "}"; + } content = content.replace(new RegExp("{" + variable + "}", "g"), value); } } @@ -1671,63 +1881,75 @@ class MessageEditor { // this._preview.innerHTML = (req.response as Email).html; // } // }, true); - } + }; showList = (filter?: string) => { - _get("/config/emails?lang=" + window.language + (filter ? "&filter=" + filter : ""), null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 200) { - window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs")); - return; - } - this._names = req.response; - const list = document.getElementById("customize-list") as HTMLDivElement; - list.textContent = ''; - for (let id in this._names) { - const tr = document.createElement("tr") as HTMLTableRowElement; - let resetButton = ``; - if (this._names[id].enabled) { - resetButton = ``; + _get( + "/config/emails?lang=" + window.language + (filter ? "&filter=" + filter : ""), + null, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) { + window.notifications.customError( + "loadTemplateError", + window.lang.notif("errorFailureCheckLogs"), + ); + return; } - let innerHTML = ` + this._names = req.response; + const list = document.getElementById("customize-list") as HTMLDivElement; + list.textContent = ""; + for (let id in this._names) { + const tr = document.createElement("tr") as HTMLTableRowElement; + let resetButton = ``; + if (this._names[id].enabled) { + resetButton = ``; + } + let innerHTML = ` ${this._names[id].name} `; - if (this._names[id].description != "") innerHTML += ` + if (this._names[id].description != "") + innerHTML += `
    ${this._names[id].description}
    `; - innerHTML += ` + innerHTML += ` ${resetButton} `; - tr.innerHTML = innerHTML; - (tr.querySelector("span.button") as HTMLSpanElement).onclick = () => { - window.modals.customizeEmails.close() - this.loadEditor(id); - }; - if (this._names[id].enabled) { - const rb = tr.querySelector("span.customize-reset") as HTMLElement; - rb.classList.add("button"); - rb.onclick = () => _post("/config/emails/" + id + "/state/disable", null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 200 && req.status != 204) { - window.notifications.customError("setEmailStateError", window.lang.notif("errorFailureCheckLogs")); - return; - } - rb.remove(); - } - }); + tr.innerHTML = innerHTML; + (tr.querySelector("span.button") as HTMLSpanElement).onclick = () => { + window.modals.customizeEmails.close(); + this.loadEditor(id); + }; + if (this._names[id].enabled) { + const rb = tr.querySelector("span.customize-reset") as HTMLElement; + rb.classList.add("button"); + rb.onclick = () => + _post("/config/emails/" + id + "/state/disable", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200 && req.status != 204) { + window.notifications.customError( + "setEmailStateError", + window.lang.notif("errorFailureCheckLogs"), + ); + return; + } + rb.remove(); + } + }); + } + list.appendChild(tr); } - list.appendChild(tr); + window.modals.customizeEmails.show(); } - window.modals.customizeEmails.show(); - } - }); - } + }, + ); + }; constructor() { this._textArea.onkeyup = () => { @@ -1740,12 +1962,12 @@ class MessageEditor { // }; this._form.onsubmit = (event: Event) => { - event.preventDefault() + event.preventDefault(); if (this._textArea.value == this._content && this._names[this._currentID].enabled) { window.modals.editor.close(); return; } - _post("/config/emails/" + this._currentID, { "content": this._textArea.value }, (req: XMLHttpRequest) => { + _post("/config/emails/" + this._currentID, { content: this._textArea.value }, (req: XMLHttpRequest) => { if (req.readyState == 4) { window.modals.editor.close(); if (req.status != 200) { @@ -1757,36 +1979,39 @@ class MessageEditor { }); }; - const descriptions = document.getElementsByClassName("editor-syntax-description") as HTMLCollectionOf; + const descriptions = document.getElementsByClassName( + "editor-syntax-description", + ) as HTMLCollectionOf; for (let el of descriptions) { el.innerHTML = window.lang.template("strings", "syntaxDescription", { - "variable": `{varname}`, - "ifTruth": `{if address}Message sent to {address}{end}`, - "ifCompare": `{if profile == "Friends"}Friend{else if profile != "Admins"}User{end}` + variable: `{varname}`, + ifTruth: `{if address}Message sent to {address}{end}`, + ifCompare: `{if profile == "Friends"}Friend{else if profile != "Admins"}User{end}`, }); - }; + } // Get rid of nasty CSS window.modals.editor.onclose = () => { this._preview.textContent = ``; - } + }; } } class TasksList { private _list: HTMLElement = document.getElementById("modal-tasks-list"); - load = () => _get("/tasks", null, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - if (req.status != 200) return; - let resp = req.response["tasks"] as TaskDTO[]; - this._list.textContent = ""; - for (let t of resp) { - const task = new Task(t); - this._list.appendChild(task.asElement()); - } - window.modals.tasks.show(); - }); + load = () => + _get("/tasks", null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + if (req.status != 200) return; + let resp = req.response["tasks"] as TaskDTO[]; + this._list.textContent = ""; + for (let t of resp) { + const task = new Task(t); + this._list.appendChild(task.asElement()); + } + window.modals.tasks.show(); + }); } interface TaskDTO { @@ -1797,10 +2022,12 @@ interface TaskDTO { class Task { private _el: HTMLElement; - asElement = () => { return this._el }; + asElement = () => { + return this._el; + }; constructor(t: TaskDTO) { this._el = document.createElement("div"); - this._el.classList.add("aside", "flex", "flex-row", "gap-4", "justify-between", "dark:shadow-md") + this._el.classList.add("aside", "flex", "flex-row", "gap-4", "justify-between", "dark:shadow-md"); this._el.innerHTML = `
    @@ -1817,13 +2044,13 @@ class Task { _post(t.url, null, (req: XMLHttpRequest) => { if (req.readyState != 4) return; removeLoader(button); - setTimeout(window.modals.tasks.close, 1000) + setTimeout(window.modals.tasks.close, 1000); if (req.status != 204) { window.notifications.customError("errorRunTask", window.lang.notif("errorFailureCheckLogs")); return; } window.notifications.customSuccess("runTask", window.lang.notif("runTask")); - }) - } + }); + }; } } diff --git a/ts/modules/stripmd.ts b/ts/modules/stripmd.ts index 39381ab..402ea76 100644 --- a/ts/modules/stripmd.ts +++ b/ts/modules/stripmd.ts @@ -1,38 +1,38 @@ const removeMd = require("remove-markdown"); function stripAltText(md: string): string { - let altStart = -1; // Start of alt text (between '[' & ']') - let urlStart = -1; // Start of url (between '(' & ')') - let urlEnd = -1; - let prevURLEnd = -2; - let out = ""; + let altStart = -1; // Start of alt text (between '[' & ']') + let urlStart = -1; // Start of url (between '(' & ')') + let urlEnd = -1; + let prevURLEnd = -2; + let out = ""; for (let i = 0; i < md.length; i++) { - if (altStart != -1 && urlStart != -1 && md.charAt(i) == ')') { - urlEnd = i - 1; - out += md.substring(prevURLEnd+2, altStart-1) + md.substring(urlStart, urlEnd+1); - prevURLEnd = urlEnd; - altStart = -1; + if (altStart != -1 && urlStart != -1 && md.charAt(i) == ")") { + urlEnd = i - 1; + out += md.substring(prevURLEnd + 2, altStart - 1) + md.substring(urlStart, urlEnd + 1); + prevURLEnd = urlEnd; + altStart = -1; urlStart = -1; urlEnd = -1; - continue; - } - if (md.charAt(i) == '[' && altStart == -1) { - altStart = i + 1 - if (i > 0 && md.charAt(i-1) == '!') { - altStart-- - } - } - if (i > 0 && md.charAt(i-1) == ']' && md.charAt(i) == '(' && urlStart == -1) { - urlStart = i + 1 - } - } + continue; + } + if (md.charAt(i) == "[" && altStart == -1) { + altStart = i + 1; + if (i > 0 && md.charAt(i - 1) == "!") { + altStart--; + } + } + if (i > 0 && md.charAt(i - 1) == "]" && md.charAt(i) == "(" && urlStart == -1) { + urlStart = i + 1; + } + } if (prevURLEnd + 1 != md.length - 1) { - out += md.substring(prevURLEnd+2) + out += md.substring(prevURLEnd + 2); } if (out == "") { - return md + return md; } - return out + return out; } export function stripMarkdown(md: string): string { diff --git a/ts/modules/tabs.ts b/ts/modules/tabs.ts index ade6824..130c064 100644 --- a/ts/modules/tabs.ts +++ b/ts/modules/tabs.ts @@ -8,15 +8,14 @@ export interface Tab { postFunc?: () => void; } - export class Tabs implements Tabs { private _current: string = ""; private _baseOffset = -1; tabs: Map; pages: PageManager; - + constructor() { - this.tabs = new Map; + this.tabs = new Map(); this.pages = new PageManager({ hideOthersOnPageShow: true, defaultName: "invites", @@ -24,7 +23,13 @@ export class Tabs implements Tabs { }); } - addTab = (tabID: string, url: string, preFunc = () => void {}, postFunc = () => void {}, unloadFunc = () => void {}) => { + addTab = ( + tabID: string, + url: string, + preFunc = () => void {}, + postFunc = () => void {}, + unloadFunc = () => void {}, + ) => { let tab: Tab = { page: null, tabEl: document.getElementById("tab-" + tabID) as HTMLDivElement, @@ -37,12 +42,12 @@ export class Tabs implements Tabs { } tab.page = { name: tabID, - title: document.title, /*FIXME: Get actual names from translations*/ + title: document.title /*FIXME: Get actual names from translations*/, url: url, show: () => { tab.buttonEl.classList.add("active", "~urge"); tab.tabEl.classList.remove("unfocused"); - tab.buttonEl.parentElement.scrollTo(tab.buttonEl.offsetLeft-this._baseOffset, 0); + tab.buttonEl.parentElement.scrollTo(tab.buttonEl.offsetLeft - this._baseOffset, 0); document.dispatchEvent(new CustomEvent("tab-change", { detail: tabID })); return true; }, @@ -56,23 +61,33 @@ export class Tabs implements Tabs { shouldSkip: () => false, }; this.pages.setPage(tab.page); - tab.buttonEl.onclick = () => { this.switch(tabID); }; + tab.buttonEl.onclick = () => { + this.switch(tabID); + }; this.tabs.set(tabID, tab); - } + }; - get current(): string { return this._current; } - set current(tabID: string) { this.switch(tabID); } + get current(): string { + return this._current; + } + set current(tabID: string) { + this.switch(tabID); + } switch = (tabID: string, noRun: boolean = false) => { let t = this.tabs.get(tabID); if (t == undefined) { [t] = this.tabs.values(); } - + this._current = t.page.name; - if (t.preFunc && !noRun) { t.preFunc(); } + if (t.preFunc && !noRun) { + t.preFunc(); + } this.pages.load(tabID); - if (t.postFunc && !noRun) { t.postFunc(); } - } + if (t.postFunc && !noRun) { + t.postFunc(); + } + }; } diff --git a/ts/modules/theme.ts b/ts/modules/theme.ts index 7638f20..e56b95b 100644 --- a/ts/modules/theme.ts +++ b/ts/modules/theme.ts @@ -1,21 +1,19 @@ export class ThemeManager { - private _themeButton: HTMLElement = null; private _metaTag: HTMLMetaElement; - + private _cssLightFiles: HTMLLinkElement[]; private _cssDarkFiles: HTMLLinkElement[]; - private _beforeTransition = () => { const doc = document.documentElement; const onTransitionDone = () => { - doc.classList.remove('nightwind'); - doc.removeEventListener('transitionend', onTransitionDone); - } - doc.addEventListener('transitionend', onTransitionDone); - if (!doc.classList.contains('nightwind')) { - doc.classList.add('nightwind'); + doc.classList.remove("nightwind"); + doc.removeEventListener("transitionend", onTransitionDone); + }; + doc.addEventListener("transitionend", onTransitionDone); + if (!doc.classList.contains("nightwind")) { + doc.classList.add("nightwind"); } }; @@ -40,20 +38,24 @@ export class ThemeManager { this._themeButton = button; this._themeButton.onclick = this.toggle; this._updateThemeIcon(); - } + }; toggle = () => { this._toggle(); if (this._themeButton) { this._updateThemeIcon(); } - } + }; constructor(button?: HTMLElement) { this._metaTag = document.querySelector("meta[name=color-scheme]") as HTMLMetaElement; - - this._cssLightFiles = Array.from(document.head.querySelectorAll("link[data-theme=light]")) as Array; - this._cssDarkFiles = Array.from(document.head.querySelectorAll("link[data-theme=dark]")) as Array; + + this._cssLightFiles = Array.from( + document.head.querySelectorAll("link[data-theme=light]"), + ) as Array; + this._cssDarkFiles = Array.from( + document.head.querySelectorAll("link[data-theme=dark]"), + ) as Array; this._cssLightFiles.forEach((el) => el.remove()); this._cssDarkFiles.forEach((el) => el.remove()); const theme = localStorage.getItem("theme"); @@ -61,12 +63,11 @@ export class ThemeManager { this._enable(true); } else if (theme == "light") { this._enable(false); - } else if (window.matchMedia('(prefers-color-scheme: dark)').media !== 'not all') { + } else if (window.matchMedia("(prefers-color-scheme: dark)").media !== "not all") { this._enable(true); } - if (button) - this.bindButton(button); + if (button) this.bindButton(button); } private _toggle = () => { @@ -74,25 +75,25 @@ export class ThemeManager { this._beforeTransition(); const dark = !document.documentElement.classList.contains("dark"); if (dark) { - document.documentElement.classList.add('dark'); + document.documentElement.classList.add("dark"); metaValue = "dark light"; this._cssLightFiles.forEach((el) => el.remove()); this._cssDarkFiles.forEach((el) => document.head.appendChild(el)); } else { - document.documentElement.classList.remove('dark'); + document.documentElement.classList.remove("dark"); this._cssDarkFiles.forEach((el) => el.remove()); this._cssLightFiles.forEach((el) => document.head.appendChild(el)); } - localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? "dark" : "light"); - + localStorage.setItem("theme", document.documentElement.classList.contains("dark") ? "dark" : "light"); + // this._metaTag.setAttribute("content", metaValue); }; private _enable = (dark: boolean) => { const mode = dark ? "dark" : "light"; const opposite = dark ? "light" : "dark"; - - localStorage.setItem('theme', dark ? "dark" : "light"); + + localStorage.setItem("theme", dark ? "dark" : "light"); this._beforeTransition(); @@ -100,7 +101,7 @@ export class ThemeManager { document.documentElement.classList.remove(opposite); } document.documentElement.classList.add(mode); - + if (dark) { this._cssLightFiles.forEach((el) => el.remove()); this._cssDarkFiles.forEach((el) => document.head.appendChild(el)); @@ -117,4 +118,4 @@ export class ThemeManager { this._updateThemeIcon(); } }; - } +} diff --git a/ts/modules/ui.ts b/ts/modules/ui.ts index 006047d..a158ed6 100644 --- a/ts/modules/ui.ts +++ b/ts/modules/ui.ts @@ -5,7 +5,7 @@ export interface HiddenInputConf { customContainerHTML?: string; input?: string; clickAwayShouldSave?: boolean; -}; +} export class HiddenInputField { public static editClass = "ri-edit-line"; @@ -13,17 +13,17 @@ export class HiddenInputField { private _c: HiddenInputConf; private _input: HTMLInputElement; - private _content: HTMLElement + private _content: HTMLElement; private _toggle: HTMLElement; previous: string; constructor(c: HiddenInputConf) { this._c = c; - if (!(this._c.customContainerHTML)) { + if (!this._c.customContainerHTML) { this._c.customContainerHTML = ``; } - if (!(this._c.input)) { + if (!this._c.input) { this._c.input = ``; } this._c.container.innerHTML = ` @@ -40,7 +40,7 @@ export class HiddenInputField { this._input.classList.add("py-0.5", "px-1", "hidden"); this._toggle = this._c.container.querySelector(".hidden-input-toggle"); this._content = this._c.container.querySelector(".hidden-input-content"); - + this._toggle.onclick = () => { this.editing = !this.editing; }; @@ -49,22 +49,29 @@ export class HiddenInputField { e.preventDefault(); this._toggle.click(); } - }) - - + }); this.setEditing(false, true); } // FIXME: not working outerClickListener = ((event: Event) => { - if (!(event.target instanceof HTMLElement && (this._input.contains(event.target) || this._toggle.contains(event.target)))) { - this.toggle(!(this._c.clickAwayShouldSave)); + if ( + !( + event.target instanceof HTMLElement && + (this._input.contains(event.target) || this._toggle.contains(event.target)) + ) + ) { + this.toggle(!this._c.clickAwayShouldSave); } }).bind(this); - get editing(): boolean { return this._toggle.classList.contains(HiddenInputField.saveClass); } - set editing(e: boolean) { this.setEditing(e); } + get editing(): boolean { + return this._toggle.classList.contains(HiddenInputField.saveClass); + } + set editing(e: boolean) { + this.setEditing(e); + } setEditing(e: boolean, noEvent: boolean = false, noSave: boolean = false) { if (e) { @@ -84,11 +91,13 @@ export class HiddenInputField { // done by set value() // this._content.classList.remove("hidden"); this._input.classList.add("hidden"); - if (this.value != this.previous && !noEvent && !noSave) this._c.onSet() + if (this.value != this.previous && !noEvent && !noSave) this._c.onSet(); } } - get value(): string { return this._content.textContent; }; + get value(): string { + return this._content.textContent; + } set value(v: string) { this._content.textContent = v; this._input.value = v; @@ -96,5 +105,7 @@ export class HiddenInputField { else this._content.classList.remove("hidden"); } - toggle(noSave: boolean = false) { this.setEditing(!this.editing, false, noSave); } + toggle(noSave: boolean = false) { + this.setEditing(!this.editing, false, noSave); + } } diff --git a/ts/modules/update.ts b/ts/modules/update.ts index c39fc63..b47aec5 100644 --- a/ts/modules/update.ts +++ b/ts/modules/update.ts @@ -13,28 +13,35 @@ export class Updater implements updater { private _date: number; updateAvailable = false; - checkForUpdates = (run?: (req: XMLHttpRequest) => void) => _get("/config/update", null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 200) { - window.notifications.customError("errorCheckUpdate", window.lang.notif("errorCheckUpdate")); - return + checkForUpdates = (run?: (req: XMLHttpRequest) => void) => + _get("/config/update", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) { + window.notifications.customError("errorCheckUpdate", window.lang.notif("errorCheckUpdate")); + return; + } + let resp = req.response as updateDTO; + if (resp.new) { + this.update = resp.update; + if (run) { + run(req); + } + // } else { + // window.notifications.customPositive("noUpdatesAvailable", "", window.lang.notif("noUpdatesAvailable")); + } } - let resp = req.response as updateDTO; - if (resp.new) { - this.update = resp.update; - if (run) { run(req); } - // } else { - // window.notifications.customPositive("noUpdatesAvailable", "", window.lang.notif("noUpdatesAvailable")); - } - } - }); - get date(): number { return this._date; } + }); + get date(): number { + return this._date; + } set date(unix: number) { this._date = unix; document.getElementById("update-date").textContent = toDateString(new Date(this._date * 1000)); } - - get description(): string { return this._update.description; } + + get description(): string { + return this._update.description; + } set description(description: string) { this._update.description = description; const el = document.getElementById("update-description") as HTMLParagraphElement; @@ -46,35 +53,49 @@ export class Updater implements updater { } } - get changelog(): string { return this._update.changelog; } + get changelog(): string { + return this._update.changelog; + } set changelog(changelog: string) { this._update.changelog = changelog; document.getElementById("update-changelog").innerHTML = Marked.parse(changelog); } - get version(): string { return this._update.version; } + get version(): string { + return this._update.version; + } set version(version: string) { this._update.version = version; document.getElementById("update-version").textContent = version; } - get commit(): string { return this._update.commit; } + get commit(): string { + return this._update.commit; + } set commit(commit: string) { this._update.commit = commit; document.getElementById("update-commit").textContent = commit.slice(0, 7); } - get link(): string { return this._update.link; } + get link(): string { + return this._update.link; + } set link(link: string) { this._update.link = link; (document.getElementById("update-version") as HTMLAnchorElement).href = link; } - get download_link(): string { return this._update.download_link; } - set download_link(link: string) { this._update.download_link = link; } + get download_link(): string { + return this._update.download_link; + } + set download_link(link: string) { + this._update.download_link = link; + } - get can_update(): boolean { return this._update.can_update; } + get can_update(): boolean { + return this._update.can_update; + } set can_update(can: boolean) { this._update.can_update = can; const download = document.getElementById("update-download") as HTMLSpanElement; @@ -89,7 +110,9 @@ export class Updater implements updater { } } - get update(): Update { return this._update; } + get update(): Update { + return this._update; + } set update(update: Update) { this._update = update; this.version = update.version; @@ -106,24 +129,36 @@ export class Updater implements updater { const update = document.getElementById("update-update") as HTMLSpanElement; update.onclick = () => { toggleLoader(update); - _post("/config/update", null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - toggleLoader(update); - const success = req.response["success"] as Boolean; - if (req.status == 500 && success) { - window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh")); - } else if (req.status != 200) { - window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate")); - } else { + _post( + "/config/update", + null, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(update); + const success = req.response["success"] as Boolean; + if (req.status == 500 && success) { + window.notifications.customSuccess( + "applyUpdate", + window.lang.notif("updateAppliedRefresh"), + ); + } else if (req.status != 200) { + window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate")); + } else { + window.notifications.customSuccess( + "applyUpdate", + window.lang.notif("updateAppliedRefresh"), + ); + } + window.modals.updateInfo.close(); + } + }, + true, + (req: XMLHttpRequest) => { + if (req.status == 0) { window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh")); } - window.modals.updateInfo.close(); - } - }, true, (req: XMLHttpRequest) => { - if (req.status == 0) { - window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh")); - } - }); + }, + ); }; this.checkForUpdates(() => { this.updateAvailable = true; diff --git a/ts/modules/validator.ts b/ts/modules/validator.ts index 3a10843..112d6a6 100644 --- a/ts/modules/validator.ts +++ b/ts/modules/validator.ts @@ -20,7 +20,7 @@ interface pwValStrings { lowercase: pwValString; number: pwValString; special: pwValString; - [ type: string ]: pwValString; + [type: string]: pwValString; } declare var window: valWindow; @@ -32,7 +32,9 @@ class Requirement { private _valid: HTMLSpanElement; private _li: HTMLLIElement; - get valid(): boolean { return this._valid.classList.contains("~positive"); } + get valid(): boolean { + return this._valid.classList.contains("~positive"); + } set valid(state: boolean) { if (state) { this._valid.classList.add("~positive"); @@ -57,12 +59,14 @@ class Requirement { if (this._minCount == 1) { text = window.validationStrings[this._name].singular.replace("{n}", "1"); } else { - text = window.validationStrings[this._name].plural.replace("{n}", ""+this._minCount); + text = window.validationStrings[this._name].plural.replace("{n}", "" + this._minCount); } this._content.textContent = text; } - validate = (count: number) => { this.valid = (count >= this._minCount); } + validate = (count: number) => { + this.valid = count >= this._minCount; + }; } export interface ValidatorConf { @@ -73,8 +77,12 @@ export interface ValidatorConf { validatorFunc?: (oncomplete: (valid: boolean) => void) => void; } -export interface Validation { [name: string]: number } -export interface Requirements { [category: string]: Requirement }; +export interface Validation { + [name: string]: number; +} +export interface Requirements { + [category: string]: Requirement; +} export class Validator { private _conf: ValidatorConf; @@ -82,29 +90,29 @@ export class Validator { private _defaultPwValStrings: pwValStrings = { length: { singular: "Must have at least {n} character", - plural: "Must have at least {n} characters" + plural: "Must have at least {n} characters", }, uppercase: { singular: "Must have at least {n} uppercase character", - plural: "Must have at least {n} uppercase characters" + plural: "Must have at least {n} uppercase characters", }, lowercase: { singular: "Must have at least {n} lowercase character", - plural: "Must have at least {n} lowercase characters" + plural: "Must have at least {n} lowercase characters", }, number: { singular: "Must have at least {n} number", - plural: "Must have at least {n} numbers" + plural: "Must have at least {n} numbers", }, special: { singular: "Must have at least {n} special character", - plural: "Must have at least {n} special characters" - } + plural: "Must have at least {n} special characters", + }, }; private _checkPasswords = () => { return this._conf.passwordField.value == this._conf.rePasswordField.value; - } + }; validate = () => { const pw = this._checkPasswords(); @@ -125,51 +133,66 @@ export class Validator { }); }; - private _isInt = (s: string): boolean => { return (s >= '0' && s <= '9'); } - + private _isInt = (s: string): boolean => { + return s >= "0" && s <= "9"; + }; + private _testStrings = (f: pwValString): boolean => { const testString = (s: string): boolean => { - if (s == "" || !s.includes("{n}")) { return false; } + if (s == "" || !s.includes("{n}")) { + return false; + } return true; - } + }; return testString(f.singular) && testString(f.plural); - } + }; private _validate = (s: string): Validation => { let v: Validation = {}; - for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { v[criteria] = 0; } + for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { + v[criteria] = 0; + } v["length"] = s.length; for (let c of s) { - if (this._isInt(c)) { v["number"]++; } - else { + if (this._isInt(c)) { + v["number"]++; + } else { const upper = c.toUpperCase(); - if (upper == c.toLowerCase()) { v["special"]++; } - else { - if (upper == c) { v["uppercase"]++; } - else if (upper != c) { v["lowercase"]++; } + if (upper == c.toLowerCase()) { + v["special"]++; + } else { + if (upper == c) { + v["uppercase"]++; + } else if (upper != c) { + v["lowercase"]++; + } } } } - return v - } - + return v; + }; + private _bindRequirements = () => { for (let category in window.validationStrings) { if (!this._testStrings(window.validationStrings[category])) { window.validationStrings[category] = this._defaultPwValStrings[category]; } const el = document.getElementById("requirement-" + category); - if (typeof(el) === 'undefined' || el == null) continue; + if (typeof el === "undefined" || el == null) continue; this._requirements[category] = new Requirement(category, el as HTMLLIElement); } }; - get requirements(): Requirements { return this._requirements }; + get requirements(): Requirements { + return this._requirements; + } constructor(conf: ValidatorConf) { this._conf = conf; - if (!(this._conf.validatorFunc)) { - this._conf.validatorFunc = (oncomplete: (valid: boolean) => void) => { oncomplete(true); }; + if (!this._conf.validatorFunc) { + this._conf.validatorFunc = (oncomplete: (valid: boolean) => void) => { + oncomplete(true); + }; } this._conf.rePasswordField.addEventListener("keyup", this.validate); this._conf.passwordField.addEventListener("keyup", this.validate); diff --git a/ts/pwr.ts b/ts/pwr.ts index 175e755..daec4d2 100644 --- a/ts/pwr.ts +++ b/ts/pwr.ts @@ -10,7 +10,7 @@ interface formWindow extends Window { telegramModal: Modal; discordModal: Modal; matrixModal: Modal; - confirmationModal: Modal + confirmationModal: Modal; code: string; messages: { [key: string]: string }; confirmation: boolean; @@ -66,7 +66,7 @@ let validatorConf: ValidatorConf = { rePasswordField: rePasswordField, submitInput: submitInput, submitButton: submitSpan, - validatorFunc: baseValidator + validatorFunc: baseValidator, }; var validator = new Validator(validatorConf); @@ -90,7 +90,7 @@ form.onsubmit = (event: Event) => { const params = new URLSearchParams(window.location.search); let send: sendDTO = { pin: params.get("pin"), - password: passwordField.value + password: passwordField.value, }; if (window.captcha) { if (window.reCAPTCHA) { @@ -99,13 +99,34 @@ form.onsubmit = (event: Event) => { send.captcha_text = captcha.input.value; } } - _post("/reset", send, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - removeLoader(submitSpan); - if (req.status == 400) { - if (req.response["error"] as string) { + _post( + "/reset", + send, + (req: XMLHttpRequest) => { + if (req.readyState == 4) { + removeLoader(submitSpan); + if (req.status == 400) { + if (req.response["error"] as string) { + const old = submitSpan.textContent; + submitSpan.textContent = window.messages[req.response["error"]]; + submitSpan.classList.add("~critical"); + submitSpan.classList.remove("~urge"); + setTimeout(() => { + submitSpan.classList.add("~urge"); + submitSpan.classList.remove("~critical"); + submitSpan.textContent = old; + }, 2000); + } else { + for (let type in req.response) { + if (requirements[type]) { + requirements[type].valid = req.response[type] as boolean; + } + } + } + return; + } else if (req.status != 200) { const old = submitSpan.textContent; - submitSpan.textContent = window.messages[req.response["error"]]; + submitSpan.textContent = window.messages["errorUnknown"]; submitSpan.classList.add("~critical"); submitSpan.classList.remove("~urge"); setTimeout(() => { @@ -114,26 +135,12 @@ form.onsubmit = (event: Event) => { submitSpan.textContent = old; }, 2000); } else { - for (let type in req.response) { - if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; } - } + window.successModal.show(); } - return; - } else if (req.status != 200) { - const old = submitSpan.textContent; - submitSpan.textContent = window.messages["errorUnknown"]; - submitSpan.classList.add("~critical"); - submitSpan.classList.remove("~urge"); - setTimeout(() => { - submitSpan.classList.add("~urge"); - submitSpan.classList.remove("~critical"); - submitSpan.textContent = old; - }, 2000); - } else { - window.successModal.show(); } - } - }, true); + }, + true, + ); }; validator.validate(); diff --git a/ts/setup.ts b/ts/setup.ts index 9e8cb64..a07eeae 100644 --- a/ts/setup.ts +++ b/ts/setup.ts @@ -11,13 +11,15 @@ declare var window: sWindow; const theme = new ThemeManager(document.getElementById("button-theme")); -window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); - +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; }; - +const text = (id: string, val: string) => { + document.getElementById(id).textContent = val; +}; +const html = (id: string, val: string) => { + document.getElementById(id).innerHTML = val; +}; // FIXME: Reuse setting types from ts/modules/settings.ts interface boolEvent extends Event { @@ -26,18 +28,35 @@ interface boolEvent extends Event { class Input { private _el: HTMLInputElement; - get value(): string { return ""+this._el.value; } - set value(v: string) { this._el.value = v; } + 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) { + 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 (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 (el.parentElement.tagName == "LABEL") { + el = el.parentElement; + } if (event.detail !== dependsTrue) { el.classList.add("unfocused"); } else { @@ -51,9 +70,13 @@ class Input { 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; } - + 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 = () => { @@ -62,10 +85,10 @@ class Checkbox { state = false; } if (this._section && this._setting) { - const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": state }) + const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { detail: state }); document.dispatchEvent(ev); } - } + }; set onchange(f: () => void) { this._el.addEventListener("change", f); } @@ -85,7 +108,7 @@ class Checkbox { if (section && setting) { this._section = section; this._setting = setting; - this._el.onchange = this.broadcast; + this._el.onchange = this.broadcast; } if (depends) { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { @@ -110,21 +133,23 @@ class Checkbox { 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; + 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 }) + 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) { @@ -177,26 +202,32 @@ class BoolRadios { class Select { private _el: HTMLSelectElement; - get value(): string { return this._el.value; } - set value(v: string) { this._el.value = v; } + 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 }) + 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) { @@ -221,137 +252,194 @@ class Select { } class LangSelect extends Select { - constructor(page: string, el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { + 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]); + _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"; } - this.value = "en-us"; - } - }, true); + }, + true, + ); } } -const replaceLink = (elName: string, sect: string, name: string, url: string, text: string) => html(elName, window.lang.var(sect, name, `${text}`)); +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( + "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"); +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")) + 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") + 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")), - "jfa_url": new Input(get("ui-jfa_url")), - "theme": new Select(get("ui-theme")), + ui: { + host: new Input(get("ui-host")), + port: new Input(get("ui-port")), + url_base: new Input(get("ui-url_base")), + jfa_url: new Input(get("ui-jfa_url")), + 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"]) + 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") + 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") + 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") + 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") + 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")) + notifications: { + enabled: new Checkbox(get("notifications-enabled")), }, - "user_page": { - "enabled": new Checkbox(get("userpage-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") + 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"), + invite_emails: { + enabled: new Checkbox(get("invite_emails-enabled"), "", false, "invite_emails", "enabled"), + subject: new Input(get("invite_emails-subject"), "", "", "enabled", true, "invite_emails"), }, - "mailgun": { - "api_url": new Input(get("mailgun-api_url")), - "api_key": new Input(get("mailgun-api_key")) + 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")) + 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") + 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") + 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"), }, - "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")) { @@ -366,7 +454,7 @@ settings["ui"]["theme"].onchange = checkTheme; checkTheme(); const fixFullURL = (v: string): string => { - if (!(v.startsWith("http://")) && !(v.startsWith("https://"))) { + if (!v.startsWith("http://") && !v.startsWith("https://")) { v = "http://" + v; } return v; @@ -374,9 +462,11 @@ const fixFullURL = (v: string): string => { const formatSubpath = (v: string): string => { if (v == "/") return ""; - if (v.charAt(-1) == "/") { v = v.slice(0, -1); } + if (v.charAt(-1) == "/") { + v = v.slice(0, -1); + } return v; -} +}; const constructNewURLs = (): string[] => { let local = settings["ui"]["host"].value + ":" + settings["ui"]["port"].value; @@ -390,7 +480,7 @@ const constructNewURLs = (): string[] => { } remote = fixFullURL(remote); return [local, remote]; -} +}; const restartButton = document.getElementById("restart") as HTMLSpanElement; const serialize = () => { @@ -405,54 +495,63 @@ const serialize = () => { } } 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; + _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 refreshURLs = constructNewURLs(); + const refreshButtons = [ + document.getElementById("refresh-internal") as HTMLAnchorElement, + document.getElementById("refresh-external") as HTMLAnchorElement, + ]; + ["internal", "external"].forEach((urltype, i) => { + const button = refreshButtons[i]; + button.classList.remove("unfocused"); + button.href = refreshURLs[i]; + button.innerHTML = `${urltype.charAt(0).toUpperCase() + urltype.slice(1)}:${button.href}`; + // skip external if it isn't set + if (refreshURLs.length == 1) return; + }); } - restartButton.parentElement.querySelector("span.back").classList.add("unfocused"); - restartButton.classList.add("unfocused"); - const refreshURLs = constructNewURLs(); - const refreshButtons = [document.getElementById("refresh-internal") as HTMLAnchorElement, document.getElementById("refresh-external") as HTMLAnchorElement]; - ["internal", "external"].forEach((urltype, i) => { - const button = refreshButtons[i]; - button.classList.remove("unfocused"); - button.href = refreshURLs[i]; - button.innerHTML = `${urltype.charAt(0).toUpperCase() + urltype.slice(1)}:${button.href}`; - // skip external if it isn't set - if (refreshURLs.length == 1) return; - }); - } - }, true, (req: XMLHttpRequest) => { - if (req.status == 0) { - window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused")); - } - }); -} + }, + 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")); @@ -503,7 +602,7 @@ const getParentCard = (el: HTMLElement): HTMLDivElement => { const jellyfinLoginAccessChange = () => { const adminOnly = settings["ui"]["admin_only"].value == "true"; - const allowAll = settings["ui"]["allow_all"].value == "true"; + const allowAll = settings["ui"]["allow_all"].value == "true"; const adminOnlyEl = document.getElementById("ui-admin_only") as HTMLInputElement; const allowAllEl = document.getElementById("ui-allow_all") as HTMLInputElement; const nextButton = getParentCard(adminOnlyEl).querySelector("span.next") as HTMLSpanElement; @@ -515,10 +614,10 @@ const jellyfinLoginAccessChange = () => { adminOnlyEl.disabled = true; allowAllEl.disabled = false; nextButton.removeAttribute("disabled"); - } else { + } else { adminOnlyEl.disabled = false; allowAllEl.disabled = false; - nextButton.setAttribute("disabled", "true") + nextButton.setAttribute("disabled", "true"); } }; @@ -534,7 +633,7 @@ const embyHidePWR = () => { } else if (val == "emby") { pwr.classList.add("hidden"); } -} +}; settings["jellyfin"]["type"].onchange = embyHidePWR; embyHidePWR(); @@ -552,7 +651,9 @@ let pages = new PageManager({ defaultTitle: "Setup - jfa-go", }); -const cards = Array.from(document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned")) as Array; +const cards = Array.from( + document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned"), +) as Array; (window as any).cards = cards; (() => { @@ -582,10 +683,11 @@ const cards = Array.from(document.getElementsByClassName("page-container")[0].qu }, }); if (back) back.addEventListener("click", () => pages.prev(title)); - if (next) next.addEventListener("click", () => { - if (next.hasAttribute("disabled")) return; - pages.next(title); - }); + if (next) + next.addEventListener("click", () => { + if (next.hasAttribute("disabled")) return; + pages.next(title); + }); } })(); @@ -596,51 +698,57 @@ const cards = Array.from(document.getElementsByClassName("page-container")[0].qu 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 + 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"); + _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("~critical"); + button.classList.remove("~positive"); }, 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")); - } - }); + }, + true, + (req: XMLHttpRequest) => { + if (req.status == 0) { + window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused")); + } + }, + ); }; })(); diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 9e0426d..46370ec 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -1,6 +1,6 @@ declare interface Modal { modal: HTMLElement; - closeButton: HTMLSpanElement + closeButton: HTMLSpanElement; show: () => void; close: (event?: Event, noDispatch?: boolean) => void; toggle: () => void; @@ -58,12 +58,12 @@ declare interface GlobalWindow extends Window { jfAdminOnly: boolean; jfAllowAll: boolean; referralsEnabled: boolean; - loginAppearance: string; + loginAppearance: string; } declare interface InviteList { empty: boolean; - invites: { [code: string]: Invite } + invites: { [code: string]: Invite }; add: (invite: Invite) => void; reload: (callback?: () => void) => void; isInviteURL: () => boolean; @@ -71,25 +71,25 @@ declare interface InviteList { } declare interface Invite { - code: string; // Invite code + code: string; // Invite code valid_till: number; // Unix timestamp of expiry - user_expiry: boolean; // Whether or not user expiry is enabled - user_months?: number; // Number of months till user expiry - user_days?: number; // Number of days till user expiry - user_hours?: number; // Number of hours till user expiry - user_minutes?: number; // Number of minutes till user expiry - created: number; // Date of creation (unix timestamp) - profile: string; // Profile used on this invite - used_by?: { [user: string]: number }; // Users who have used this invite mapped to their creation time in Epoch/Unix time - no_limit: boolean; // If true, invite can be used any number of times - remaining_uses?: number; // Remaining number of uses (if applicable) - send_to?: string; // DEPRECATED Email/Discord username the invite was sent to (if applicable) - sent_to?: SentToList; // Email/Discord usernames attempts were made to send this invite to, and a failure reason if failed. + user_expiry: boolean; // Whether or not user expiry is enabled + user_months?: number; // Number of months till user expiry + user_days?: number; // Number of days till user expiry + user_hours?: number; // Number of hours till user expiry + user_minutes?: number; // Number of minutes till user expiry + created: number; // Date of creation (unix timestamp) + profile: string; // Profile used on this invite + used_by?: { [user: string]: number }; // Users who have used this invite mapped to their creation time in Epoch/Unix time + no_limit: boolean; // If true, invite can be used any number of times + remaining_uses?: number; // Remaining number of uses (if applicable) + send_to?: string; // DEPRECATED Email/Discord username the invite was sent to (if applicable) + sent_to?: SentToList; // Email/Discord usernames attempts were made to send this invite to, and a failure reason if failed. - notify_expiry?: boolean; // Whether to notify the requesting user of expiry or not - notify_creation?: boolean; // Whether to notify the requesting user of account creation or not - label?: string; // Optional label for the invite - user_label?: string; // Label to apply to users created w/ this invite. + notify_expiry?: boolean; // Whether to notify the requesting user of expiry or not + notify_creation?: boolean; // Whether to notify the requesting user of account creation or not + label?: string; // Optional label for the invite + user_label?: string; // Label to apply to users created w/ this invite. } declare interface SendFailure { @@ -103,9 +103,9 @@ declare interface SentToList { } declare interface Update { - version: string; - commit: string; - date: number; + version: string; + commit: string; + date: number; description: string; changelog: string; link: string; @@ -131,7 +131,7 @@ declare interface Lang { declare interface NotificationBox { connectionError: () => void; customError: (type: string, message: string) => void; - customPositive: (type: string, bold: string, message: string) => void; + customPositive: (type: string, bold: string, message: string) => void; customSuccess: (type: string, message: string) => void; } @@ -182,7 +182,7 @@ interface PaginatedReqDTO { page: number; sortByField: string; ascending: boolean; -}; +} interface DateAttempt { year?: number; @@ -198,7 +198,7 @@ interface ParsedDate { date: Date; text: string; invalid?: boolean; -}; +} declare var config: Object; declare var modifiedConfig: Object; diff --git a/ts/user.ts b/ts/user.ts index ca0d062..3c62dac 100644 --- a/ts/user.ts +++ b/ts/user.ts @@ -1,7 +1,17 @@ import { ThemeManager } from "./modules/theme.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { Modal } from "./modules/modal.js"; -import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, addLoader, removeLoader, toClipboard } from "./modules/common.js"; +import { + _get, + _post, + _delete, + notificationBox, + whichAnimationEvent, + toDateString, + addLoader, + removeLoader, + toClipboard, +} from "./modules/common.js"; import { Login } from "./modules/login.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; @@ -77,7 +87,7 @@ pages.setPage({ pages.setPage({ name: "reset", title: document.title, - url: basePath+"/password/reset", + url: basePath + "/password/reset", show: () => { const usernameInput = document.getElementById("login-user") as HTMLInputElement; const input = document.getElementById("pwr-address") as HTMLInputElement; @@ -98,11 +108,11 @@ pages.setPage({ const resetButton = document.getElementById("modal-login-pwr"); resetButton.onclick = () => { pages.load("reset"); - } + }; } })(); -window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); +window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement, 5); if (window.pwrEnabled && window.linkResetEnabled) { const submitButton = document.getElementById("pwr-submit"); @@ -113,7 +123,7 @@ if (window.pwrEnabled && window.linkResetEnabled) { if (req.readyState != 4) return; removeLoader(submitButton); if (req.status != 204) { - window.notifications.customError("unkownError", window.lang.notif("errorUnknown"));; + window.notifications.customError("unkownError", window.lang.notif("errorUnknown")); window.modals.pwr.close(); return; } @@ -168,9 +178,9 @@ interface ContactDTO { class ContactMethods { private _card: HTMLElement; private _content: HTMLElement; - private _buttons: { [name: string]: { element: HTMLElement, details: MyDetailsContactMethod } }; + private _buttons: { [name: string]: { element: HTMLElement; details: MyDetailsContactMethod } }; - constructor (card: HTMLElement) { + constructor(card: HTMLElement) { this._card = card; this._content = this._card.querySelector(".content"); this._buttons = {}; @@ -179,9 +189,15 @@ class ContactMethods { clear = () => { this._content.textContent = ""; this._buttons = {}; - } + }; - append = (name: string, details: MyDetailsContactMethod, icon: string, addEditFunc?: (add: boolean) => void, required?: boolean) => { + append = ( + name: string, + details: MyDetailsContactMethod, + icon: string, + addEditFunc?: (add: boolean) => void, + required?: boolean, + ) => { const row = document.createElement("div"); row.classList.add("flex", "flex-row", "justify-between", "gap-2", "flex-nowrap"); let innerHTML = ` @@ -191,7 +207,7 @@ class ContactMethods { ${icon} - ${(details.value == "") ? window.lang.strings("notSet") : details.value} + ${details.value == "" ? window.lang.strings("notSet") : details.value}
    @@ -224,12 +240,12 @@ class ContactMethods { `; row.innerHTML = innerHTML; - + this._buttons[name] = { element: row, - details: details + details: details, }; - + const button = row.querySelector(".user-contact-enabled-disabled") as HTMLButtonElement; const checkbox = button.querySelector("input[type=checkbox]") as HTMLInputElement; const setButtonAppearance = () => { @@ -260,13 +276,14 @@ class ContactMethods { const addEditButton = row.querySelector(".user-contact-edit") as HTMLButtonElement; addEditButton.onclick = () => addEditFunc(details.value == ""); } - + if (!required && details.value != "") { const deleteButton = row.querySelector(".user-contact-delete") as HTMLButtonElement; - deleteButton.onclick = () => _delete("/my/" + name, null, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - document.dispatchEvent(new CustomEvent("details-reload")); - }); + deleteButton.onclick = () => + _delete("/my/" + name, null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + document.dispatchEvent(new CustomEvent("details-reload")); + }); } this._content.appendChild(row); @@ -305,46 +322,52 @@ class ReferralCard { private _expiryEl: HTMLSpanElement; private _descriptionEl: HTMLSpanElement; - get code(): string { return this._code; } + get code(): string { + return this._code; + } set code(c: string) { this._code = c; - // let u = new URL(window.location.href); // const path = window.pages.Base + window.pages.Form + "/" + this._code; - // + // // u.pathname = path; // u.hash = ""; // u.search = ""; - + // this._url = u.toString(); this._url = generateCodeLink(this._code); } - get remaining_uses(): number { return this._remainingUses; } - set remaining_uses(v: number) { + get remaining_uses(): number { + return this._remainingUses; + } + set remaining_uses(v: number) { this._remainingUses = v; - if (v > 0 && !(this._noLimit)) - this._remainingUsesEl.textContent = `${v}`; + if (v > 0 && !this._noLimit) this._remainingUsesEl.textContent = `${v}`; } - get no_limit(): boolean { return this._noLimit; } + get no_limit(): boolean { + return this._noLimit; + } set no_limit(v: boolean) { this._noLimit = v; - if (v) - this._remainingUsesEl.textContent = `∞`; - else - this._remainingUsesEl.textContent = `${this._remainingUses}`; + if (v) this._remainingUsesEl.textContent = `∞`; + else this._remainingUsesEl.textContent = `${this._remainingUses}`; } - get expiry(): Date { return this._expiry; }; + get expiry(): Date { + return this._expiry; + } set expiry(expiryUnix: number) { this._expiryUnix = expiryUnix; this._expiry = new Date(expiryUnix * 1000); this._expiryEl.textContent = toDateString(this._expiry); } - get use_expiry(): boolean { return this._useExpiry; } + get use_expiry(): boolean { + return this._useExpiry; + } set use_expiry(v: boolean) { this._useExpiry = v; if (v) { @@ -353,7 +376,7 @@ class ReferralCard { this._descriptionEl.textContent = window.lang.strings("referralsDescription"); } } - + constructor(card: HTMLElement) { this._card = card; this._button = this._card.querySelector(".user-referrals-button") as HTMLButtonElement; @@ -372,10 +395,10 @@ class ReferralCard {
    `; - + this._remainingUsesEl = this._infoArea.querySelector(".referral-remaining-uses") as HTMLSpanElement; this._expiryEl = this._infoArea.querySelector(".referral-expiry") as HTMLSpanElement; - + document.addEventListener("timefmt-change", () => { this.expiry = this._expiryUnix; }); @@ -432,25 +455,25 @@ class ExpiryCard { let ymd = [0, 0, 0]; while (now.getFullYear() != this._expiry.getFullYear()) { ymd[0] += 1; - now.setFullYear(now.getFullYear()+1); + now.setFullYear(now.getFullYear() + 1); } if (now.getMonth() > this._expiry.getMonth()) { - ymd[0] -=1; - now.setFullYear(now.getFullYear()-1); + ymd[0] -= 1; + now.setFullYear(now.getFullYear() - 1); } while (now.getMonth() != this._expiry.getMonth()) { ymd[1] += 1; now.setMonth(now.getMonth() + 1); } if (now.getDate() > this._expiry.getDate()) { - ymd[1] -=1; - now.setMonth(now.getMonth()-1); + ymd[1] -= 1; + now.setMonth(now.getMonth() - 1); } while (now.getDate() != this._expiry.getDate()) { ymd[2] += 1; now.setDate(now.getDate() + 1); } - + const langKeys = ["year", "month", "day"]; let innerHTML = ``; for (let i = 0; i < langKeys.length; i++) { @@ -467,7 +490,9 @@ class ExpiryCard { this._countdown.innerHTML = innerHTML; }; - get expiry(): Date { return this._expiry; }; + get expiry(): Date { + return this._expiry; + } set expiry(expiryUnix: number) { if (this._interval !== null) { window.clearInterval(this._interval); @@ -479,10 +504,12 @@ class ExpiryCard { return; } this._expiry = new Date(expiryUnix * 1000); - this._aside.textContent = window.lang.strings("yourAccountIsValidUntil").replace("{date}", toDateString(this._expiry)); + this._aside.textContent = window.lang + .strings("yourAccountIsValidUntil") + .replace("{date}", toDateString(this._expiry)); this._card.classList.remove("unfocused"); - this._interval = window.setInterval(this._drawCountdown, 60*1000); + this._interval = window.setInterval(this._drawCountdown, 60 * 1000); this._drawCountdown(); } } @@ -496,37 +523,45 @@ var contactMethodList = new ContactMethods(contactCard); const addEditEmail = (add: boolean): void => { const heading = window.modals.email.modal.querySelector(".heading"); - heading.innerHTML = (add ? window.lang.strings("addContactMethod") : window.lang.strings("editContactMethod")) + `×`; + heading.innerHTML = + (add ? window.lang.strings("addContactMethod") : window.lang.strings("editContactMethod")) + + `×`; const input = document.getElementById("modal-email-input") as HTMLInputElement; input.value = ""; const confirmationRequired = window.modals.email.modal.querySelector(".confirmation-required"); confirmationRequired.classList.add("unfocused"); - + const content = window.modals.email.modal.querySelector(".content"); content.classList.remove("unfocused"); const submit = window.modals.email.modal.querySelector(".modal-submit") as HTMLButtonElement; submit.onclick = () => { addLoader(submit); - _post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(submit); - if (req.status == 303 || req.status == 200) { - document.dispatchEvent(new CustomEvent("details-reload")); - window.modals.email.close(); - } - }, true, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(submit); - if (req.status == 401) { - content.classList.add("unfocused"); - confirmationRequired.classList.remove("unfocused"); - } - }); - } + _post( + "/my/email", + { email: input.value }, + (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(submit); + if (req.status == 303 || req.status == 200) { + document.dispatchEvent(new CustomEvent("details-reload")); + window.modals.email.close(); + } + }, + true, + (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(submit); + if (req.status == 401) { + content.classList.add("unfocused"); + confirmationRequired.classList.remove("unfocused"); + } + }, + ); + }; window.modals.email.show(); -} +}; const discordConf: ServiceConfiguration = { modal: window.modals.discord as Modal, @@ -539,7 +574,7 @@ const discordConf: ServiceConfiguration = { successError: window.lang.notif("verified"), successFunc: (modalClosed: boolean) => { if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload")); - } + }, }; let discord: Discord; @@ -555,7 +590,7 @@ const telegramConf: ServiceConfiguration = { successError: window.lang.notif("verified"), successFunc: (modalClosed: boolean) => { if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload")); - } + }, }; let telegram: Telegram; @@ -571,13 +606,12 @@ const matrixConf: MatrixConfiguration = { successError: window.lang.notif("verified"), successFunc: () => { setTimeout(() => document.dispatchEvent(new CustomEvent("details-reload")), 1200); - } + }, }; let matrix: Matrix; if (window.matrixEnabled) matrix = new Matrix(matrixConf); - const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement; const newPasswordField = document.getElementById("user-new-password") as HTMLInputElement; const rePasswordField = document.getElementById("user-reenter-new-password") as HTMLInputElement; @@ -592,7 +626,7 @@ let validatorConf: ValidatorConf = { passwordField: newPasswordField, rePasswordField: rePasswordField, submitButton: changePasswordButton, - validatorFunc: baseValidator + validatorFunc: baseValidator, }; let validator = new Validator(validatorConf); @@ -601,25 +635,33 @@ let validator = new Validator(validatorConf); oldPasswordField.addEventListener("keyup", validator.validate); changePasswordButton.addEventListener("click", () => { addLoader(changePasswordButton); - _post("/my/password", { old: oldPasswordField.value, new: newPasswordField.value }, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(changePasswordButton); - if (req.status == 400) { - window.notifications.customError("errorPassword", window.lang.notif("errorPassword")); - } else if (req.status == 500) { - window.notifications.customError("errorUnknown", window.lang.notif("errorUnknown")); - } else if (req.status == 204) { - window.notifications.customSuccess("passwordChanged", window.lang.notif("passwordChanged")); - setTimeout(() => { window.location.reload() }, 2000); - } - }, true, (req: XMLHttpRequest) => { - if (req.readyState != 4) return; - removeLoader(changePasswordButton); - if (req.status == 401) { - window.notifications.customError("oldPasswordError", window.lang.notif("errorOldPassword")); - return; - } - }); + _post( + "/my/password", + { old: oldPasswordField.value, new: newPasswordField.value }, + (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(changePasswordButton); + if (req.status == 400) { + window.notifications.customError("errorPassword", window.lang.notif("errorPassword")); + } else if (req.status == 500) { + window.notifications.customError("errorUnknown", window.lang.notif("errorUnknown")); + } else if (req.status == 204) { + window.notifications.customSuccess("passwordChanged", window.lang.notif("passwordChanged")); + setTimeout(() => { + window.location.reload(); + }, 2000); + } + }, + true, + (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(changePasswordButton); + if (req.status == 401) { + window.notifications.customError("oldPasswordError", window.lang.notif("errorOldPassword")); + return; + } + }, + ); }); document.addEventListener("details-reload", () => { @@ -644,20 +686,56 @@ document.addEventListener("details-reload", () => { rootCard.querySelector(".heading").innerHTML = innerHTML; - contactMethodList.clear(); + contactMethodList.clear(); // Note the weird format of the functions for discord/telegram: // "this" was being redefined within the onclick() method, so // they had to be wrapped in an anonymous function. - const contactMethods: { name: string, icon: string, f: (add: boolean) => void, required: boolean, enabled: boolean }[] = [ - {name: "email", icon: ``, f: addEditEmail, required: true, enabled: true}, - {name: "discord", icon: ``, f: (add: boolean) => { discord.onclick(); }, required: window.discordRequired, enabled: window.discordEnabled}, - {name: "telegram", icon: ``, f: (add: boolean) => { telegram.onclick() }, required: window.telegramRequired, enabled: window.telegramEnabled}, - {name: "matrix", icon: `[m]`, f: (add: boolean) => { matrix.show(); }, required: window.matrixRequired, enabled: window.matrixEnabled} + const contactMethods: { + name: string; + icon: string; + f: (add: boolean) => void; + required: boolean; + enabled: boolean; + }[] = [ + { + name: "email", + icon: ``, + f: addEditEmail, + required: true, + enabled: true, + }, + { + name: "discord", + icon: ``, + f: (add: boolean) => { + discord.onclick(); + }, + required: window.discordRequired, + enabled: window.discordEnabled, + }, + { + name: "telegram", + icon: ``, + f: (add: boolean) => { + telegram.onclick(); + }, + required: window.telegramRequired, + enabled: window.telegramEnabled, + }, + { + name: "matrix", + icon: `[m]`, + f: (add: boolean) => { + matrix.show(); + }, + required: window.matrixRequired, + enabled: window.matrixEnabled, + }, ]; - + for (let method of contactMethods) { - if (!(method.enabled)) continue; + if (!method.enabled) continue; if (method.name in details) { contactMethodList.append(method.name, details[method.name], method.icon, method.f, method.required); } @@ -671,7 +749,7 @@ document.addEventListener("details-reload", () => { let messageCard = document.getElementById("card-message"); if (details.accounts_admin) { adminBackButton.classList.remove("unfocused"); - if (typeof(messageCard) == "undefined" || messageCard == null) { + if (typeof messageCard == "undefined" || messageCard == null) { messageCard = document.createElement("div"); messageCard.classList.add("card", "@low", "dark:~d_neutral", "content"); messageCard.id = "card-message"; @@ -685,7 +763,7 @@ document.addEventListener("details-reload", () => { } } - if (typeof(messageCard) != "undefined" && messageCard != null) { + if (typeof messageCard != "undefined" && messageCard != null) { messageCard.innerHTML = messageCard.innerHTML.replace(new RegExp("{username}", "g"), details.username); // setBestRowSpan(messageCard, false); // contactCard.querySelector(".content").classList.add("h-100"); @@ -696,7 +774,7 @@ document.addEventListener("details-reload", () => { if (window.referralsEnabled) { if (details.has_referrals) { _get("/my/referral", null, (req: XMLHttpRequest) => { - if (req.readyState != 4 || req.status != 200) return; + if (req.readyState != 4 || req.status != 200) return; const referral: MyReferral = req.response as MyReferral; referralCard.update(referral); setCardOrder(messageCard); @@ -715,9 +793,9 @@ document.addEventListener("details-reload", () => { const setCardOrder = (messageCard: HTMLElement) => { const cards = document.getElementById("user-cardlist"); const children = Array.from(cards.children); - const idxs = [...Array(cards.childElementCount).keys()] + const idxs = [...Array(cards.childElementCount).keys()]; // The message card is the first element and should always be so, so remove it from the list. - const hasMessageCard = !(typeof(messageCard) == "undefined" || messageCard == null); + const hasMessageCard = !(typeof messageCard == "undefined" || messageCard == null); if (hasMessageCard) idxs.shift(); const perms = generatePermutations(idxs); let minHeight = 999999; @@ -781,8 +859,7 @@ const setBestRowSpan = (el: HTMLElement, setOnParent: boolean) => { let rowSpan = Math.ceil(computeRealHeight(el) / largestNonMessageCardHeight); - if (rowSpan > 0) - (setOnParent ? el.parentElement : el).style.gridRow = `span ${rowSpan}`; + if (rowSpan > 0) (setOnParent ? el.parentElement : el).style.gridRow = `span ${rowSpan}`; }; const computeRealHeight = (el: HTMLElement): number => { @@ -801,12 +878,12 @@ const computeRealHeight = (el: HTMLElement): number => { } } return total; -} +}; const generatePermutations = (xs: number[]): [number[], number[]][] => { const l = xs.length; let out: [number[], number[]][] = []; - for (let i = 0; i < (l << 1); i++) { + for (let i = 0; i < l << 1; i++) { let incl = []; let excl = []; for (let j = 0; j < l; j++) { @@ -819,7 +896,7 @@ const generatePermutations = (xs: number[]): [number[], number[]][] => { out.push([incl, excl]); } return out; -} +}; login.bindLogout(document.getElementById("logout-button"));