mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-03-18 13:40:31 +01:00
uses the <tool-tip> tag now, and the setupTooltips() function in ui.ts must be called at any point in the pages load. added "below-center" position, showing below and centered horizontally. breakpoint tailwind lingo also now works (e.g. "below-center sm:right"). If a left or right-aligned tooltip clips off the screen, it will be shifted the appropriate amount left/right to avoid that automatically.
3076 lines
126 KiB
TypeScript
3076 lines
126 KiB
TypeScript
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, RadioBasedTabSelector } from "./ui";
|
|
import { PaginatedList } from "./list";
|
|
import { TableRow } from "./row";
|
|
|
|
declare var window: GlobalWindow;
|
|
|
|
const USER_DEFAULT_SORT_FIELD = "name";
|
|
const USER_DEFAULT_SORT_ASCENDING = true;
|
|
|
|
const dateParser = require("any-date-parser");
|
|
|
|
enum SelectAllState {
|
|
None = 0,
|
|
Some = 0.1,
|
|
AllVisible = 0.9,
|
|
All = 1,
|
|
}
|
|
|
|
interface UserDTO {
|
|
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;
|
|
referrals_enabled: boolean;
|
|
}
|
|
|
|
interface getPinResponse {
|
|
token: string;
|
|
username: string;
|
|
}
|
|
|
|
interface announcementTemplate {
|
|
name: string;
|
|
subject: string;
|
|
message: string;
|
|
}
|
|
|
|
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",
|
|
},
|
|
};
|
|
};
|
|
|
|
class User extends TableRow implements UserDTO, SearchableItem {
|
|
private _id = "";
|
|
private _check: HTMLInputElement;
|
|
private _username: HTMLSpanElement;
|
|
private _admin: HTMLSpanElement;
|
|
private _disabled: HTMLSpanElement;
|
|
private _email: HTMLInputElement;
|
|
private _emailEditor: HiddenInputField;
|
|
private _notifyEmail: boolean;
|
|
private _emailAddress: string;
|
|
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 _labelEditor: HiddenInputField;
|
|
private _userLabel: string;
|
|
private _accounts_admin: HTMLInputElement;
|
|
private _selected: boolean;
|
|
private _referralsEnabled: boolean;
|
|
private _referralsEnabledCheck: HTMLElement;
|
|
|
|
notInList: boolean = false;
|
|
|
|
focus = () => this._row.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
|
|
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.setSelected(state, true);
|
|
}
|
|
|
|
setSelected(state: boolean, dispatchEvent: boolean) {
|
|
this._selected = state;
|
|
this._check.checked = state;
|
|
if (dispatchEvent && !this.notInList)
|
|
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("hidden");
|
|
}
|
|
set admin(state: boolean) {
|
|
if (state) {
|
|
this._admin.classList.remove("hidden");
|
|
this._admin.textContent = window.lang.strings("admin");
|
|
} else {
|
|
this._admin.classList.add("hidden");
|
|
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("hidden");
|
|
}
|
|
set disabled(state: boolean) {
|
|
if (state) {
|
|
this._disabled.classList.remove("hidden");
|
|
this._disabled.textContent = window.lang.strings("disabled");
|
|
} else {
|
|
this._disabled.classList.add("hidden");
|
|
this._disabled.textContent = "";
|
|
}
|
|
}
|
|
|
|
get email(): string {
|
|
return this._emailAddress;
|
|
}
|
|
set email(value: string) {
|
|
this._emailAddress = value;
|
|
this._emailEditor.value = 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;
|
|
}
|
|
}
|
|
|
|
get referrals_enabled(): boolean {
|
|
return this._referralsEnabled;
|
|
}
|
|
set referrals_enabled(v: boolean) {
|
|
this._referralsEnabled = v;
|
|
if (!window.referralsEnabled) return;
|
|
if (!v) {
|
|
this._referralsEnabledCheck.textContent = ``;
|
|
} else {
|
|
this._referralsEnabledCheck.innerHTML = `<i class="ri-check-line" aria-label="${window.lang.strings("enabled")}"></i>`;
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<i class="icon ri-settings-2-line dropdown-button"></i>
|
|
<div class="dropdown manual over-top">
|
|
<div class="dropdown-display lg">
|
|
<div class="card ~neutral @low flex flex-col gap-2 w-max">
|
|
<div class="supra sm">${window.lang.strings("contactThrough")}</div>
|
|
<div class="accounts-area-email">
|
|
<label class="row switch flex flex-row gap-2">
|
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
|
</span>Email</span>
|
|
</label>
|
|
</div>
|
|
<div class="accounts-area-telegram">
|
|
<label class="row switch flex flex-row gap-2">
|
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
|
|
<span>Telegram</span>
|
|
</label>
|
|
</div>
|
|
<div class="accounts-area-discord">
|
|
<label class="row switch flex flex-row gap-2">
|
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
|
|
<span>Discord</span>
|
|
</label>
|
|
</div>
|
|
<div class="accounts-area-matrix">
|
|
<label class="row switch flex flex-row gap-2">
|
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-matrix">
|
|
<span>Matrix</span>
|
|
</label>
|
|
</div>
|
|
<div class="supra sm accounts-unlink-header">${window.lang.strings("unlink")}:</div>
|
|
<div class="accounts-unlink-telegram">
|
|
<button class="button ~critical w-full">Telegram</button>
|
|
</div>
|
|
<div class="accounts-unlink-discord">
|
|
<button class="button ~critical w-full">Discord</button>
|
|
</div>
|
|
<div class="accounts-unlink-matrix">
|
|
<button class="button ~critical w-full">Matrix</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
el.innerHTML = innerHTML;
|
|
const button = el.querySelector("i");
|
|
const dropdown = el.querySelector("div.dropdown") as HTMLDivElement;
|
|
const checks = el.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
|
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 = `
|
|
<div class="table-inline justify-center">
|
|
<span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span>
|
|
<input type="text" class="input ~neutral @low stealth-input unfocused" placeholder="@user:riot.im">
|
|
</div>
|
|
`;
|
|
(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 = `
|
|
<div class="accounts-settings-area flex flex-row gap-2 justify-center">
|
|
${u}
|
|
</div>
|
|
`;
|
|
if (lastNotifyMethod) {
|
|
(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;
|
|
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 = `<div class="table-inline justify-center"><span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span></div>`;
|
|
(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 = `
|
|
<div class="accounts-settings-area flex flex-row gap-2 justify-center">
|
|
<a class="force-ltr" href="https://t.me/${u}" target="_blank">@${u}</a>
|
|
</div>
|
|
`;
|
|
if (lastNotifyMethod) {
|
|
(this._telegram.querySelector(".accounts-settings-area") 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 = `<div class="table-inline justify-center"><span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span></div>`;
|
|
(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 = `
|
|
<div class="accounts-settings-area flex flex-row gap-2 justify-center">
|
|
<a href="https://discord.com/users/${this._discordID}" class="discord-link force-ltr" target="_blank">${u}</a>
|
|
</div>
|
|
`;
|
|
if (lastNotifyMethod) {
|
|
(this._discord.querySelector(".accounts-settings-area") 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._labelEditor.value = l ? l : "";
|
|
}
|
|
|
|
matchesSearch = (query: string): boolean => {
|
|
return (
|
|
this.id.includes(query) ||
|
|
this.name.toLowerCase().includes(query) ||
|
|
this.label.toLowerCase().includes(query) ||
|
|
this.email.toLowerCase().includes(query) ||
|
|
this.discord.toLowerCase().includes(query) ||
|
|
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 });
|
|
|
|
constructor(user: UserDTO) {
|
|
super();
|
|
let innerHTML = `
|
|
<td><input type="checkbox" class="accounts-select-user" value=""></td>
|
|
<td><div class="flex flex-row gap-2 items-center">
|
|
<span class="accounts-username hover:underline hover:cursor-pointer"></span>
|
|
<div class="flex flex-row gap-2 items-baseline">
|
|
<span class="accounts-label-container" title="${window.lang.strings("label")}"></span>
|
|
<span class="accounts-admin chip ~info hidden"></span>
|
|
<span class="accounts-disabled chip ~warning hidden"></span></span>
|
|
</div>
|
|
</div></td>
|
|
`;
|
|
if (window.jellyfinLogin) {
|
|
innerHTML += `
|
|
<td><div class="table-inline justify-center"><input type="checkbox" class="accounts-access-jfa" value=""></div></td>
|
|
`;
|
|
}
|
|
innerHTML += `
|
|
<td><div class="flex flex-row gap-2 items-baseline">
|
|
<span class="accounts-email-container" title="${window.lang.strings("emailAddress")}"></span>
|
|
</div></td>
|
|
`;
|
|
if (window.telegramEnabled) {
|
|
innerHTML += `
|
|
<td class="accounts-telegram"></td>
|
|
`;
|
|
}
|
|
if (window.matrixEnabled) {
|
|
innerHTML += `
|
|
<td class="accounts-matrix"></td>
|
|
`;
|
|
}
|
|
if (window.discordEnabled) {
|
|
innerHTML += `
|
|
<td class="accounts-discord"></td>
|
|
`;
|
|
}
|
|
if (window.referralsEnabled) {
|
|
innerHTML += `
|
|
<td class="accounts-referrals grid gap-4 place-items-stretch"></td>
|
|
`;
|
|
}
|
|
innerHTML += `
|
|
<td class="accounts-expiry"></td>
|
|
<td class="accounts-last-active whitespace-nowrap"></td>
|
|
`;
|
|
this._row.innerHTML = innerHTML;
|
|
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._username.onclick = () =>
|
|
document.dispatchEvent(
|
|
new CustomEvent("accounts-show-details", {
|
|
detail: {
|
|
username: this.name,
|
|
id: this.id,
|
|
},
|
|
}),
|
|
);
|
|
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._emailEditor = new HiddenInputField({
|
|
container: this._email,
|
|
onSet: this._updateEmail,
|
|
customContainerHTML: `<span class="hidden-input-content"></span>`,
|
|
buttonOnLeft: true,
|
|
clickAwayShouldSave: false,
|
|
});
|
|
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._labelEditor = new HiddenInputField({
|
|
container: this._label,
|
|
onSet: this._updateLabel,
|
|
customContainerHTML: `<span class="chip ~gray hidden-input-content"></span>`,
|
|
buttonOnLeft: true,
|
|
clickAwayShouldSave: false,
|
|
});
|
|
|
|
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"));
|
|
}
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
if (window.referralsEnabled) {
|
|
this._referralsEnabledCheck = this._row.querySelector(".accounts-referrals");
|
|
}
|
|
|
|
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;
|
|
_post("/users/labels", send, (req: XMLHttpRequest) => {
|
|
if (req.readyState == 4) {
|
|
if (req.status != 204) {
|
|
this.label = this._labelEditor.previous;
|
|
window.notifications.customError("labelChanged", window.lang.notif("errorUnknown"));
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
private _updateEmail = () => {
|
|
let send = {};
|
|
send[this.id] = this._emailEditor.value;
|
|
_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 = this._emailEditor.previous;
|
|
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;
|
|
}
|
|
set id(v: string) {
|
|
this._id = v;
|
|
this._row.setAttribute(SearchableItemDataAttribute, v);
|
|
}
|
|
|
|
update = (user: UserDTO) => {
|
|
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;
|
|
this.referrals_enabled = user.referrals_enabled;
|
|
};
|
|
|
|
remove = () => {
|
|
if (this.selected && !this.notInList) {
|
|
document.dispatchEvent(this._uncheckEvent());
|
|
}
|
|
super.remove();
|
|
};
|
|
}
|
|
|
|
interface UsersDTO extends PaginatedDTO {
|
|
users: UserDTO[];
|
|
}
|
|
|
|
declare interface ExtendExpiryDTO {
|
|
users: string[];
|
|
months?: number;
|
|
days?: number;
|
|
hours?: number;
|
|
minutes?: number;
|
|
timestamp?: number;
|
|
notify: boolean;
|
|
reason?: string;
|
|
try_extend_from_previous_expiry?: boolean;
|
|
}
|
|
|
|
export class accountsList extends PaginatedList implements Navigatable, AsTab {
|
|
readonly tabName = "accounts";
|
|
readonly pagePath = "accounts";
|
|
private _details: UserInfo;
|
|
private _table = document.getElementById("accounts-table") as HTMLTableElement;
|
|
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;
|
|
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 _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 _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;
|
|
private _usingExtendExpiryTextInput = true;
|
|
|
|
private _extendExpiryDate = document.getElementById("extend-expiry-date") as HTMLElement;
|
|
private _removeExpiry = document.getElementById("accounts-remove-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 _modifySettingsProfileSource: RadioBasedTabSelector;
|
|
private _enableReferrals = document.getElementById("accounts-enable-referrals") as HTMLSpanElement;
|
|
/*private _enableReferralsProfile = document.getElementById("radio-referrals-use-profile") as HTMLInputElement;
|
|
private _enableReferralsInvite = document.getElementById("radio-referrals-use-invite") as HTMLInputElement;*/
|
|
private _enableReferralsSource: RadioBasedTabSelector;
|
|
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 _referralsProfileSelect = document.getElementById("enable-referrals-user-profiles") as HTMLSelectElement;
|
|
private _referralsInviteSelect = document.getElementById("enable-referrals-user-invites") as HTMLSelectElement;
|
|
private _referralsExpiry = document.getElementById("enable-referrals-user-expiry") as HTMLInputElement;
|
|
|
|
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 _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
|
|
private _selectAllState: SelectAllState = SelectAllState.None;
|
|
// private _users: { [id: string]: user };
|
|
// private _ordering: string[] = [];
|
|
get users(): Map<string, User> {
|
|
return this._search.items as Map<string, User>;
|
|
}
|
|
// set users(v: { [id: string]: user }) { this._search.items = v as SearchableItems; }
|
|
|
|
// 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 } = {};
|
|
|
|
// Whether the "Extend expiry" is extending or setting an expiry.
|
|
private _settingExpiry = false;
|
|
|
|
private _sortingByButton = document.getElementById("accounts-sort-by-field") as HTMLButtonElement;
|
|
|
|
private _maxDayHourMinuteOptions = 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._maxDayHourMinuteOptions; n++) {
|
|
const opt = document.createElement("option") as HTMLOptionElement;
|
|
opt.textContent = "" + n;
|
|
opt.value = "" + n;
|
|
field.appendChild(opt);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
private _inDetails: boolean = false;
|
|
details(jfId?: string) {
|
|
if (!jfId) jfId = this.users.keys().next().value;
|
|
this._details.load(
|
|
this.users.get(jfId),
|
|
() => {
|
|
this.unbindPageEvents();
|
|
this._inDetails = true;
|
|
// To make things look better, run processSelectedAccounts before -actually- unhiding.
|
|
this.processSelectedAccounts();
|
|
this._table.classList.add("unfocused");
|
|
this._details.hidden = false;
|
|
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set("details", jfId);
|
|
window.history.pushState(null, "", url.toString());
|
|
},
|
|
this.closeDetails,
|
|
);
|
|
}
|
|
|
|
closeDetails = () => {
|
|
if (!this._inDetails) return;
|
|
this._inDetails = false;
|
|
this.processSelectedAccounts();
|
|
this._details.hidden = true;
|
|
this._table.classList.remove("unfocused");
|
|
this.bindPageEvents();
|
|
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete("details");
|
|
window.history.pushState(null, "", url.toString());
|
|
};
|
|
|
|
constructor() {
|
|
super({
|
|
loader: document.getElementById("accounts-loader"),
|
|
loadMoreButtons: Array.from([
|
|
document.getElementById("accounts-load-more") as HTMLButtonElement,
|
|
]) as Array<HTMLButtonElement>,
|
|
loadAllButtons: Array.from(
|
|
document.getElementsByClassName("accounts-load-all"),
|
|
) as Array<HTMLButtonElement>,
|
|
refreshButton: document.getElementById("accounts-refresh") as HTMLButtonElement,
|
|
filterArea: document.getElementById("accounts-filter-area"),
|
|
searchOptionsHeader: document.getElementById("accounts-search-options-header"),
|
|
searchBox: document.getElementById("accounts-search") as HTMLInputElement,
|
|
recordCounter: document.getElementById("accounts-record-counter"),
|
|
totalEndpoint: "/users/count",
|
|
getPageEndpoint: "/users",
|
|
itemsPerPage: 40,
|
|
maxItemsLoadedForSearch: 200,
|
|
appendNewItems: (resp: PaginatedDTO) => {
|
|
// console.log("append");
|
|
for (let u of (resp as UsersDTO).users || []) {
|
|
if (this.users.has(u.id)) {
|
|
this.users.get(u.id).update(u);
|
|
} else {
|
|
this.add(u);
|
|
}
|
|
}
|
|
|
|
this._search.setOrdering(
|
|
this._columns[this._search.sortField].sort(this.users),
|
|
this._search.sortField,
|
|
this._search.ascending,
|
|
);
|
|
},
|
|
replaceWithNewItems: (resp: PaginatedDTO) => {
|
|
// console.log("replace");
|
|
let accountsOnDOM = new Map<string, boolean>();
|
|
|
|
for (let id of this.users.keys()) {
|
|
accountsOnDOM.set(id, true);
|
|
}
|
|
for (let u of (resp as UsersDTO).users || []) {
|
|
if (accountsOnDOM.has(u.id)) {
|
|
this.users.get(u.id).update(u);
|
|
accountsOnDOM.delete(u.id);
|
|
} else {
|
|
this.add(u);
|
|
}
|
|
}
|
|
|
|
// Delete accounts w/ remaining IDs (those not in resp.users)
|
|
// console.log("Removing", Object.keys(accountsOnDOM).length, "from DOM");
|
|
for (let id of accountsOnDOM.keys()) {
|
|
this.users.get(id).remove();
|
|
this.users.delete(id);
|
|
}
|
|
|
|
this._search.setOrdering(
|
|
this._columns[this._search.sortField].sort(this.users),
|
|
this._search.sortField,
|
|
this._search.ascending,
|
|
);
|
|
},
|
|
defaultSortField: USER_DEFAULT_SORT_FIELD,
|
|
defaultSortAscending: USER_DEFAULT_SORT_ASCENDING,
|
|
pageLoadCallback: (req: XMLHttpRequest) => {
|
|
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,
|
|
searchOptionsHeader: this._c.searchOptionsHeader,
|
|
notFoundPanel: document.getElementById("accounts-not-found"),
|
|
notFoundLocallyText: document.getElementById("accounts-no-local-results"),
|
|
filterList: document.getElementById("accounts-filter-list"),
|
|
search: this._c.searchBox,
|
|
queries: queries(),
|
|
setVisibility: null,
|
|
clearSearchButtonSelector: ".accounts-search-clear",
|
|
serverSearchButtonSelector: ".accounts-search-server",
|
|
onSearchCallback: (_0: boolean, _1: boolean) => {
|
|
this.closeDetails();
|
|
this.processSelectedAccounts();
|
|
},
|
|
searchServer: null,
|
|
clearServerSearch: null,
|
|
};
|
|
|
|
this.initSearch(searchConfig);
|
|
|
|
this._selectAll.checked = false;
|
|
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();
|
|
});
|
|
this._addUserButton.onclick = () => {
|
|
this._populateAddUserProfiles();
|
|
window.modals.addUser.toggle();
|
|
};
|
|
this._addUserForm.addEventListener("submit", this._addUser);
|
|
|
|
this._deleteNotify.onchange = () => {
|
|
if (this._deleteNotify.checked) {
|
|
this._deleteReason.classList.remove("unfocused");
|
|
} else {
|
|
this._deleteReason.classList.add("unfocused");
|
|
}
|
|
};
|
|
this._modifySettings.onclick = this.modifyUsers;
|
|
this._modifySettings.classList.add("unfocused");
|
|
|
|
this._modifySettingsProfileSource = new RadioBasedTabSelector(
|
|
document.getElementById("modify-user-profile-source"),
|
|
"modify-user-profile-source",
|
|
{
|
|
name: window.lang.strings("profile"),
|
|
id: "profile",
|
|
content: this._profileSelect.parentElement,
|
|
onShow: () => {
|
|
this._applyOmbi?.parentElement.classList.remove("unfocused");
|
|
this._applyJellyseerr?.parentElement.classList.remove("unfocused");
|
|
},
|
|
},
|
|
{
|
|
name: window.lang.strings("user"),
|
|
id: "user",
|
|
content: this._userSelect.parentElement,
|
|
onShow: () => {
|
|
this._applyOmbi?.parentElement.classList.add("unfocused");
|
|
this._applyJellyseerr?.parentElement.classList.add("unfocused");
|
|
},
|
|
},
|
|
);
|
|
|
|
if (window.referralsEnabled) {
|
|
this._enableReferralsSource = new RadioBasedTabSelector(
|
|
document.getElementById("enable-referrals-user-source"),
|
|
"enable-referrals-user-source",
|
|
{
|
|
name: window.lang.strings("profile"),
|
|
id: "profile",
|
|
content: this._referralsProfileSelect.parentElement,
|
|
},
|
|
{
|
|
name: window.lang.strings("invite"),
|
|
id: "invite",
|
|
content: this._referralsInviteSelect.parentElement,
|
|
},
|
|
);
|
|
this._enableReferrals.onclick = () => {
|
|
this.enableReferrals();
|
|
this._enableReferralsSource.selected = 0;
|
|
};
|
|
}
|
|
|
|
this._deleteUser.onclick = this.deleteUsers;
|
|
this._deleteUser.classList.add("unfocused");
|
|
|
|
this._announceButton.onclick = this.announce;
|
|
this._announceButton.parentElement.classList.add("unfocused");
|
|
|
|
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._displayExpiryDate();
|
|
};
|
|
|
|
this._extendExpiryTextInput.onclick = () => {
|
|
this._extendExpiryTextInput.parentElement.classList.remove("opacity-60");
|
|
this._extendExpiryFieldInputs.classList.add("opacity-60");
|
|
this._usingExtendExpiryTextInput = true;
|
|
this._displayExpiryDate();
|
|
};
|
|
|
|
this._extendExpiryFieldInputs.onclick = () => {
|
|
this._extendExpiryFieldInputs.classList.remove("opacity-60");
|
|
this._extendExpiryTextInput.parentElement.classList.add("opacity-60");
|
|
this._usingExtendExpiryTextInput = false;
|
|
this._displayExpiryDate();
|
|
};
|
|
|
|
this._extendExpiryFromPreviousExpiry.onclick = this._displayExpiryDate;
|
|
|
|
for (let field of ["months", "days", "hours", "minutes"]) {
|
|
(document.getElementById("extend-expiry-" + field) as HTMLSelectElement).onchange = () => {
|
|
this._extendExpiryFieldInputs.classList.remove("opacity-60");
|
|
this._extendExpiryTextInput.parentElement.classList.add("opacity-60");
|
|
this._usingExtendExpiryTextInput = false;
|
|
this._displayExpiryDate();
|
|
};
|
|
}
|
|
|
|
this._disableEnable.onclick = this.enableDisableUsers;
|
|
this._disableEnable.parentElement.classList.add("unfocused");
|
|
|
|
this._enableExpiry.onclick = () => {
|
|
this.extendExpiry(true);
|
|
};
|
|
this._enableExpiryNotify.onchange = () => {
|
|
if (this._enableExpiryNotify.checked) {
|
|
this._enableExpiryReason.classList.remove("unfocused");
|
|
} else {
|
|
this._enableExpiryReason.classList.add("unfocused");
|
|
}
|
|
};
|
|
|
|
if (!window.usernameEnabled) {
|
|
this._addUserName.classList.add("unfocused");
|
|
this._addUserName = this._addUserEmail;
|
|
}
|
|
|
|
if (!window.linkResetEnabled) {
|
|
this._sendPWR.classList.add("unfocused");
|
|
} else {
|
|
this._sendPWR.onclick = this.sendPWR;
|
|
}
|
|
/*if (!window.emailEnabled) {
|
|
this._deleteNotify.parentElement.classList.add("unfocused");
|
|
this._deleteNotify.checked = false;
|
|
}*/
|
|
|
|
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;
|
|
}
|
|
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;
|
|
announceVarUsername.onclick = () => {
|
|
insertText(this._announceTextarea, announceVarUsername.children[0].textContent);
|
|
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",
|
|
];
|
|
for (let i = 0; i < headerNames.length; i++) {
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Start off sorting by username (this._c.defaultSortField)
|
|
const defaultSort = () => {
|
|
document.dispatchEvent(new CustomEvent("header-click", { detail: this._c.defaultSortField }));
|
|
this._columns[this._c.defaultSortField].ascending = this._c.defaultSortAscending;
|
|
this._columns[this._c.defaultSortField].hideIcon();
|
|
this._sortingByButton.classList.add("hidden");
|
|
this._search.showHideSearchOptionsHeader();
|
|
};
|
|
|
|
this._sortingByButton.addEventListener("click", defaultSort);
|
|
|
|
document.addEventListener("header-click", (event: CustomEvent) => {
|
|
this._search.setOrdering(
|
|
this._columns[event.detail].sort(this.users),
|
|
event.detail,
|
|
this._columns[event.detail].ascending,
|
|
);
|
|
this._sortingByButton.replaceChildren(this._columns[event.detail].asElement());
|
|
this._sortingByButton.classList.remove("hidden");
|
|
// console.log("ordering by", event.detail, ": ", this._ordering);
|
|
if (this._search.inSearch) {
|
|
this._search.onSearchBoxChange();
|
|
} else {
|
|
this.setVisibility(this._search.ordering, true);
|
|
this._search.setNotFoundPanelVisibility(false);
|
|
}
|
|
this._search.inServerSearch = false;
|
|
this.autoSetServerSearchButtonsDisabled();
|
|
this._search.showHideSearchOptionsHeader();
|
|
});
|
|
|
|
defaultSort();
|
|
|
|
this._search.showHideSearchOptionsHeader();
|
|
|
|
this.registerURLListener();
|
|
// FIXME: registerParamListener once PageManager is global
|
|
//
|
|
this._details = new UserInfo(document.getElementsByClassName("accounts-details")[0] as HTMLElement);
|
|
document.addEventListener("accounts-show-details", (ev: ShowDetailsEvent) => {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set("details", ev.detail.id);
|
|
window.history.pushState(null, "", url.toString());
|
|
// this.details(ev.detail.id);
|
|
});
|
|
|
|
// Get rid of nasty CSS
|
|
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);
|
|
};
|
|
|
|
loadAll = (callback?: (resp?: PaginatedDTO) => void) => {
|
|
this._loadAll(callback);
|
|
};
|
|
|
|
get selectAll(): SelectAllState {
|
|
return this._selectAllState;
|
|
}
|
|
|
|
cycleSelectAll = () => {
|
|
let next: SelectAllState;
|
|
switch (this.selectAll) {
|
|
case SelectAllState.None:
|
|
case SelectAllState.Some:
|
|
next = SelectAllState.AllVisible;
|
|
break;
|
|
case SelectAllState.AllVisible:
|
|
next = SelectAllState.All;
|
|
break;
|
|
case SelectAllState.All:
|
|
next = SelectAllState.None;
|
|
break;
|
|
}
|
|
|
|
this._selectAllState = next;
|
|
console.debug("New check state:", next);
|
|
|
|
if (next == SelectAllState.None) {
|
|
// Deselect -all- users, rather than just visible ones, to be safe.
|
|
for (let id of this.users.keys()) {
|
|
this.users.get(id).setSelected(false, false);
|
|
}
|
|
this._selectAll.checked = false;
|
|
this._selectAll.indeterminate = false;
|
|
this.processSelectedAccounts();
|
|
return;
|
|
}
|
|
|
|
// FIXME: Decide whether to keep the AllVisible/All distinction and actually use it, or to get rid of it an just make "load all" more visible.
|
|
const selectAllVisible = () => {
|
|
let count = 0;
|
|
for (let id of this._visible) {
|
|
this.users.get(id).setSelected(true, false);
|
|
count++;
|
|
}
|
|
console.debug("Selected", count);
|
|
this._selectAll.checked = true;
|
|
if (this.lastPage) {
|
|
this._selectAllState = SelectAllState.All;
|
|
}
|
|
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) {
|
|
// Pretend to live-select elements as they load.
|
|
this._counter.selected = this._counter.shown;
|
|
return;
|
|
}
|
|
selectAllVisible();
|
|
});
|
|
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.get(id).asElement())) continue;
|
|
this.users.get(id).selected = true;
|
|
if (id == endID) return;
|
|
}
|
|
};
|
|
|
|
add = (u: UserDTO) => {
|
|
this.users.set(u.id, new User(u));
|
|
// console.log("after appending lengths:", Object.keys(this.users).length, Object.keys(this._search.items).length);
|
|
};
|
|
|
|
private processSelectedAccounts = () => {
|
|
console.debug("processSelectedAccounts");
|
|
const list = this._collectUsers();
|
|
this._counter.selected = list.length;
|
|
if (this._counter.selected == 0) {
|
|
this._selectAll.indeterminate = false;
|
|
this._selectAll.checked = false;
|
|
this._selectAll.title = "";
|
|
this._modifySettings.classList.add("unfocused");
|
|
if (window.referralsEnabled) {
|
|
this._enableReferrals.classList.add("unfocused");
|
|
}
|
|
this._deleteUser.classList.add("unfocused");
|
|
if (window.emailEnabled || window.telegramEnabled) {
|
|
this._announceButton.parentElement.classList.add("unfocused");
|
|
}
|
|
this._expiryDropdown.classList.add("unfocused");
|
|
this._disableEnable.parentElement.classList.add("unfocused");
|
|
this._sendPWR.classList.add("unfocused");
|
|
} else {
|
|
if (this._counter.selected == this._visible.length) {
|
|
this._selectAll.checked = true;
|
|
if (this.lastPage) {
|
|
this._selectAll.indeterminate = false;
|
|
this._selectAll.title = window.lang.strings("allMatchingSelected");
|
|
// FIXME: Hover text "all matching records selected."
|
|
} else {
|
|
this._selectAll.indeterminate = true;
|
|
this._selectAll.title = window.lang.strings("allLoadedSelected");
|
|
// FIXME: Hover text "all loaded matching records selected. Click again to load all."
|
|
}
|
|
} else {
|
|
this._selectAll.checked = false;
|
|
this._selectAll.indeterminate = true;
|
|
}
|
|
this._modifySettings.classList.remove("unfocused");
|
|
if (window.referralsEnabled) {
|
|
this._enableReferrals.classList.remove("unfocused");
|
|
}
|
|
this._deleteUser.classList.remove("unfocused");
|
|
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
|
|
if (window.emailEnabled || window.telegramEnabled) {
|
|
this._announceButton.parentElement.classList.remove("unfocused");
|
|
}
|
|
|
|
let anyNonExpiries = list.length == 0 ? true : false;
|
|
let allNonExpiries = true;
|
|
let noContactCount = 0;
|
|
let referralState = Number(this.users.get(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.get(list[0]).disabled;
|
|
let showDisableEnable = true;
|
|
for (let id of list) {
|
|
if (!anyNonExpiries && !this.users.get(id).expiry) {
|
|
anyNonExpiries = true;
|
|
this._expiryDropdown.classList.add("unfocused");
|
|
}
|
|
if (this.users.get(id).expiry) {
|
|
allNonExpiries = false;
|
|
}
|
|
if (showDisableEnable && this.users.get(id).disabled != this._shouldEnable) {
|
|
showDisableEnable = false;
|
|
this._disableEnable.parentElement.classList.add("unfocused");
|
|
}
|
|
if (!showDisableEnable && anyNonExpiries) {
|
|
break;
|
|
}
|
|
if (!this.users.get(id).lastNotifyMethod()) {
|
|
noContactCount++;
|
|
}
|
|
if (
|
|
window.referralsEnabled &&
|
|
referralState != -1 &&
|
|
Number(this.users.get(id).referrals_enabled) != referralState
|
|
) {
|
|
referralState = -1;
|
|
}
|
|
}
|
|
this._settingExpiry = false;
|
|
if (!anyNonExpiries && !allNonExpiries) {
|
|
this._expiryDropdown.classList.remove("unfocused");
|
|
this._extendExpiry.textContent = window.lang.strings("extendExpiry");
|
|
this._removeExpiry.classList.remove("unfocused");
|
|
}
|
|
if (allNonExpiries) {
|
|
this._expiryDropdown.classList.remove("unfocused");
|
|
this._extendExpiry.textContent = window.lang.strings("setExpiry");
|
|
this._settingExpiry = true;
|
|
this._removeExpiry.classList.add("unfocused");
|
|
}
|
|
// Only show "Send PWR" if a maximum of 1 user selected doesn't have a contact method
|
|
if (noContactCount > 1) {
|
|
this._sendPWR.classList.add("unfocused");
|
|
} else if (window.linkResetEnabled) {
|
|
this._sendPWR.classList.remove("unfocused");
|
|
}
|
|
if (showDisableEnable) {
|
|
let message: string;
|
|
if (this._shouldEnable) {
|
|
this._disableEnable.parentElement.classList.remove("manual");
|
|
message = window.lang.strings("reEnable");
|
|
this._disableEnable.classList.add("~positive");
|
|
this._disableEnable.classList.remove("~warning");
|
|
} else {
|
|
this._disableEnable.parentElement.classList.add("manual");
|
|
message = window.lang.strings("disable");
|
|
this._disableEnable.classList.add("~warning");
|
|
this._disableEnable.classList.remove("~positive");
|
|
}
|
|
this._disableEnable.parentElement.classList.remove("unfocused");
|
|
this._disableEnable.textContent = message;
|
|
}
|
|
if (window.referralsEnabled) {
|
|
if (referralState == -1) {
|
|
this._enableReferrals.classList.add("unfocused");
|
|
} else {
|
|
this._enableReferrals.classList.remove("unfocused");
|
|
}
|
|
if (referralState == 0) {
|
|
this._enableReferrals.classList.add("~urge");
|
|
this._enableReferrals.classList.remove("~warning");
|
|
this._enableReferrals.textContent = window.lang.strings("enableReferrals");
|
|
} else if (referralState == 1) {
|
|
this._enableReferrals.classList.add("~warning");
|
|
this._enableReferrals.classList.remove("~urge");
|
|
this._enableReferrals.textContent = window.lang.strings("disableReferrals");
|
|
}
|
|
}
|
|
if (this._details.hidden) {
|
|
this._c.loadAllButtons.forEach((el) => {
|
|
// FIXME: Using hidden here instead of unfocused so that it doesn't interfere with any PaginatedList behaviour. Don't do this.
|
|
el.classList.add("hidden");
|
|
});
|
|
this._addUserButton.classList.remove("unfocused");
|
|
} else {
|
|
this._c.loadAllButtons.forEach((el) => {
|
|
// FIXME: Using hidden here instead of unfocused so that it doesn't interfere with any PaginatedList behaviour. Don't do this.
|
|
el.classList.add("hidden");
|
|
});
|
|
this._addUserButton.classList.add("unfocused");
|
|
}
|
|
}
|
|
};
|
|
|
|
private _collectUsers = (): string[] => {
|
|
if (this._inDetails && this._details.jfId != "") return [this._details.jfId];
|
|
let list: string[] = [];
|
|
for (let id of this._visible) {
|
|
if (this.users.get(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,
|
|
};
|
|
for (let field in send) {
|
|
if (!send[field]) {
|
|
window.notifications.customError("addUserBlankField", window.lang.notif("errorBlankFields"));
|
|
return;
|
|
}
|
|
}
|
|
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");
|
|
}
|
|
} 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);
|
|
}
|
|
if (req.response["error"] as String) {
|
|
console.error(req.response["error"]);
|
|
}
|
|
|
|
this.reload();
|
|
window.modals.addUser.close();
|
|
}
|
|
},
|
|
true,
|
|
);
|
|
};
|
|
loadPreview = () => {
|
|
let content = this._announceTextarea.value;
|
|
if (!this._previewLoaded) {
|
|
content = stripMarkdown(content);
|
|
this._announcePreview.textContent = content;
|
|
} else {
|
|
content = Marked.parse(content);
|
|
this._announcePreview.innerHTML = content;
|
|
}
|
|
};
|
|
saveAnnouncement = (event: Event) => {
|
|
event.preventDefault();
|
|
const form = document.getElementById("form-announce") as HTMLFormElement;
|
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
|
if (this._announceNameLabel.classList.contains("unfocused")) {
|
|
this._announceNameLabel.classList.remove("unfocused");
|
|
form.onsubmit = this.saveAnnouncement;
|
|
button.textContent = window.lang.get("strings", "saveAsTemplate");
|
|
this._announceSaveButton.classList.add("unfocused");
|
|
const details = document.getElementById("announce-details");
|
|
details.classList.add("unfocused");
|
|
return;
|
|
}
|
|
const name = (this._announceNameLabel.querySelector("input") as HTMLInputElement).value;
|
|
if (!name) {
|
|
return;
|
|
}
|
|
const subject = document.getElementById("announce-subject") as HTMLInputElement;
|
|
let send: announcementTemplate = {
|
|
name: name,
|
|
subject: subject.value,
|
|
message: this._announceTextarea.value,
|
|
};
|
|
_post("/users/announce/template", send, (req: XMLHttpRequest) => {
|
|
if (req.readyState == 4) {
|
|
this.reload();
|
|
toggleLoader(button);
|
|
window.modals.announce.close();
|
|
if (req.status != 200 && req.status != 204) {
|
|
window.notifications.customError("announcementError", window.lang.notif("errorFailureCheckLogs"));
|
|
} else {
|
|
window.notifications.customSuccess("announcementSuccess", window.lang.notif("savedAnnouncement"));
|
|
}
|
|
}
|
|
});
|
|
};
|
|
announce = (event?: Event, template?: announcementTemplate) => {
|
|
const modalHeader = document.getElementById("header-announce");
|
|
modalHeader.textContent = window.lang.quantity("announceTo", this._collectUsers().length);
|
|
const form = document.getElementById("form-announce") as HTMLFormElement;
|
|
let list = this._collectUsers();
|
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
|
removeLoader(button);
|
|
button.textContent = window.lang.get("strings", "send");
|
|
const details = document.getElementById("announce-details");
|
|
details.classList.remove("unfocused");
|
|
this._announceSaveButton.classList.remove("unfocused");
|
|
const subject = document.getElementById("announce-subject") as HTMLInputElement;
|
|
this._announceNameLabel.classList.add("unfocused");
|
|
if (template) {
|
|
subject.value = template.subject;
|
|
this._announceTextarea.value = template.message;
|
|
} else {
|
|
subject.value = "";
|
|
this._announceTextarea.value = "";
|
|
}
|
|
form.onsubmit = (event: Event) => {
|
|
event.preventDefault();
|
|
toggleLoader(button);
|
|
let send = {
|
|
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"),
|
|
);
|
|
} else {
|
|
window.notifications.customSuccess(
|
|
"announcementSuccess",
|
|
window.lang.notif("sentAnnouncement"),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
_get("/config/emails/Announcement", null, (req: XMLHttpRequest) => {
|
|
if (req.readyState == 4) {
|
|
const preview = document.getElementById("announce-preview") as HTMLDivElement;
|
|
if (req.status != 200) {
|
|
preview.innerHTML = `<pre class="preview-content" class="font-mono bg-inherit"></pre>`;
|
|
window.modals.announce.show();
|
|
this._previewLoaded = false;
|
|
return;
|
|
}
|
|
|
|
let templ = req.response as templateEmail;
|
|
if (!templ.html) {
|
|
preview.innerHTML = `<pre class="preview-content" class="font-mono bg-inherit"></pre>`;
|
|
this._previewLoaded = false;
|
|
} else {
|
|
preview.innerHTML = templ.html;
|
|
this._previewLoaded = true;
|
|
}
|
|
this._announcePreview = preview.getElementsByClassName("preview-content")[0] as HTMLElement;
|
|
this.loadPreview();
|
|
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")} <i class="ri-arrow-drop-down-line"></i>`;
|
|
}
|
|
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 = `
|
|
<span class="button ~neutral sm full-width accounts-announce-template-button">${name}</span><span class="button ~critical accounts-announce-template-delete">×</span>
|
|
`;
|
|
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);
|
|
}
|
|
});
|
|
};
|
|
(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();
|
|
}
|
|
});
|
|
};
|
|
dList.appendChild(el);
|
|
}
|
|
}
|
|
});
|
|
|
|
private _enableDisableUsers = (
|
|
users: string[],
|
|
enable: boolean,
|
|
notify: boolean,
|
|
reason: string | null,
|
|
post: (req: XMLHttpRequest) => void,
|
|
) => {
|
|
let send = {
|
|
users: users,
|
|
enabled: enable,
|
|
notify: notify,
|
|
};
|
|
if (reason) send["reason"] = reason;
|
|
_post("/users/enable", send, post, true);
|
|
};
|
|
|
|
enableDisableUsers = () => {
|
|
// We can share the delete modal for this
|
|
const modalHeader = document.getElementById("header-delete-user");
|
|
const form = document.getElementById("form-delete-user") as HTMLFormElement;
|
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
|
let list = this._collectUsers();
|
|
if (this._shouldEnable) {
|
|
modalHeader.textContent = window.lang.quantity("reEnableUsers", list.length);
|
|
button.textContent = window.lang.strings("reEnable");
|
|
button.classList.add("~urge");
|
|
button.classList.remove("~critical");
|
|
} else {
|
|
modalHeader.textContent = window.lang.quantity("disableUsers", list.length);
|
|
button.textContent = window.lang.strings("disable");
|
|
button.classList.add("~critical");
|
|
button.classList.remove("~urge");
|
|
}
|
|
this._deleteNotify.checked = false;
|
|
this._deleteReason.value = "";
|
|
this._deleteReason.classList.add("unfocused");
|
|
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");
|
|
}
|
|
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();
|
|
}
|
|
},
|
|
);
|
|
};
|
|
window.modals.deleteUser.show();
|
|
};
|
|
|
|
deleteUsers = () => {
|
|
const modalHeader = document.getElementById("header-delete-user");
|
|
let list = this._collectUsers();
|
|
modalHeader.textContent = window.lang.quantity("deleteNUsers", list.length);
|
|
const form = document.getElementById("form-delete-user") as HTMLFormElement;
|
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
|
button.textContent = window.lang.strings("delete");
|
|
button.classList.add("~critical");
|
|
button.classList.remove("~urge");
|
|
this._deleteNotify.checked = false;
|
|
this._deleteReason.value = "";
|
|
this._deleteReason.classList.add("unfocused");
|
|
form.onsubmit = (event: Event) => {
|
|
event.preventDefault();
|
|
toggleLoader(button);
|
|
let send = {
|
|
users: list,
|
|
notify: this._deleteNotify.checked,
|
|
reason: this._deleteNotify ? this._deleteReason.value : "",
|
|
};
|
|
_delete("/users", send, (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 {
|
|
window.notifications.customSuccess(
|
|
"deleteUserSuccess",
|
|
window.lang.quantity("deletedUser", list.length),
|
|
);
|
|
}
|
|
this.reload();
|
|
}
|
|
});
|
|
};
|
|
window.modals.deleteUser.show();
|
|
};
|
|
|
|
sendPWR = () => {
|
|
addLoader(this._sendPWR);
|
|
let list = this._collectUsers();
|
|
let manualUser: User;
|
|
for (let id of list) {
|
|
let user = this.users.get(id);
|
|
if (!user.lastNotifyMethod() && !user.email) {
|
|
manualUser = user;
|
|
break;
|
|
}
|
|
}
|
|
const messageBox = document.getElementById("send-pwr-note") as HTMLParagraphElement;
|
|
let message: string;
|
|
let send = {
|
|
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) {
|
|
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,
|
|
);
|
|
};
|
|
|
|
modifyUsers = () => {
|
|
const modalHeader = document.getElementById("header-modify-user");
|
|
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length);
|
|
let list = this._collectUsers();
|
|
|
|
(() => {
|
|
let innerHTML = "";
|
|
for (const profile of window.availableProfiles) {
|
|
innerHTML += `<option value="${profile}">${profile}</option>`;
|
|
}
|
|
this._profileSelect.innerHTML = innerHTML;
|
|
})();
|
|
|
|
(() => {
|
|
let innerHTML = "";
|
|
for (let id of this.users.keys()) {
|
|
innerHTML += `<option value="${id}">${this.users.get(id).name}</option>`;
|
|
}
|
|
this._userSelect.innerHTML = innerHTML;
|
|
})();
|
|
|
|
const form = document.getElementById("form-modify-user") as HTMLFormElement;
|
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
|
|
|
this._modifySettingsProfileSource.selected = 0;
|
|
form.onsubmit = (event: Event) => {
|
|
event.preventDefault();
|
|
toggleLoader(button);
|
|
let send = {
|
|
apply_to: list,
|
|
homescreen: this._applyHomescreen.checked,
|
|
configuration: this._applyConfiguration.checked,
|
|
};
|
|
if (window.ombiEnabled) {
|
|
send["ombi"] = this._applyOmbi.checked;
|
|
}
|
|
if (window.jellyseerrEnabled) {
|
|
send["jellyseerr"] = this._applyJellyseerr.checked;
|
|
}
|
|
if (this._modifySettingsProfileSource.selected == "profile") {
|
|
send["from"] = "profile";
|
|
send["profile"] = this._profileSelect.value;
|
|
} else if (this._modifySettingsProfileSource.selected == "user") {
|
|
send["from"] = "user";
|
|
send["id"] = this._userSelect.value;
|
|
}
|
|
_post("/users/settings", send, (req: XMLHttpRequest) => {
|
|
if (req.readyState == 4) {
|
|
toggleLoader(button);
|
|
if (req.status == 500) {
|
|
let response = JSON.parse(req.response);
|
|
let errorMsg = "";
|
|
if ("homescreen" in response && "policy" in response) {
|
|
const homescreen = Object.keys(response["homescreen"]).length;
|
|
const policy = Object.keys(response["policy"]).length;
|
|
if (homescreen != 0 && policy == 0) {
|
|
errorMsg = window.lang.notif("errorSettingsAppliedNoHomescreenLayout");
|
|
} else if (policy != 0 && homescreen == 0) {
|
|
errorMsg = window.lang.notif("errorHomescreenAppliedNoSettings");
|
|
} else if (policy != 0 && homescreen != 0) {
|
|
errorMsg = window.lang.notif("errorSettingsFailed");
|
|
}
|
|
} else if ("error" in response) {
|
|
errorMsg = response["error"];
|
|
}
|
|
window.notifications.customError("modifySettingsError", errorMsg);
|
|
} else if (req.status == 200 || req.status == 204) {
|
|
window.notifications.customSuccess(
|
|
"modifySettingsSuccess",
|
|
window.lang.quantity("appliedSettings", this._collectUsers().length),
|
|
);
|
|
}
|
|
this.reload();
|
|
window.modals.modifyUser.close();
|
|
}
|
|
});
|
|
};
|
|
window.modals.modifyUser.show();
|
|
};
|
|
|
|
enableReferrals = () => {
|
|
const modalHeader = document.getElementById("header-enable-referrals-user");
|
|
modalHeader.textContent = window.lang.quantity("enableReferralsFor", this._collectUsers().length);
|
|
let list = this._collectUsers();
|
|
|
|
// Check if we're disabling or enabling
|
|
if (this.users.get(list[0]).referrals_enabled) {
|
|
_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),
|
|
);
|
|
this.reload();
|
|
});
|
|
return;
|
|
}
|
|
|
|
(() => {
|
|
_get("/invites", null, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4 || req.status != 200) return;
|
|
|
|
// 1. Invites
|
|
|
|
let innerHTML = "";
|
|
let invites = req.response["invites"] as Array<Invite>;
|
|
if (invites) {
|
|
for (let inv of invites) {
|
|
let name = inv.code;
|
|
if (inv.label) {
|
|
name = `${inv.label} (${inv.code})`;
|
|
}
|
|
innerHTML += `<option value="${inv.code}">${name}</option>`;
|
|
}
|
|
this._enableReferralsSource.selected = "invite";
|
|
} else {
|
|
this._enableReferralsSource.selected = "profile";
|
|
innerHTML += `<option>${window.lang.strings("inviteNoInvites")}</option>`;
|
|
}
|
|
this._referralsInviteSelect.innerHTML = innerHTML;
|
|
|
|
// 2. Profiles
|
|
|
|
innerHTML = "";
|
|
for (const profile of window.availableProfiles) {
|
|
innerHTML += `<option value="${profile}">${profile}</option>`;
|
|
}
|
|
this._referralsProfileSelect.innerHTML = innerHTML;
|
|
});
|
|
})();
|
|
|
|
const form = document.getElementById("form-enable-referrals-user") as HTMLFormElement;
|
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
|
form.onsubmit = (event: Event) => {
|
|
event.preventDefault();
|
|
toggleLoader(button);
|
|
let send = {
|
|
users: list,
|
|
};
|
|
// console.log("profile:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
|
|
if (this._enableReferralsSource.selected == "profile") {
|
|
send["from"] = "profile";
|
|
send["profile"] = this._referralsProfileSelect.value;
|
|
} else if (this._enableReferralsSource.selected == "invite") {
|
|
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),
|
|
);
|
|
}
|
|
this.reload();
|
|
window.modals.enableReferralsUser.close();
|
|
}
|
|
},
|
|
);
|
|
};
|
|
this._enableReferralsSource.selected = 0;
|
|
this._referralsExpiry.checked = false;
|
|
window.modals.enableReferralsUser.show();
|
|
};
|
|
|
|
removeExpiry = () => {
|
|
const list = this._collectUsers();
|
|
|
|
let success = true;
|
|
for (let id of list) {
|
|
_delete("/users/" + id + "/expiry", null, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4) return;
|
|
if (req.status != 200) {
|
|
success = false;
|
|
return;
|
|
}
|
|
});
|
|
if (!success) break;
|
|
}
|
|
|
|
if (success) {
|
|
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;
|
|
let invalid = false;
|
|
let cantShow = false;
|
|
let users = this._collectUsers();
|
|
if (this._usingExtendExpiryTextInput) {
|
|
date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date;
|
|
invalid = "invalid" in (date as any);
|
|
} else {
|
|
if (this._extendExpiryFromPreviousExpiry.checked) {
|
|
cantShow = true;
|
|
} else {
|
|
let fields: Array<HTMLSelectElement> = [
|
|
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,
|
|
];
|
|
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.get(id).expiry * 1000);
|
|
if (this.users.get(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);
|
|
}
|
|
}
|
|
}
|
|
const submit = this._extendExpiryForm.querySelector(`input[type="submit"]`) as HTMLInputElement;
|
|
const submitSpan = submit.nextElementSibling;
|
|
if (invalid || cantShow) {
|
|
this._extendExpiryDate.classList.add("unfocused");
|
|
}
|
|
if (invalid) {
|
|
submit.disabled = true;
|
|
submitSpan.classList.add("opacity-60");
|
|
} else {
|
|
submit.disabled = false;
|
|
submitSpan.classList.remove("opacity-60");
|
|
if (!cantShow) {
|
|
this._extendExpiryDate.innerHTML = `
|
|
<div class="flex flex-col">
|
|
<span>${window.lang.strings("accountWillExpire").replace("{date}", toDateString(date))}</span>
|
|
${users.length > 1 ? "<span>" + window.lang.strings("expirationBasedOn") + "</span>" : ""}
|
|
</div>
|
|
`;
|
|
this._extendExpiryDate.classList.remove("unfocused");
|
|
}
|
|
}
|
|
};
|
|
|
|
extendExpiry = (enableUser?: boolean) => {
|
|
const list = this._collectUsers();
|
|
let applyList: string[] = [];
|
|
for (let id of list) {
|
|
applyList.push(id);
|
|
}
|
|
this._enableExpiryReason.classList.add("unfocused");
|
|
this._enableExpiryNotify.parentElement.classList.remove("unfocused");
|
|
this._enableExpiryNotify.checked = false;
|
|
this._enableExpiryReason.value = "";
|
|
let header: string;
|
|
if (enableUser) {
|
|
header = window.lang.quantity("reEnableUsers", list.length);
|
|
} else if (this._settingExpiry) {
|
|
header = window.lang.quantity("setExpiry", list.length);
|
|
// this._enableExpiryNotify.parentElement.classList.add("unfocused");
|
|
} else {
|
|
header = window.lang.quantity("extendExpiry", applyList.length);
|
|
// this._enableExpiryNotify.parentElement.classList.add("unfocused");
|
|
}
|
|
document.getElementById("header-extend-expiry").textContent = header;
|
|
const extend = () => {
|
|
let send: ExtendExpiryDTO = {
|
|
users: applyList,
|
|
timestamp: 0,
|
|
notify: this._enableExpiryNotify.checked,
|
|
};
|
|
if (this._enableExpiryNotify.checked) {
|
|
send.reason = this._enableExpiryReason.value;
|
|
}
|
|
if (this._usingExtendExpiryTextInput) {
|
|
let date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date;
|
|
send.timestamp = Math.floor(date.getTime() / 1000);
|
|
if ("invalid" in (date as any)) {
|
|
window.notifications.customError("extendExpiryError", window.lang.notif("errorInvalidDate"));
|
|
return;
|
|
}
|
|
} else {
|
|
if (this._extendExpiryFromPreviousExpiry.checked) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
_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"),
|
|
);
|
|
} else {
|
|
window.notifications.customSuccess(
|
|
"extendExpiry",
|
|
window.lang.quantity("extendedExpiry", applyList.length),
|
|
);
|
|
}
|
|
window.modals.extendExpiry.close();
|
|
this.reload();
|
|
}
|
|
});
|
|
};
|
|
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");
|
|
}
|
|
window.notifications.customError("deleteUserError", errorMsg);
|
|
return;
|
|
}
|
|
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 = `<option value="none">${window.lang.strings("inviteNoProfile")}</option>`;
|
|
for (let i = 0; i < window.availableProfiles.length; i++) {
|
|
innerHTML += `<option value="${window.availableProfiles[i]}" ${i == 0 ? "selected" : ""}>${window.availableProfiles[i]}</option>`;
|
|
}
|
|
this._addUserProfile.innerHTML = innerHTML;
|
|
};
|
|
|
|
focusAccount = (userID: string) => {
|
|
console.debug("focusing user", userID);
|
|
this._search.setQueryParam(`id:"${userID}"`);
|
|
if (userID in this.users) this.users.get(userID).focus();
|
|
};
|
|
|
|
public static readonly _accountURLEvent = "account-url";
|
|
registerURLListener = () => {
|
|
document.addEventListener(accountsList._accountURLEvent, (event: CustomEvent) => {
|
|
this.focusAccount(event.detail);
|
|
});
|
|
window.tabs.pages.registerParamListener(
|
|
this.tabName,
|
|
(_: URLSearchParams) => {
|
|
this.navigate();
|
|
},
|
|
"user",
|
|
"details",
|
|
"search",
|
|
);
|
|
};
|
|
|
|
isURL = (url?: string) => {
|
|
const urlParams = new URLSearchParams(url || window.location.search);
|
|
const userID = urlParams.get("user");
|
|
const details = urlParams.get("details");
|
|
return Boolean(userID) || Boolean(details) || this._search.isURL(url);
|
|
};
|
|
|
|
navigate = (url?: string) => {
|
|
const urlParams = new URLSearchParams(url || window.location.search);
|
|
const details = urlParams.get("details");
|
|
const userID = details || urlParams.get("user");
|
|
let search = urlParams.get("search") || "";
|
|
if (userID) {
|
|
search = `id:"${userID}"`;
|
|
urlParams.set("search", search);
|
|
// Get rid of it, as it'll now be included in the "search" param anyway
|
|
urlParams.delete("user");
|
|
}
|
|
this._search.navigate(urlParams.toString(), () => {
|
|
if (details) this.details(details);
|
|
});
|
|
};
|
|
|
|
clearURL() {
|
|
this._search.clearURL();
|
|
}
|
|
}
|
|
|
|
// An alternate view showing accounts in sub-lists grouped by group/label.
|
|
export class groupedAccountsList {}
|
|
|
|
export const accountURLEvent = (id: string) => {
|
|
return new CustomEvent(accountsList._accountURLEvent, { detail: id });
|
|
};
|
|
|
|
type GetterReturnType = Boolean | boolean | String | Number | number;
|
|
type Getter = () => GetterReturnType;
|
|
|
|
// When a column is clicked, it broadcasts it's name and ordering to be picked up and stored by accountsList
|
|
// When list is refreshed, accountList calls method of the specific Column and re-orders accordingly.
|
|
// Listen for broadcast event from others, check its not us by comparing the header className in the message, then hide the arrow icon
|
|
class Column {
|
|
private _header: HTMLTableCellElement;
|
|
private _card: HTMLElement;
|
|
private _cardSortingByIcon: HTMLElement;
|
|
private _name: string;
|
|
private _headerContent: string;
|
|
private _getter: Getter;
|
|
private _ascending: boolean;
|
|
private _active: boolean;
|
|
|
|
constructor(header: HTMLTableCellElement, name: string, getter: Getter) {
|
|
this._header = header;
|
|
this._name = name;
|
|
this._headerContent = this._header.textContent;
|
|
this._getter = getter;
|
|
this._ascending = true;
|
|
this._active = false;
|
|
|
|
this._header.addEventListener("click", () => {
|
|
// If we are the active sort column, a click means to switch between ascending/descending.
|
|
if (this._active) {
|
|
this.ascending = !this.ascending;
|
|
return;
|
|
}
|
|
this._active = true;
|
|
this._header.setAttribute("aria-sort", this._headerContent);
|
|
this.updateHeader();
|
|
document.dispatchEvent(new CustomEvent("header-click", { detail: this._name }));
|
|
});
|
|
document.addEventListener("header-click", (event: CustomEvent) => {
|
|
if (event.detail != this._name) {
|
|
this._active = false;
|
|
this._header.removeAttribute("aria-sort");
|
|
this.hideIcon();
|
|
}
|
|
});
|
|
|
|
this._card = document.createElement("button");
|
|
this._card.classList.add("button", "~neutral", "@low", "center", "flex", "flex-row", "gap-1");
|
|
this._card.innerHTML = `
|
|
<i class="sorting-by-direction ri-arrow-up-s-line"></i>
|
|
<span class="font-bold">${window.lang.strings("sortingBy")}: </span><span>${this._headerContent}</span>
|
|
<i class="ri-close-line text-2xl"></i>
|
|
`;
|
|
this._cardSortingByIcon = this._card.querySelector(".sorting-by-direction");
|
|
}
|
|
|
|
hideIcon = () => {
|
|
this._header.textContent = this._headerContent;
|
|
};
|
|
|
|
updateHeader = () => {
|
|
this._header.innerHTML = `
|
|
<span class="">${this._headerContent}</span>
|
|
<i class="ri-arrow-${this._ascending ? "up" : "down"}-s-line" aria-hidden="true"></i>
|
|
`;
|
|
};
|
|
|
|
asElement = () => {
|
|
return this._card;
|
|
};
|
|
|
|
get ascending() {
|
|
return this._ascending;
|
|
}
|
|
set ascending(v: boolean) {
|
|
this._ascending = v;
|
|
if (v) {
|
|
this._cardSortingByIcon.classList.add("ri-arrow-up-s-line");
|
|
this._cardSortingByIcon.classList.remove("ri-arrow-down-s-line");
|
|
} else {
|
|
this._cardSortingByIcon.classList.add("ri-arrow-down-s-line");
|
|
this._cardSortingByIcon.classList.remove("ri-arrow-up-s-line");
|
|
}
|
|
if (!this._active) return;
|
|
this.updateHeader();
|
|
this._header.setAttribute("aria-sort", this._headerContent);
|
|
document.dispatchEvent(new CustomEvent("header-click", { detail: this._name }));
|
|
}
|
|
|
|
// Sorts the user list. previouslyActive is whether this column was previously sorted by, indicating that the direction should change.
|
|
sort = (users: Map<string, User>): string[] => {
|
|
let userIDs = Array.from(users.keys());
|
|
userIDs.sort((a: string, b: string): number => {
|
|
let av: GetterReturnType = this._getter.call(users.get(a));
|
|
if (typeof av === "string") av = av.toLowerCase();
|
|
let bv: GetterReturnType = this._getter.call(users.get(b));
|
|
if (typeof bv === "string") bv = bv.toLowerCase();
|
|
if (av < bv) return this._ascending ? -1 : 1;
|
|
if (av > bv) return this._ascending ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
return userIDs;
|
|
};
|
|
}
|
|
|
|
type ActivitySeverity =
|
|
| "Fatal"
|
|
| "None"
|
|
| "Trace"
|
|
| "Debug"
|
|
| "Information"
|
|
| "Info"
|
|
| "Warn"
|
|
| "Warning"
|
|
| "Error"
|
|
| "Critical";
|
|
interface ActivityLogEntryDTO {
|
|
Id: number;
|
|
Name: string;
|
|
Overview: string;
|
|
ShortOverview: string;
|
|
Type: string;
|
|
ItemId: string;
|
|
Date: number;
|
|
UserId: string;
|
|
UserPrimaryImageTag: string;
|
|
Severity: ActivitySeverity;
|
|
}
|
|
interface PaginatedActivityLogEntriesDTO {
|
|
entries: ActivityLogEntryDTO[];
|
|
last_page: boolean;
|
|
}
|
|
|
|
class ActivityLogEntry extends TableRow implements ActivityLogEntryDTO, SearchableItem {
|
|
private _e: ActivityLogEntryDTO;
|
|
private _username: string;
|
|
private _severity: HTMLElement;
|
|
private _user: HTMLElement;
|
|
private _name: HTMLElement;
|
|
private _type: HTMLElement;
|
|
private _overview: HTMLElement;
|
|
private _time: HTMLElement;
|
|
|
|
update = (user: string | null, entry: ActivityLogEntryDTO) => {
|
|
this._e = entry;
|
|
if (user != null) this.User = user;
|
|
this.Id = entry.Id;
|
|
this.Name = entry.Name;
|
|
this.Overview = entry.Overview;
|
|
this.ShortOverview = entry.ShortOverview;
|
|
this.Type = entry.Type;
|
|
this.ItemId = entry.ItemId;
|
|
this.Date = entry.Date;
|
|
this.UserId = entry.UserId;
|
|
this.UserPrimaryImageTag = entry.UserPrimaryImageTag;
|
|
this.Severity = entry.Severity;
|
|
};
|
|
|
|
constructor(user: string, entry: ActivityLogEntryDTO) {
|
|
super();
|
|
this._row.innerHTML = `
|
|
<td class="text-center-i"><span class="jf-activity-log-severity chip ~info dark:~d_info unfocused"></span></td>
|
|
<td class="jf-activity-log-user-name-combined max-w-96 truncate"><div class="flex flex-row gap-2 items-baseline"><span class="chip ~gray dark:~d_gray jf-activity-log-user unfocused"></span><span class="jf-activity-log-name truncate"></span></div></td>
|
|
<td class="jf-activity-log-type italic"></td>
|
|
<td class="jf-activity-log-overview"></td>
|
|
<td class="jf-activity-log-time"></td>
|
|
`;
|
|
this._severity = this._row.getElementsByClassName("jf-activity-log-severity")[0] as HTMLElement;
|
|
this._user = this._row.getElementsByClassName("jf-activity-log-user")[0] as HTMLElement;
|
|
this._name = this._row.getElementsByClassName("jf-activity-log-name")[0] as HTMLElement;
|
|
this._type = this._row.getElementsByClassName("jf-activity-log-type")[0] as HTMLElement;
|
|
this._overview = this._row.getElementsByClassName("jf-activity-log-overview")[0] as HTMLElement;
|
|
this._time = this._row.getElementsByClassName("jf-activity-log-time")[0] as HTMLElement;
|
|
this.update(user, entry);
|
|
}
|
|
|
|
matchesSearch = (query: string): boolean => {
|
|
return (
|
|
("" + this.Id).includes(query) ||
|
|
this.User.includes(query) ||
|
|
this.UserId.includes(query) ||
|
|
this.Name.includes(query) ||
|
|
this.Overview.includes(query) ||
|
|
this.ShortOverview.includes(query) ||
|
|
this.Type.includes(query) ||
|
|
this.ItemId.includes(query) ||
|
|
this.UserId.includes(query) ||
|
|
this.Severity.includes(query)
|
|
);
|
|
};
|
|
|
|
get Id(): number {
|
|
return this._e.Id;
|
|
}
|
|
set Id(v: number) {
|
|
this._e.Id = v;
|
|
}
|
|
|
|
private setName() {
|
|
const space = this.Name.indexOf(" ");
|
|
let nameContent = this.Name;
|
|
let endOfUserBadge = ":";
|
|
if (space != -1 && this.User != "" && nameContent.substring(0, space) == this.User) {
|
|
endOfUserBadge = "";
|
|
nameContent = nameContent.substring(space + 1, nameContent.length);
|
|
}
|
|
if (this.User == "") this._user.classList.add("unfocused");
|
|
else this._user.classList.remove("unfocused");
|
|
this._user.textContent = this.User + endOfUserBadge;
|
|
this._name.textContent = nameContent;
|
|
this._name.title = nameContent;
|
|
}
|
|
|
|
// User is the username of the user. It is not part of an ActivityLogEntryDTO, rather the UserId is, but in most contexts the parent should know it anyway.
|
|
get User(): string {
|
|
return this._username;
|
|
}
|
|
set User(v: string) {
|
|
this._username = v;
|
|
this.setName();
|
|
}
|
|
|
|
// Name is a description of the entry. It often starts with the name of the user, so
|
|
get Name(): string {
|
|
return this._e.Name;
|
|
}
|
|
set Name(v: string) {
|
|
this._e.Name = v;
|
|
this.setName();
|
|
}
|
|
|
|
// "Overview" doesn't seem to be used, but we'll let it take precedence anyway.
|
|
private setOverview() {
|
|
this._overview.textContent = this.Overview || this.ShortOverview;
|
|
}
|
|
|
|
// Overview is something, but I haven't seen it actually used.
|
|
get Overview(): string {
|
|
return this._e.Overview;
|
|
}
|
|
|
|
set Overview(v: string) {
|
|
this._e.Overview = v;
|
|
this.setOverview();
|
|
}
|
|
|
|
// ShortOverview usually seems to be the IP address of the user in applicable entries.
|
|
get ShortOverview(): string {
|
|
return this._e.ShortOverview;
|
|
}
|
|
set ShortOverview(v: string) {
|
|
this._e.ShortOverview = v;
|
|
this.setOverview();
|
|
}
|
|
|
|
get Type(): string {
|
|
return this._e.Type;
|
|
}
|
|
set Type(v: string) {
|
|
this._e.Type = v;
|
|
this._type.textContent = v;
|
|
}
|
|
|
|
get ItemId(): string {
|
|
return this._e.ItemId;
|
|
}
|
|
set ItemId(v: string) {
|
|
this._e.ItemId = v;
|
|
}
|
|
|
|
get Date(): number {
|
|
return this._e.Date;
|
|
}
|
|
set Date(v: number) {
|
|
this._e.Date = v;
|
|
this._time.textContent = toDateString(new Date(v * 1000));
|
|
}
|
|
|
|
get UserId(): string {
|
|
return this._e.UserId;
|
|
}
|
|
set UserId(v: string) {
|
|
this._e.UserId = v;
|
|
}
|
|
|
|
get UserPrimaryImageTag(): string {
|
|
return this._e.UserPrimaryImageTag;
|
|
}
|
|
set UserPrimaryImageTag(v: string) {
|
|
this._e.UserPrimaryImageTag = v;
|
|
}
|
|
|
|
get Severity(): ActivitySeverity {
|
|
return this._e.Severity;
|
|
}
|
|
set Severity(v: ActivitySeverity) {
|
|
this._e.Severity = v;
|
|
if (v) this._severity.classList.remove("unfocused");
|
|
else this._severity.classList.add("unfocused");
|
|
["~neutral", "~positive", "~warning", "~critical", "~info", "~urge"].forEach((c) =>
|
|
this._severity.classList.remove(c),
|
|
);
|
|
switch (v) {
|
|
case "Info":
|
|
case "Information":
|
|
this._severity.textContent = window.lang.strings("info");
|
|
this._severity.classList.add("~info");
|
|
break;
|
|
case "Debug":
|
|
case "Trace":
|
|
this._severity.textContent = window.lang.strings("debug");
|
|
this._severity.classList.add("~urge");
|
|
break;
|
|
case "Warn":
|
|
case "Warning":
|
|
this._severity.textContent = window.lang.strings("warn");
|
|
this._severity.classList.add("~warning");
|
|
break;
|
|
case "Error":
|
|
this._severity.textContent = window.lang.strings("error");
|
|
this._severity.classList.add("~critical");
|
|
break;
|
|
case "Critical":
|
|
case "Fatal":
|
|
this._severity.textContent = window.lang.strings("fatal");
|
|
this._severity.classList.add("~critical");
|
|
break;
|
|
case "None":
|
|
this._severity.textContent = window.lang.strings("none");
|
|
this._severity.classList.add("~neutral");
|
|
default:
|
|
console.warn("Unknown key in activity severity:", v);
|
|
this._severity.textContent = v;
|
|
this._severity.classList.add("~neutral");
|
|
}
|
|
}
|
|
}
|
|
|
|
interface ShowDetailsEvent extends Event {
|
|
detail: {
|
|
username: string;
|
|
id: string;
|
|
};
|
|
}
|
|
|
|
class UserInfo extends PaginatedList {
|
|
private _hidden: boolean = true;
|
|
private _table: HTMLElement;
|
|
private _card: HTMLElement;
|
|
private _rowArea: HTMLElement;
|
|
private _noResults: HTMLElement;
|
|
private _link: HTMLAnchorElement;
|
|
get entries(): Map<string, ActivityLogEntry> {
|
|
return this._search.items as Map<string, ActivityLogEntry>;
|
|
}
|
|
private _back: HTMLButtonElement;
|
|
username: string;
|
|
jfId: string;
|
|
constructor(card: HTMLElement) {
|
|
card.classList.add("unfocused");
|
|
card.innerHTML = `
|
|
<div class="flex flex-col gap-2">
|
|
<div class="overflow-x-scroll">
|
|
<table class="table text-base leading-5">
|
|
<thead class="user-info-table-header"></thead>
|
|
<tbody class="user-info-row"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="flex flex-col gap-2">
|
|
<div class="jf-activity-source flex flex-row justify-between gap-2">
|
|
<h3 class="heading text-lg">${window.lang.strings("activityFromJF")}</h3>
|
|
<a role="link" tabindex="0" class="jf-activity-jfa-link hover:underline cursor-pointer button ~info @high flex flex-row gap-1 items-baseline">${window.lang.strings("activityFromJfa")}<i class="icon ri-external-link-line text-current"></i></a>
|
|
</div>
|
|
<!-- <h2 class="heading text-2xl">${window.lang.strings("activity")}</h2> -->
|
|
<div class="card @low overflow-x-scroll jf-activity-table">
|
|
<table class="table text-xs leading-5">
|
|
<thead>
|
|
<tr>
|
|
<th>${window.lang.strings("severity")}</th>
|
|
<th>${window.lang.strings("details")}</th>
|
|
<th>${window.lang.strings("type")}</th>
|
|
<th>${window.lang.strings("other")}</th>
|
|
<th>${window.lang.strings("date")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="jf-activity-table-content"></tbody>
|
|
</table>
|
|
<div class="unfocused h-[100%] my-3 jf-activity-no-activity">
|
|
<div class="flex flex-col gap-2 h-[100%] justify-center items-center">
|
|
<span class="text-2xl font-medium italic text-center">${window.lang.strings("noActivityFound")}</span>
|
|
<span class="text-sm font-light italic text-center" id="accounts-no-local-results">${window.lang.strings("noActivityFoundLocally")}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-row gap-2 justify-center">
|
|
<button class="button ~neutral @low jf-activity-load-more">${window.lang.strings("loadMore")}</button>
|
|
<button class="button ~neutral @low accounts-load-all jf-activity-load-all">${window.lang.strings("loadAll")}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
super({
|
|
loader: card.getElementsByClassName("jf-activity-loader")[0] as HTMLElement,
|
|
loadMoreButtons: Array.from(
|
|
document.getElementsByClassName("jf-activity-load-more"),
|
|
) as Array<HTMLButtonElement>,
|
|
loadAllButtons: Array.from(
|
|
document.getElementsByClassName("jf-activity-load-all"),
|
|
) as Array<HTMLButtonElement>,
|
|
totalEndpoint: () => "/users/" + this.jfId + "/activities/jellyfin/count",
|
|
getPageEndpoint: () => "/users/" + this.jfId + "/activities/jellyfin",
|
|
itemsPerPage: 20,
|
|
maxItemsLoadedForSearch: 200,
|
|
disableSearch: true,
|
|
hideButtonsOnLastPage: true,
|
|
appendNewItems: (resp: PaginatedDTO) => {
|
|
for (let entry of (resp as PaginatedActivityLogEntriesDTO).entries || []) {
|
|
if (this.entries.has("" + entry.Id)) {
|
|
this.entries.get("" + entry.Id).update(null, entry);
|
|
} else {
|
|
this.add(entry);
|
|
}
|
|
}
|
|
|
|
this._search.setOrdering(Array.from(this.entries.keys()), "Date", true);
|
|
},
|
|
replaceWithNewItems: (resp: PaginatedDTO) => {
|
|
let entriesOnDOM = new Map<string, boolean>();
|
|
|
|
for (let id of this.entries.keys()) {
|
|
entriesOnDOM.set(id, true);
|
|
}
|
|
for (let entry of (resp as PaginatedActivityLogEntriesDTO).entries || []) {
|
|
if (entriesOnDOM.has("" + entry.Id)) {
|
|
this.entries.get("" + entry.Id).update(null, entry);
|
|
entriesOnDOM.delete("" + entry.Id);
|
|
} else {
|
|
this.add(entry);
|
|
}
|
|
}
|
|
|
|
// Delete entries w/ remaining IDs (those not in resp.entries)
|
|
// console.log("Removing", Object.keys(entriesOnDOM).length, "from DOM");
|
|
for (let id of entriesOnDOM.keys()) {
|
|
this.entries.get(id).remove();
|
|
this.entries.delete(id);
|
|
}
|
|
|
|
this._search.setOrdering(Array.from(this.entries.keys()), "Date", true);
|
|
},
|
|
});
|
|
this._hidden = true;
|
|
this._card = card;
|
|
this._rowArea = this._card.getElementsByClassName("user-info-row")[0] as HTMLElement;
|
|
const realHead = document.getElementById("accounts-table-header") as HTMLElement;
|
|
const cloneHead = realHead.cloneNode(true) as HTMLElement;
|
|
// Remove "select all" check from header
|
|
cloneHead.querySelector("input[type=checkbox]").remove();
|
|
const head = this._card.getElementsByClassName("user-info-table-header")[0] as HTMLElement;
|
|
head.replaceWith(cloneHead);
|
|
this._table = this._card.getElementsByClassName("jf-activity-table")[0] as HTMLElement;
|
|
this._container = this._table.getElementsByClassName("jf-activity-table-content")[0] as HTMLElement;
|
|
this._back = document.getElementById("user-details-back") as HTMLButtonElement;
|
|
this._noResults = this._card.getElementsByClassName("jf-activity-no-activity")[0] as HTMLElement;
|
|
this._link = this._card.getElementsByClassName("jf-activity-jfa-link")[0] as HTMLAnchorElement;
|
|
let searchConfig: SearchConfiguration = {
|
|
queries: {},
|
|
setVisibility: null,
|
|
searchServer: null,
|
|
clearServerSearch: null,
|
|
onSearchCallback: (_0: boolean, _1: boolean) => {},
|
|
notFoundPanel: this._noResults,
|
|
};
|
|
|
|
this.initSearch(searchConfig);
|
|
}
|
|
|
|
add = (entry: ActivityLogEntryDTO) => {
|
|
this.entries.set("" + entry.Id, new ActivityLogEntry(this.username, entry));
|
|
};
|
|
|
|
loadMore = (loadAll: boolean = false, callback?: (resp?: PaginatedDTO) => void) => {
|
|
this._loadMore(loadAll, callback);
|
|
};
|
|
|
|
loadAll = (callback?: (resp?: PaginatedDTO) => void) => {
|
|
this._loadAll(callback);
|
|
};
|
|
|
|
reload = (callback?: (resp: PaginatedDTO) => void) => {
|
|
this._reload(callback);
|
|
};
|
|
|
|
load = (user: User, onLoad?: () => void, onBack?: () => void) => {
|
|
this.username = user.name;
|
|
this.jfId = user.id;
|
|
const clone = new User(user);
|
|
clone.notInList = true;
|
|
clone.setSelected(true, false);
|
|
this._rowArea.replaceChildren(clone.asElement());
|
|
this._link.setAttribute("data-id", this.jfId);
|
|
this._link.href = `${window.pages.Base}${window.pages.Admin}/activity?user=${this.username}`;
|
|
|
|
if (onBack) {
|
|
this._back.classList.remove("unfocused");
|
|
this._back.onclick = () => onBack();
|
|
} else {
|
|
this._back.classList.add("unfocused");
|
|
this._back.onclick = null;
|
|
}
|
|
this.reload(onLoad);
|
|
/*_get("/users/" + jfId + "/activities/jellyfin", null, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4) return;
|
|
if (req.status != 200) {
|
|
window.notifications.customError("errorLoadJFActivities", window.lang.notif("errorLoadActivities"));
|
|
return;
|
|
}
|
|
// FIXME: Lazy loading table
|
|
this._table.textContent = ``;
|
|
let entries = (req.response as PaginatedActivityLogEntriesDTO).entries;
|
|
for (let entry of entries) {
|
|
const row = new ActivityLogEntry(username, entry);
|
|
this._table.appendChild(row.asElement());
|
|
}
|
|
if (onLoad) onLoad();
|
|
});*/
|
|
};
|
|
|
|
get hidden(): boolean {
|
|
return this._hidden;
|
|
}
|
|
set hidden(v: boolean) {
|
|
this._hidden = v;
|
|
if (v) {
|
|
this.unbindPageEvents();
|
|
this._card.classList.add("unfocused");
|
|
this._back.classList.add("unfocused");
|
|
this.username = "";
|
|
this.jfId = "";
|
|
} else {
|
|
this.bindPageEvents();
|
|
this._card.classList.remove("unfocused");
|
|
this._back.classList.remove("unfocused");
|
|
}
|
|
}
|
|
}
|