import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateString, insertText, toClipboard } from "../modules/common.js"; import { templateEmail } from "../modules/settings.js"; import { Marked } from "@ts-stack/markdown"; import { stripMarkdown } from "../modules/stripmd.js"; import { DiscordUser, newDiscordSearch } from "../modules/discord.js"; const dateParser = require("any-date-parser"); interface User { id: string; name: string; email: string | undefined; notify_email: boolean; last_active: number; admin: boolean; disabled: boolean; expiry: number; telegram: string; notify_telegram: boolean; discord: string; notify_discord: boolean; discord_id: string; matrix: string; notify_matrix: boolean; label: string; accounts_admin: boolean; } interface getPinResponse { token: string; username: string; } interface announcementTemplate { name: string; subject: string; message: string; } var addDiscord: (passData: string) => void; class user implements User { private _id = ""; private _row: HTMLTableRowElement; private _check: HTMLInputElement; private _username: HTMLSpanElement; private _admin: HTMLSpanElement; private _disabled: HTMLSpanElement; private _email: HTMLInputElement; private _notifyEmail: boolean; private _emailAddress: string; private _emailEditButton: HTMLElement; private _telegram: HTMLTableDataCellElement; private _telegramUsername: string; private _notifyTelegram: boolean; private _discord: HTMLTableDataCellElement; private _discordUsername: string; private _discordID: string; private _notifyDiscord: boolean; private _matrix: HTMLTableDataCellElement; private _matrixID: string; private _notifyMatrix: boolean; private _expiry: HTMLTableDataCellElement; private _expiryUnix: number; private _lastActive: HTMLTableDataCellElement; private _lastActiveUnix: number; private _notifyDropdown: HTMLDivElement; private _label: HTMLInputElement; private _userLabel: string; private _labelEditButton: HTMLElement; private _accounts_admin: HTMLInputElement private _selected: boolean; lastNotifyMethod = (): string => { // Telegram, Matrix, Discord const telegram = window.telegramEnabled && this._telegramUsername && this._telegramUsername != ""; const discord = window.discordEnabled && this._discordUsername && this._discordUsername != ""; const matrix = window.matrixEnabled && this._matrixID && this._matrixID != ""; const email = window.emailEnabled && this.email != ""; if (discord) return "discord"; if (matrix) return "matrix"; if (telegram) return "telegram"; if (email) return "email"; } private _checkUnlinkArea = () => { const unlinkHeader = this._notifyDropdown.querySelector(".accounts-unlink-header") as HTMLSpanElement; if (this.lastNotifyMethod() == "email" || !this.lastNotifyMethod()) { unlinkHeader.classList.add("unfocused"); } else { unlinkHeader.classList.remove("unfocused"); } } get selected(): boolean { return this._selected; } set selected(state: boolean) { this._selected = state; this._check.checked = state; 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 admin(): boolean { return this._admin.classList.contains("chip"); } set admin(state: boolean) { if (state) { this._admin.classList.add("chip", "~info", "ml-4"); this._admin.textContent = window.lang.strings("admin"); } else { this._admin.classList.remove("chip", "~info", "ml-4"); this._admin.textContent = ""; } } 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)); if (this._accounts_admin.disabled) { this._accounts_admin.title = window.lang.strings("accessJFASettings"); } else { this._accounts_admin.title = ""; } } get disabled(): boolean { return this._disabled.classList.contains("chip"); } set disabled(state: boolean) { if (state) { this._disabled.classList.add("chip", "~warning", "ml-4"); this._disabled.textContent = window.lang.strings("disabled"); } else { this._disabled.classList.remove("chip", "~warning", "ml-4"); this._disabled.textContent = ""; } } get email(): string { return this._emailAddress; } set email(value: string) { this._emailAddress = value; const input = this._email.querySelector("input"); if (input) { input.value = value; } else { this._email.textContent = value; } const lastNotifyMethod = this.lastNotifyMethod() == "email"; if (!value) { this._notifyDropdown.querySelector(".accounts-area-email").classList.add("unfocused"); } else { this._notifyDropdown.querySelector(".accounts-area-email").classList.remove("unfocused"); if (lastNotifyMethod) { (this._email.parentElement as HTMLDivElement).appendChild(this._notifyDropdown); } } } 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; } } private _constructDropdown = (): HTMLDivElement => { const el = document.createElement("div") as HTMLDivElement; const telegram = this._telegramUsername != ""; const discord = this._discordUsername != ""; const matrix = this._matrixID != ""; const email = this._emailAddress != ""; if (!telegram && !discord && !matrix && !email) return; let innerHTML = ` `; el.innerHTML = innerHTML; const button = el.querySelector("i"); const dropdown = el.querySelector("div.dropdown") as HTMLDivElement; const checks = el.querySelectorAll("input") as NodeListOf; 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")))); } button.onclick = () => { dropdown.classList.add("selected"); document.addEventListener("click", outerClickListener); }; const outerClickListener = (event: Event) => { 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; } set matrix(u: string) { if (!window.matrixEnabled) { this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-matrix").classList.add("unfocused"); return; } const lastNotifyMethod = this.lastNotifyMethod() == "matrix"; this._matrixID = u; if (!u) { this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-matrix").classList.add("unfocused"); this._matrix.innerHTML = `
`; (this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix; } else { this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-matrix").classList.remove("unfocused"); this._matrix.innerHTML = `
${u}
`; if (lastNotifyMethod) { (this._matrix.querySelector(".table-inline") 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; const addIcon = addButton.querySelector("i"); if (addButton.classList.contains("chip")) { input.classList.remove("unfocused"); addIcon.classList.add("ri-check-line"); addIcon.classList.remove("ri-link"); addButton.classList.remove("chip") const outerClickListener = (event: Event) => { if (!(event.target instanceof HTMLElement && (this._matrix.contains(event.target) || addButton.contains(event.target)))) { document.dispatchEvent(new CustomEvent("accounts-reload")); document.removeEventListener("click", outerClickListener); } }; document.addEventListener("click", outerClickListener); } else { if (input.value.charAt(0) != "@" || !input.value.includes(":")) return; const send = { jf_id: this.id, 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")); return; } window.notifications.customSuccess("connectMatrix", window.lang.notif("accountConnected")); } }); } } 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; } set telegram(u: string) { if (!window.telegramEnabled) { this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-telegram").classList.add("unfocused"); return; } const lastNotifyMethod = this.lastNotifyMethod() == "telegram"; this._telegramUsername = u; if (!u) { this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-telegram").classList.add("unfocused"); this._telegram.innerHTML = `
`; (this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram; } else { this._notifyDropdown.querySelector(".accounts-area-telegram").classList.remove("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-telegram").classList.remove("unfocused"); this._telegram.innerHTML = `
@${u}
`; if (lastNotifyMethod) { (this._telegram.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown); } } this._checkUnlinkArea(); } 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; } } private _setNotifyMethod = () => { const email = this._notifyDropdown.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement; let send = { id: this.id, email: email.checked } if (window.telegramEnabled && this._telegramUsername) { 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; 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")); 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")); } }); } get discord(): string { return this._discordUsername; } set discord(u: string) { if (!window.discordEnabled) { this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-discord").classList.add("unfocused"); return; } const lastNotifyMethod = this.lastNotifyMethod() == "discord"; this._discordUsername = u; if (!u) { this._discord.innerHTML = `
`; (this._discord.querySelector("span") as HTMLSpanElement).onclick = () => addDiscord(this.id); this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-discord").classList.add("unfocused"); } else { this._notifyDropdown.querySelector(".accounts-area-discord").classList.remove("unfocused"); this._notifyDropdown.querySelector(".accounts-unlink-discord").classList.remove("unfocused"); this._discord.innerHTML = `
${u}
`; if (lastNotifyMethod) { (this._discord.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown); } } this._checkUnlinkArea(); } get discord_id(): string { return this._discordID; } set discord_id(id: string) { 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; } set notify_discord(s: boolean) { if (this._notifyDropdown) { (this._notifyDropdown.querySelector(".accounts-contact-discord") as HTMLInputElement).checked = s; } } 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)); } } 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)); } } get label(): string { return this._userLabel; } set label(l: string) { this._userLabel = l ? l : ""; this._label.innerHTML = l ? l : ""; this._labelEditButton.classList.add("ri-edit-line"); this._labelEditButton.classList.remove("ri-check-line"); if (!l) { this._label.classList.remove("chip", "~gray"); } else { this._label.classList.add("chip", "~gray", "mr-2"); } } matchesSearch = (query: string): boolean => { return ( this.name.includes(query) || this.label.includes(query) || this.discord.includes(query) || this.email.includes(query) || this.id.includes(query) || this.label.includes(query) || this.matrix.includes(query) || this.telegram.includes(query) ); } private _checkEvent = new CustomEvent("accountCheckEvent"); private _uncheckEvent = new CustomEvent("accountUncheckEvent"); constructor(user: User) { this._row = document.createElement("tr") as HTMLTableRowElement; let innerHTML = `
`; if (window.jellyfinLogin) { innerHTML += `
`; } innerHTML += `
`; if (window.telegramEnabled) { innerHTML += ` `; } if (window.matrixEnabled) { innerHTML += ` `; } if (window.discordEnabled) { innerHTML += ` `; } innerHTML += ` `; this._row.innerHTML = innerHTML; const emailEditor = ``; const labelEditor = ``; this._check = this._row.querySelector("input[type=checkbox].accounts-select-user") as HTMLInputElement; this._accounts_admin = this._row.querySelector("input[type=checkbox].accounts-access-jfa") as HTMLInputElement; this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement; this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement; this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement; this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement; this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement; this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement; this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement; this._matrix = this._row.querySelector(".accounts-matrix") as HTMLTableDataCellElement; this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement; this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement; this._label = this._row.querySelector(".accounts-label-container") as HTMLInputElement; this._labelEditButton = this._row.querySelector(".accounts-label-edit") as HTMLElement; this._check.onchange = () => { this.selected = this._check.checked; } if (window.jellyfinLogin) { this._accounts_admin.onchange = () => { this.accounts_admin = this._accounts_admin.checked; let send = {}; send[this.id] = this.accounts_admin; _post("/users/accounts-admin", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status != 204) { this.accounts_admin = !this.accounts_admin; window.notifications.customError("accountsAdminChanged", window.lang.notif("errorUnknown")); } } }); }; } this._notifyDropdown = this._constructDropdown(); const toggleEmailInput = () => { if (this._emailEditButton.classList.contains("ri-edit-line")) { this._email.innerHTML = emailEditor; this._email.querySelector("input").value = this._emailAddress; this._email.classList.remove("ml-2"); } else { this._email.textContent = this._emailAddress; this._email.classList.add("ml-2"); } this._emailEditButton.classList.toggle("ri-check-line"); this._emailEditButton.classList.toggle("ri-edit-line"); }; const emailClickListener = (event: Event) => { if (!(event.target instanceof HTMLElement && (this._email.contains(event.target) || this._emailEditButton.contains(event.target)))) { toggleEmailInput(); this.email = this.email; document.removeEventListener("click", emailClickListener); } }; this._emailEditButton.onclick = () => { if (this._emailEditButton.classList.contains("ri-edit-line")) { document.addEventListener('click', emailClickListener); } else { this._updateEmail(); document.removeEventListener('click', emailClickListener); } toggleEmailInput(); }; const toggleLabelInput = () => { if (this._labelEditButton.classList.contains("ri-edit-line")) { this._label.innerHTML = labelEditor; const input = this._label.querySelector("input"); input.value = this._userLabel; input.placeholder = window.lang.strings("label"); this._label.classList.remove("ml-2"); this._labelEditButton.classList.add("ri-check-line"); this._labelEditButton.classList.remove("ri-edit-line"); } else { this._updateLabel(); this._email.classList.add("ml-2"); } }; const labelClickListener = (event: Event) => { if (!(event.target instanceof HTMLElement && (this._label.contains(event.target) || this._labelEditButton.contains(event.target)))) { toggleLabelInput(); document.removeEventListener("click", labelClickListener); } }; this._labelEditButton.onclick = () => { if (this._labelEditButton.classList.contains("ri-edit-line")) { document.addEventListener('click', labelClickListener); } else { document.removeEventListener('click', labelClickListener); } toggleLabelInput(); }; this.update(user); document.addEventListener("timefmt-change", () => { this.expiry = this.expiry; this.last_active = this.last_active; }); } private _updateLabel = () => { let oldLabel = this.label; this.label = this._label.querySelector("input").value; let send = {}; send[this.id] = this.label; _post("/users/labels", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status != 204) { this.label = oldLabel; window.notifications.customError("labelChanged", window.lang.notif("errorUnknown")); } } }); }; private _updateEmail = () => { let oldEmail = this.email; this.email = this._email.querySelector("input").value; let send = {}; send[this.id] = this.email; _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}"`)); } else { this.email = oldEmail; 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 pin = document.getElementById("telegram-pin"); const link = document.getElementById("telegram-link") as HTMLAnchorElement; const username = document.getElementById("telegram-username") as HTMLSpanElement; 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; } update = (user: User) => { this.id = user.id; this.name = user.name; this.email = user.email || ""; // Little hack to get settings cogs to appear on first load this._discordUsername = user.discord; this._telegramUsername = user.telegram; this._matrixID = user.matrix; this.discord = user.discord; this.telegram = user.telegram; this.matrix = user.matrix; this.last_active = user.last_active; this.admin = user.admin; this.disabled = user.disabled; this.expiry = user.expiry; this.notify_discord = user.notify_discord; this.notify_telegram = user.notify_telegram; this.notify_matrix = user.notify_matrix; this.notify_email = user.notify_email; this.discord_id = user.discord_id; this.label = user.label; this.accounts_admin = user.accounts_admin; } asElement = (): HTMLTableRowElement => { return this._row; } remove = () => { if (this.selected) { document.dispatchEvent(this._uncheckEvent); } this._row.remove(); } } export class accountsList { private _table = 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; private _announceNameLabel = document.getElementById("announce-name") as HTMLLabelElement; private _announcePreview: HTMLElement; private _previewLoaded = false; private _announceTextarea = document.getElementById("textarea-announce") as HTMLTextAreaElement; private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement; private _disableEnable = document.getElementById("accounts-disable-enable") as HTMLSpanElement; private _enableExpiry = document.getElementById("accounts-enable-expiry") as HTMLSpanElement; private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement; private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement; private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement; private _enableExpiryNotify = document.getElementById("expiry-extend-enable") as HTMLInputElement; private _enableExpiryReason = document.getElementById("textarea-extend-enable") as HTMLTextAreaElement; private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement; private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement; private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement; private _sendPWR = document.getElementById("accounts-send-pwr") as HTMLSpanElement; private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement; private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement; private _search = document.getElementById("accounts-search") as HTMLInputElement; private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; private _users: { [id: string]: user }; private _ordering: string[] = []; private _checkCount: number = 0; private _inSearch = false; // Whether the enable/disable button should enable or not. private _shouldEnable = false; private _addUserForm = document.getElementById("form-add-user") as HTMLFormElement; private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement; 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 } = {}; private _activeSortColumn: string; private _sortingByButton = document.getElementById("accounts-sort-by-field") as HTMLButtonElement; private _filterArea = document.getElementById("accounts-filter-area"); private _searchOptionsHeader = document.getElementById("accounts-search-options-header"); // Whether the "Extend expiry" is extending or setting an expiry. private _settingExpiry = false; private _count = 30; private _populateNumbers = () => { 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++) { const field = document.getElementById(prefixes[j] + fieldIDs[i]); 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); } } } } showHideSearchOptionsHeader = () => { const sortingBy = !(this._sortingByButton.parentElement.classList.contains("hidden")); const hasFilters = this._filterArea.textContent != ""; console.log("sortingBy", sortingBy, "hasFilters", hasFilters); if (sortingBy || hasFilters) { this._searchOptionsHeader.classList.remove("hidden"); } else { this._searchOptionsHeader.classList.add("hidden"); } } private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = { "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, dependsOnTableHeader: "accounts-header-access-jfa" }, "email": { name: window.lang.strings("emailAddress"), getter: "email", bool: true, string: true, date: false, dependsOnTableHeader: "accounts-header-email" }, "telegram": { name: "Telegram", getter: "telegram", bool: true, string: true, date: false, dependsOnTableHeader: "accounts-header-telegram" }, "matrix": { name: "Matrix", getter: "matrix", bool: true, string: true, date: false, dependsOnTableHeader: "accounts-header-matrix" }, "discord": { name: "Discord", getter: "discord", bool: true, string: true, date: false, dependsOnTableHeader: "accounts-header-discord" }, "expiry": { name: window.lang.strings("expiry"), getter: "expiry", bool: true, string: false, date: true, dependsOnTableHeader: "accounts-header-expiry" }, "last-active": { name: window.lang.strings("lastActiveTime"), getter: "last_active", bool: true, string: false, date: true } } search = (query: String): string[] => { console.log(this._queries); this._filterArea.textContent = ""; query = query.toLowerCase(); let result: string[] = [...this._ordering]; // console.log("initial:", result); // const words = query.split(" "); let words: string[] = []; // FIXME: SPLIT BY SPACE, UNLESS IN QUOTES let quoteSymbol = ``; let queryStart = -1; let lastQuote = -1; for (let i = 0; i < query.length; i++) { if (queryStart == -1 && query[i] != " " && query[i] != `"` && query[i] != `'`) { queryStart = i; } if ((query[i] == `"` || query[i] == `'`) && (quoteSymbol == `` || query[i] == quoteSymbol)) { if (lastQuote != -1) { lastQuote = -1; quoteSymbol = ``; } else { lastQuote = i; quoteSymbol = query[i]; } } if (query[i] == " " || i == query.length-1) { if (lastQuote != -1) { continue; } else { let end = i+1; if (query[i] == " ") { end = i; while (i+1 < query.length && query[i+1] == " ") { i += 1; } } words.push(query.substring(queryStart, end).replace(/['"]/g, "")); console.log("pushed", words); queryStart = -1; } } } query = ""; for (let word of words) { if (!word.includes(":")) { let cachedResult = [...result]; for (let id of cachedResult) { const u = this._users[id]; if (!u.matchesSearch(word)) { result.splice(result.indexOf(id), 1); } } continue; } const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)]; if (!(split[0] in this._queries)) continue; const queryFormat = this._queries[split[0]]; if (queryFormat.bool) { let isBool = false; let boolState = false; if (split[1] == "true" || split[1] == "yes" || split[1] == "t" || split[1] == "y") { isBool = true; boolState = true; } else if (split[1] == "false" || split[1] == "no" || split[1] == "f" || split[1] == "n") { isBool = true; boolState = false; } if (isBool) { // FIXME: Generate filter card for each filter class const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full"); filterCard.innerHTML = ` ${queryFormat.name} `; filterCard.addEventListener("click", () => { for (let quote of [`"`, `'`, ``]) { this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); } this._search.oninput((null as Event)); }) this._filterArea.appendChild(filterCard); // console.log("is bool, state", boolState); // So removing elements doesn't affect us let cachedResult = [...result]; for (let id of cachedResult) { const u = this._users[id]; const value = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u); // console.log("got", queryFormat.getter + ":", value); // Remove from result if not matching query if (!((value && boolState) || (!value && !boolState))) { // console.log("not matching, result is", result); result.splice(result.indexOf(id), 1); } } continue } } if (queryFormat.string) { const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); filterCard.classList.add("button", "~neutral", "@low", "center", "mx-2", "h-full"); filterCard.innerHTML = ` ${queryFormat.name}: "${split[1]}" `; filterCard.addEventListener("click", () => { for (let quote of [`"`, `'`, ``]) { let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); this._search.value = this._search.value.replace(regex, ""); } this._search.oninput((null as Event)); }) this._filterArea.appendChild(filterCard); let cachedResult = [...result]; for (let id of cachedResult) { const u = this._users[id]; const value = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u); if (!(value.includes(split[1]))) { result.splice(result.indexOf(id), 1); } } continue; } if (queryFormat.date) { // -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0 let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2)); let unmodifiedValue = split[1]; if (compareType != 2) { split[1] = split[1].substring(1); } if (compareType == 2) compareType = 0; let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]); // Month in Date objects is 0-based, so make our parsed date that way too if ("month" in attempt) attempt["month"] -= 1; let date: Date = (Date as any).fromString(split[1]) as Date; console.log("Read", attempt, "and", date); if ("invalid" in (date as any)) continue; const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full"); filterCard.innerHTML = ` ${queryFormat.name}: ${(compareType == 1) ? window.lang.strings("after")+" " : ((compareType == -1) ? window.lang.strings("before")+" " : "")}${split[1]} `; filterCard.addEventListener("click", () => { for (let quote of [`"`, `'`, ``]) { let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig"); this._search.value = this._search.value.replace(regex, ""); } this._search.oninput((null as Event)); }) this._filterArea.appendChild(filterCard); let cachedResult = [...result]; for (let id of cachedResult) { const u = this._users[id]; const unixValue = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u); if (unixValue == 0) { result.splice(result.indexOf(id), 1); continue; } let value = new Date(unixValue*1000); const getterPairs: [string, () => number][] = [["year", Date.prototype.getFullYear], ["month", Date.prototype.getMonth], ["day", Date.prototype.getDate], ["hour", Date.prototype.getHours], ["minute", Date.prototype.getMinutes]]; // When doing > or <