ts: format finally

formatted with biome, a config file is provided.
This commit is contained in:
Harvey Tindall
2025-12-08 20:38:30 +00:00
parent ca7c553147
commit 817107622a
29 changed files with 3956 additions and 2610 deletions

9
biome.json Normal file
View File

@@ -0,0 +1,9 @@
{
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4,
"formatWithErrors": false,
"lineWidth": 120
}
}

View File

@@ -20,7 +20,7 @@ loadLangSelector("admin");
// _get(`/lang/admin/${window.language}.json`, null, (req: XMLHttpRequest) => { // _get(`/lang/admin/${window.language}.json`, null, (req: XMLHttpRequest) => {
// if (req.readyState == 4 && req.status == 200) { // if (req.readyState == 4 && req.status == 200) {
// langLoaded = true; // langLoaded = true;
// window.lang = new lang(req.response as LangFile); // window.lang = new lang(req.response as LangFile);
// } // }
// }); // });
@@ -34,35 +34,37 @@ window.availableProfiles = window.availableProfiles || [];
(() => { (() => {
window.modals = {} as Modals; window.modals = {} as Modals;
window.modals.login = new Modal(document.getElementById('modal-login'), true); window.modals.login = new Modal(document.getElementById("modal-login"), true);
window.modals.addUser = new Modal(document.getElementById('modal-add-user')); window.modals.addUser = new Modal(document.getElementById("modal-add-user"));
window.modals.about = new Modal(document.getElementById('modal-about')); window.modals.about = new Modal(document.getElementById("modal-about"));
(document.getElementById('setting-about') as HTMLSpanElement).onclick = window.modals.about.toggle; (document.getElementById("setting-about") as HTMLSpanElement).onclick = window.modals.about.toggle;
window.modals.modifyUser = new Modal(document.getElementById('modal-modify-user')); window.modals.modifyUser = new Modal(document.getElementById("modal-modify-user"));
window.modals.deleteUser = new Modal(document.getElementById('modal-delete-user')); window.modals.deleteUser = new Modal(document.getElementById("modal-delete-user"));
window.modals.settingsRestart = new Modal(document.getElementById('modal-restart')); window.modals.settingsRestart = new Modal(document.getElementById("modal-restart"));
window.modals.settingsRefresh = new Modal(document.getElementById('modal-refresh')); window.modals.settingsRefresh = new Modal(document.getElementById("modal-refresh"));
window.modals.ombiProfile = new Modal(document.getElementById('modal-ombi-profile')); window.modals.ombiProfile = new Modal(document.getElementById("modal-ombi-profile"));
document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiProfile.close); document.getElementById("form-ombi-defaults").addEventListener("submit", window.modals.ombiProfile.close);
window.modals.jellyseerrProfile = new Modal(document.getElementById('modal-jellyseerr-profile')); window.modals.jellyseerrProfile = new Modal(document.getElementById("modal-jellyseerr-profile"));
document.getElementById('form-jellyseerr-defaults').addEventListener('submit', window.modals.jellyseerrProfile.close); document
.getElementById("form-jellyseerr-defaults")
.addEventListener("submit", window.modals.jellyseerrProfile.close);
window.modals.profiles = new Modal(document.getElementById("modal-user-profiles")); window.modals.profiles = new Modal(document.getElementById("modal-user-profiles"));
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile")); window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
window.modals.editProfile = new Modal(document.getElementById("modal-edit-profile")); window.modals.editProfile = new Modal(document.getElementById("modal-edit-profile"));
window.modals.announce = new Modal(document.getElementById("modal-announce")); window.modals.announce = new Modal(document.getElementById("modal-announce"));
window.modals.editor = new Modal(document.getElementById("modal-editor")); window.modals.editor = new Modal(document.getElementById("modal-editor"));
window.modals.customizeEmails = new Modal(document.getElementById("modal-customize")); window.modals.customizeEmails = new Modal(document.getElementById("modal-customize"));
@@ -74,7 +76,7 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.matrix = new Modal(document.getElementById("modal-matrix")); window.modals.matrix = new Modal(document.getElementById("modal-matrix"));
window.modals.logs = new Modal(document.getElementById("modal-logs")); window.modals.logs = new Modal(document.getElementById("modal-logs"));
window.modals.tasks = new Modal(document.getElementById("modal-tasks")); window.modals.tasks = new Modal(document.getElementById("modal-tasks"));
window.modals.backedUp = new Modal(document.getElementById("modal-backed-up")); window.modals.backedUp = new Modal(document.getElementById("modal-backed-up"));
@@ -111,7 +113,7 @@ var settings = new settingsList();
var profiles = new ProfileEditor(); var profiles = new ProfileEditor();
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement, 5);
/*const modifySettingsSource = function () { /*const modifySettingsSource = function () {
const profile = document.getElementById('radio-use-profile') as HTMLInputElement; const profile = document.getElementById('radio-use-profile') as HTMLInputElement;
@@ -131,46 +133,47 @@ let isInviteURL = window.invites.isInviteURL();
let isAccountURL = accounts.isAccountURL(); let isAccountURL = accounts.isAccountURL();
// load tabs // load tabs
const tabs: { id: string, url: string, reloader: () => void, unloader?: () => void }[] = [ const tabs: { id: string; url: string; reloader: () => void; unloader?: () => void }[] = [
{ {
id: "invites", id: "invites",
url: "", url: "",
reloader: () => window.invites.reload(() => { reloader: () =>
if (isInviteURL) { window.invites.reload(() => {
window.invites.loadInviteURL(); if (isInviteURL) {
// Don't keep loading the same item on every tab refresh window.invites.loadInviteURL();
isInviteURL = false; // Don't keep loading the same item on every tab refresh
} isInviteURL = false;
}), }
}),
}, },
{ {
id: "accounts", id: "accounts",
url: "accounts", url: "accounts",
reloader: () => accounts.reload(() => { reloader: () =>
if (isAccountURL) { accounts.reload(() => {
accounts.loadAccountURL(); if (isAccountURL) {
// Don't keep loading the same item on every tab refresh accounts.loadAccountURL();
isAccountURL = false; // Don't keep loading the same item on every tab refresh
} isAccountURL = false;
accounts.bindPageEvents(); }
}), accounts.bindPageEvents();
unloader: accounts.unbindPageEvents }),
unloader: accounts.unbindPageEvents,
}, },
{ {
id: "activity", id: "activity",
url: "activity", url: "activity",
reloader: () => { reloader: () => {
activity.reload() activity.reload();
activity.bindPageEvents(); activity.bindPageEvents();
}, },
unloader: activity.unbindPageEvents unloader: activity.unbindPageEvents,
}, },
{ {
id: "settings", id: "settings",
url: "settings", url: "settings",
reloader: settings.reload reloader: settings.reload,
} },
]; ];
const defaultTab = tabs[0]; const defaultTab = tabs[0];
@@ -178,10 +181,16 @@ const defaultTab = tabs[0];
window.tabs = new Tabs(); window.tabs = new Tabs();
for (let tab of tabs) { for (let tab of tabs) {
window.tabs.addTab(tab.id, window.pages.Base + window.pages.Admin + "/" + tab.url, null, tab.reloader, tab.unloader || null); window.tabs.addTab(
tab.id,
window.pages.Base + window.pages.Admin + "/" + tab.url,
null,
tab.reloader,
tab.unloader || null,
);
} }
let matchedTab = false let matchedTab = false;
for (const tab of tabs) { for (const tab of tabs) {
if (window.location.pathname.startsWith(window.pages.Base + window.pages.Current + "/" + tab.url)) { if (window.location.pathname.startsWith(window.pages.Base + window.pages.Current + "/" + tab.url)) {
window.tabs.switch(tab.url, true); window.tabs.switch(tab.url, true);
@@ -199,10 +208,13 @@ login.onLogin = () => {
window.updater = new Updater(); window.updater = new Updater();
// FIXME: Decide whether to autoload activity or not // FIXME: Decide whether to autoload activity or not
reloadProfileNames(); reloadProfileNames();
setInterval(() => { window.invites.reload(); accounts.reloadIfNotInScroll(); }, 30*1000); setInterval(() => {
window.invites.reload();
accounts.reloadIfNotInScroll();
}, 30 * 1000);
// Triggers pre and post funcs, even though we're already on that page // Triggers pre and post funcs, even though we're already on that page
window.tabs.switch(window.tabs.current); window.tabs.switch(window.tabs.current);
} };
bindManualDropdowns(); bindManualDropdowns();

View File

@@ -22,7 +22,7 @@ const buttonChange = (type: string) => {
buttonNormal.classList.add("@low"); buttonNormal.classList.add("@low");
buttonNormal.classList.remove("@high"); buttonNormal.classList.remove("@high");
} }
} };
buttonNormal.onclick = () => buttonChange("normal"); buttonNormal.onclick = () => buttonChange("normal");
buttonSanitized.onclick = () => buttonChange("sanitized"); buttonSanitized.onclick = () => buttonChange("sanitized");

View File

@@ -50,7 +50,6 @@ window.animationEvent = whichAnimationEvent();
window.successModal = new Modal(document.getElementById("modal-success"), true); window.successModal = new Modal(document.getElementById("modal-success"), true);
var telegramVerified = false; var telegramVerified = false;
if (window.telegramEnabled) { if (window.telegramEnabled) {
window.telegramModal = new Modal(document.getElementById("modal-telegram"), window.telegramRequired); window.telegramModal = new Modal(document.getElementById("modal-telegram"), window.telegramRequired);
@@ -74,23 +73,25 @@ if (window.telegramEnabled) {
checkbox.parentElement.classList.remove("unfocused"); checkbox.parentElement.classList.remove("unfocused");
checkbox.checked = true; checkbox.checked = true;
validator.validate(); validator.validate();
} },
}; };
const telegram = new Telegram(telegramConf); const telegram = new Telegram(telegramConf);
telegramButton.onclick = () => { telegram.onclick(); }; telegramButton.onclick = () => {
telegram.onclick();
};
} }
var discordVerified = false; var discordVerified = false;
if (window.discordEnabled) { if (window.discordEnabled) {
window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired); window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
const discordButton = document.getElementById("link-discord") as HTMLSpanElement; const discordButton = document.getElementById("link-discord") as HTMLSpanElement;
const discordConf: ServiceConfiguration = { const discordConf: ServiceConfiguration = {
modal: window.discordModal as Modal, modal: window.discordModal as Modal,
pin: window.discordPIN, pin: window.discordPIN,
inviteURL: window.discordInviteLink ? (window.pages.Form + "/" + window.code + "/discord/invite") : "", inviteURL: window.discordInviteLink ? window.pages.Form + "/" + window.code + "/discord/invite" : "",
pinURL: "", pinURL: "",
verifiedURL: window.pages.Form + "/" + window.code + "/discord/verified/", verifiedURL: window.pages.Form + "/" + window.code + "/discord/verified/",
invalidCodeError: window.messages["errorInvalidPIN"], invalidCodeError: window.messages["errorInvalidPIN"],
@@ -103,15 +104,17 @@ if (window.discordEnabled) {
document.getElementById("contact-via").classList.remove("unfocused"); document.getElementById("contact-via").classList.remove("unfocused");
document.getElementById("contact-via-email").parentElement.classList.remove("unfocused"); document.getElementById("contact-via-email").parentElement.classList.remove("unfocused");
const checkbox = document.getElementById("contact-via-discord") as HTMLInputElement; const checkbox = document.getElementById("contact-via-discord") as HTMLInputElement;
checkbox.parentElement.classList.remove("unfocused") checkbox.parentElement.classList.remove("unfocused");
checkbox.checked = true; checkbox.checked = true;
validator.validate(); validator.validate();
} },
}; };
const discord = new Discord(discordConf); const discord = new Discord(discordConf);
discordButton.onclick = () => { discord.onclick(); }; discordButton.onclick = () => {
discord.onclick();
};
} }
var matrixVerified = false; var matrixVerified = false;
@@ -119,7 +122,7 @@ var matrixPIN = "";
if (window.matrixEnabled) { if (window.matrixEnabled) {
window.matrixModal = new Modal(document.getElementById("modal-matrix"), window.matrixRequired); window.matrixModal = new Modal(document.getElementById("modal-matrix"), window.matrixRequired);
const matrixButton = document.getElementById("link-matrix") as HTMLSpanElement; const matrixButton = document.getElementById("link-matrix") as HTMLSpanElement;
const matrixConf: MatrixConfiguration = { const matrixConf: MatrixConfiguration = {
modal: window.matrixModal as Modal, modal: window.matrixModal as Modal,
sendMessageURL: window.pages.Form + "/" + window.code + "/matrix/user", sendMessageURL: window.pages.Form + "/" + window.code + "/matrix/user",
@@ -138,12 +141,14 @@ if (window.matrixEnabled) {
checkbox.parentElement.classList.remove("unfocused"); checkbox.parentElement.classList.remove("unfocused");
checkbox.checked = true; checkbox.checked = true;
validator.validate(); validator.validate();
} },
}; };
const matrix = new Matrix(matrixConf); const matrix = new Matrix(matrixConf);
matrixButton.onclick = () => { matrix.show(); }; matrixButton.onclick = () => {
matrix.show();
};
} }
if (window.confirmation) { if (window.confirmation) {
@@ -154,7 +159,7 @@ declare var window: formWindow;
if (window.userExpiryEnabled) { if (window.userExpiryEnabled) {
const messageEl = document.getElementById("user-expiry-message") as HTMLElement; const messageEl = document.getElementById("user-expiry-message") as HTMLElement;
const calculateTime = () => { const calculateTime = () => {
let time = new Date() let time = new Date();
time.setMonth(time.getMonth() + window.userExpiryMonths); time.setMonth(time.getMonth() + window.userExpiryMonths);
time.setDate(time.getDate() + window.userExpiryDays); time.setDate(time.getDate() + window.userExpiryDays);
time.setHours(time.getHours() + window.userExpiryHours); time.setHours(time.getHours() + window.userExpiryHours);
@@ -162,7 +167,7 @@ if (window.userExpiryEnabled) {
messageEl.textContent = window.userExpiryMessage.replace("{date}", toDateString(time)); messageEl.textContent = window.userExpiryMessage.replace("{date}", toDateString(time));
setTimeout(calculateTime, 1000); setTimeout(calculateTime, 1000);
}; };
document.addEventListener("timefmt-change", calculateTime) document.addEventListener("timefmt-change", calculateTime);
calculateTime(); calculateTime();
} }
@@ -174,7 +179,8 @@ let usernameField = document.getElementById("create-username") as HTMLInputEleme
const emailField = document.getElementById("create-email") as HTMLInputElement; const emailField = document.getElementById("create-email") as HTMLInputElement;
window.emailRequired &&= window.collectEmail; window.emailRequired &&= window.collectEmail;
if (!window.usernameEnabled) { if (!window.usernameEnabled) {
usernameField.parentElement.remove(); usernameField = emailField; usernameField.parentElement.remove();
usernameField = emailField;
} else if (!window.collectEmail) { } else if (!window.collectEmail) {
emailField.parentElement.classList.add("unfocused"); emailField.parentElement.classList.add("unfocused");
emailField.value = ""; emailField.value = "";
@@ -229,7 +235,7 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool
oncomplete(true); oncomplete(true);
} }
let baseValidator = captcha.baseValidatorWrapper(_baseValidator); let baseValidator = captcha.baseValidatorWrapper(_baseValidator);
declare var grecaptcha: GreCAPTCHA; declare var grecaptcha: GreCAPTCHA;
@@ -238,14 +244,14 @@ let validatorConf: ValidatorConf = {
rePasswordField: rePasswordField, rePasswordField: rePasswordField,
submitInput: submitInput, submitInput: submitInput,
submitButton: submitSpan, submitButton: submitSpan,
validatorFunc: baseValidator validatorFunc: baseValidator,
}; };
let validator = new Validator(validatorConf); let validator = new Validator(validatorConf);
var requirements = validator.requirements; var requirements = validator.requirements;
if (window.emailRequired) { if (window.emailRequired) {
emailField.addEventListener("keyup", validator.validate) emailField.addEventListener("keyup", validator.validate);
} }
interface sendDTO { interface sendDTO {
@@ -273,7 +279,6 @@ if (window.captcha && !window.reCAPTCHA) {
const create = (event: SubmitEvent) => { const create = (event: SubmitEvent) => {
event.preventDefault(); event.preventDefault();
if (window.captcha && !window.reCAPTCHA && !captcha.verified) { if (window.captcha && !window.reCAPTCHA && !captcha.verified) {
} }
addLoader(submitSpan); addLoader(submitSpan);
let send: sendDTO = { let send: sendDTO = {
@@ -281,8 +286,8 @@ const create = (event: SubmitEvent) => {
username: usernameField.value, username: usernameField.value,
email: emailField.value, email: emailField.value,
email_contact: true, email_contact: true,
password: passwordField.value password: passwordField.value,
} };
if (telegramVerified) { if (telegramVerified) {
send.telegram_pin = window.telegramPIN; send.telegram_pin = window.telegramPIN;
const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement; const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement;
@@ -316,62 +321,74 @@ const create = (event: SubmitEvent) => {
send.captcha_text = captcha.input.value; send.captcha_text = captcha.input.value;
} }
} }
_post("/user/invite", send, (req: XMLHttpRequest) => { _post(
if (req.readyState != 4) return; "/user/invite",
removeLoader(submitSpan); send,
let vals = req.response as ValidatorRespDTO; (req: XMLHttpRequest) => {
let valid = true; if (req.readyState != 4) return;
for (let type in vals) { removeLoader(submitSpan);
if (requirements[type]) requirements[type].valid = vals[type]; let vals = req.response as ValidatorRespDTO;
if (!vals[type]) valid = false; let valid = true;
} for (let type in vals) {
if (req.status == 200 && valid) { if (requirements[type]) requirements[type].valid = vals[type];
if (window.redirectToJellyfin == true) { if (!vals[type]) valid = false;
const url = ((document.getElementById("modal-success") as HTMLDivElement).querySelector("a.submit") as HTMLAnchorElement).href;
window.location.href = url;
} else {
if (window.customSuccessCard) {
const content = window.successModal.asElement().querySelector(".card");
content.innerHTML = content.innerHTML.replace(new RegExp("{username}", "g"), send.username)
} else if (window.userPageEnabled) {
const userPageNoticeArea = document.getElementById("modal-success-user-page-area");
const link = `<a href="${window.userPageAddress}" target="_blank">${userPageNoticeArea.getAttribute("my-account-term")}</a>`;
userPageNoticeArea.innerHTML = userPageNoticeArea.textContent.replace("{myAccount}", link);
}
window.successModal.show();
} }
} else if (req.status != 401 && req.status != 400){ if (req.status == 200 && valid) {
submitSpan.classList.add("~critical"); if (window.redirectToJellyfin == true) {
submitSpan.classList.remove("~urge"); const url = (
if (req.response["error"] as string) { (document.getElementById("modal-success") as HTMLDivElement).querySelector(
submitSpan.textContent = window.messages[req.response["error"]]; "a.submit",
} else { ) as HTMLAnchorElement
submitSpan.textContent = window.messages["errorPassword"]; ).href;
} window.location.href = url;
setTimeout(() => { } else {
submitSpan.classList.add("~urge"); if (window.customSuccessCard) {
submitSpan.classList.remove("~critical"); const content = window.successModal.asElement().querySelector(".card");
submitSpan.textContent = submitText; content.innerHTML = content.innerHTML.replace(new RegExp("{username}", "g"), send.username);
}, 1000); } else if (window.userPageEnabled) {
} const userPageNoticeArea = document.getElementById("modal-success-user-page-area");
}, true, (req: XMLHttpRequest) => { const link = `<a href="${window.userPageAddress}" target="_blank">${userPageNoticeArea.getAttribute("my-account-term")}</a>`;
if (req.readyState != 4) return; userPageNoticeArea.innerHTML = userPageNoticeArea.textContent.replace("{myAccount}", link);
removeLoader(submitSpan); }
if (req.status == 401 || req.status == 400) { window.successModal.show();
if (req.response["error"] as string) {
if (req.response["error"] == "confirmEmail") {
window.confirmationModal.show();
return;
} }
if (req.response["error"] in window.messages) { } else if (req.status != 401 && req.status != 400) {
submitSpan.classList.add("~critical");
submitSpan.classList.remove("~urge");
if (req.response["error"] as string) {
submitSpan.textContent = window.messages[req.response["error"]]; submitSpan.textContent = window.messages[req.response["error"]];
} else { } else {
submitSpan.textContent = req.response["error"]; submitSpan.textContent = window.messages["errorPassword"];
} }
setTimeout(() => { submitSpan.textContent = submitText; }, 1000); setTimeout(() => {
submitSpan.classList.add("~urge");
submitSpan.classList.remove("~critical");
submitSpan.textContent = submitText;
}, 1000);
} }
} },
}); true,
(req: XMLHttpRequest) => {
if (req.readyState != 4) return;
removeLoader(submitSpan);
if (req.status == 401 || req.status == 400) {
if (req.response["error"] as string) {
if (req.response["error"] == "confirmEmail") {
window.confirmationModal.show();
return;
}
if (req.response["error"] in window.messages) {
submitSpan.textContent = window.messages[req.response["error"]];
} else {
submitSpan.textContent = req.response["error"];
}
setTimeout(() => {
submitSpan.textContent = submitText;
}, 1000);
}
}
},
);
}; };
validator.validate(); validator.validate();
@@ -379,6 +396,6 @@ validator.validate();
form.onsubmit = create; form.onsubmit = create;
const invitedByAside = document.getElementById("invite-from-user"); const invitedByAside = document.getElementById("invite-from-user");
if (typeof(invitedByAside) != "undefined" && invitedByAside != null) { if (typeof invitedByAside != "undefined" && invitedByAside != null) {
invitedByAside.textContent = invitedByAside.textContent.replace("{user}", invitedByAside.getAttribute("data-from")); invitedByAside.textContent = invitedByAside.textContent.replace("{user}", invitedByAside.getAttribute("data-from"));
} }

View File

@@ -45,7 +45,7 @@ export interface ServiceConfiguration {
accountLinkedError: string; accountLinkedError: string;
successError: string; successError: string;
successFunc: (modalClosed: boolean) => void; successFunc: (modalClosed: boolean) => void;
}; }
export interface DiscordInvite { export interface DiscordInvite {
invite: string; invite: string;
@@ -61,7 +61,9 @@ export class ServiceLinker {
protected _name: string; protected _name: string;
protected _pin: string; protected _pin: string;
get verified(): boolean { return this._verified; } get verified(): boolean {
return this._verified;
}
constructor(conf: ServiceConfiguration) { constructor(conf: ServiceConfiguration) {
this._conf = conf; this._conf = conf;
@@ -90,7 +92,7 @@ export class ServiceLinker {
this._verified = true; this._verified = true;
this._waiting.classList.add("~positive"); this._waiting.classList.add("~positive");
this._waiting.classList.remove("~info"); this._waiting.classList.remove("~info");
window.notifications.customPositive(this._name + "Verified", "", this._conf.successError); window.notifications.customPositive(this._name + "Verified", "", this._conf.successError);
if (this._conf.successFunc) { if (this._conf.successFunc) {
this._conf.successFunc(false); this._conf.successFunc(false);
} }
@@ -100,7 +102,6 @@ export class ServiceLinker {
this._conf.successFunc(true); this._conf.successFunc(true);
} }
}, 2000); }, 2000);
} else if (!this._modalClosed) { } else if (!this._modalClosed) {
setTimeout(this._checkVerified, 1500); setTimeout(this._checkVerified, 1500);
} }
@@ -135,29 +136,29 @@ export class ServiceLinker {
} }
export class Discord extends ServiceLinker { export class Discord extends ServiceLinker {
constructor(conf: ServiceConfiguration) { constructor(conf: ServiceConfiguration) {
super(conf); super(conf);
this._name = "discord"; this._name = "discord";
this._waiting = document.getElementById("discord-waiting") as HTMLSpanElement; this._waiting = document.getElementById("discord-waiting") as HTMLSpanElement;
} }
private _getInviteURL = () => _get(this._conf.inviteURL, null, (req: XMLHttpRequest) => { private _getInviteURL = () =>
if (req.readyState != 4) return; _get(this._conf.inviteURL, null, (req: XMLHttpRequest) => {
const inv = req.response as DiscordInvite; if (req.readyState != 4) return;
const link = document.getElementById("discord-invite") as HTMLSpanElement; const inv = req.response as DiscordInvite;
(link.parentElement as HTMLAnchorElement).href = inv.invite; const link = document.getElementById("discord-invite") as HTMLSpanElement;
(link.parentElement as HTMLAnchorElement).target = "_blank"; (link.parentElement as HTMLAnchorElement).href = inv.invite;
let innerHTML = ``; (link.parentElement as HTMLAnchorElement).target = "_blank";
if (inv.icon != "") { let innerHTML = ``;
innerHTML += `<span class="img-circle lg"><img class="img-circle" src="${inv.icon}" width="64" height="64"></span>${window.discordServerName}`; if (inv.icon != "") {
} else { innerHTML += `<span class="img-circle lg"><img class="img-circle" src="${inv.icon}" width="64" height="64"></span>${window.discordServerName}`;
innerHTML += ` } else {
innerHTML += `
<span class="shield bg-discord"><i class="ri-discord-fill ri-xl text-white"></i></span>${window.discordServerName} <span class="shield bg-discord"><i class="ri-discord-fill ri-xl text-white"></i></span>${window.discordServerName}
`; `;
} }
link.innerHTML = innerHTML; link.innerHTML = innerHTML;
}); });
onclick() { onclick() {
if (this._conf.inviteURL != "") { if (this._conf.inviteURL != "") {
@@ -176,7 +177,7 @@ export class Telegram extends ServiceLinker {
this._name = "telegram"; this._name = "telegram";
this._waiting = document.getElementById("telegram-waiting") as HTMLSpanElement; this._waiting = document.getElementById("telegram-waiting") as HTMLSpanElement;
} }
}; }
export interface MatrixConfiguration { export interface MatrixConfiguration {
modal: Modal; modal: Modal;
@@ -198,14 +199,20 @@ export class Matrix {
private _input: HTMLInputElement; private _input: HTMLInputElement;
private _submit: HTMLSpanElement; private _submit: HTMLSpanElement;
get verified(): boolean { return this._verified; } get verified(): boolean {
get pin(): string { return this._pin; } return this._verified;
}
get pin(): string {
return this._pin;
}
constructor(conf: MatrixConfiguration) { constructor(conf: MatrixConfiguration) {
this._conf = conf; this._conf = conf;
this._input = document.getElementById("matrix-userid") as HTMLInputElement; this._input = document.getElementById("matrix-userid") as HTMLInputElement;
this._submit = document.getElementById("matrix-send") as HTMLSpanElement; this._submit = document.getElementById("matrix-send") as HTMLSpanElement;
this._submit.onclick = () => { this._onclick(); }; this._submit.onclick = () => {
this._onclick();
};
} }
private _onclick = () => { private _onclick = () => {
@@ -220,52 +227,53 @@ export class Matrix {
show = () => { show = () => {
this._input.value = ""; this._input.value = "";
this._conf.modal.show(); this._conf.modal.show();
} };
private _sendMessage = () => _post(this._conf.sendMessageURL, { "user_id": this._input.value }, (req: XMLHttpRequest) => { private _sendMessage = () =>
if (req.readyState != 4) return; _post(this._conf.sendMessageURL, { user_id: this._input.value }, (req: XMLHttpRequest) => {
removeLoader(this._submit); if (req.readyState != 4) return;
if (req.status == 400 && req.response["error"] == "errorAccountLinked") { removeLoader(this._submit);
this._conf.modal.close(); if (req.status == 400 && req.response["error"] == "errorAccountLinked") {
window.notifications.customError("accountLinkedError", this._conf.accountLinkedError); this._conf.modal.close();
return; window.notifications.customError("accountLinkedError", this._conf.accountLinkedError);
} else if (req.status != 200) { return;
this._conf.modal.close(); } else if (req.status != 200) {
window.notifications.customError("unknownError", this._conf.unknownError); this._conf.modal.close();
return; window.notifications.customError("unknownError", this._conf.unknownError);
} return;
this._userID = this._input.value;
this._submit.classList.add("~positive");
this._submit.classList.remove("~info");
setTimeout(() => {
this._submit.classList.add("~info");
this._submit.classList.remove("~positive");
}, 2000);
this._input.placeholder = "PIN";
this._input.value = "";
});
private _verifyCode = () => _get(this._conf.verifiedURL + this._userID + "/" + this._input.value, null, (req: XMLHttpRequest) => {
if (req.readyState != 4) return;
removeLoader(this._submit);
const valid = req.response["success"] as boolean;
if (valid) {
this._conf.modal.close();
window.notifications.customPositive(this._name + "Verified", "", this._conf.successError);
this._verified = true;
this._pin = this._input.value;
if (this._conf.successFunc) {
this._conf.successFunc();
} }
} else { this._userID = this._input.value;
window.notifications.customError("invalidCodeError", this._conf.invalidCodeError); this._submit.classList.add("~positive");
this._submit.classList.add("~critical");
this._submit.classList.remove("~info"); this._submit.classList.remove("~info");
setTimeout(() => { setTimeout(() => {
this._submit.classList.add("~info"); this._submit.classList.add("~info");
this._submit.classList.remove("~critical"); this._submit.classList.remove("~positive");
}, 800); }, 2000);
} this._input.placeholder = "PIN";
}); this._input.value = "";
} });
private _verifyCode = () =>
_get(this._conf.verifiedURL + this._userID + "/" + this._input.value, null, (req: XMLHttpRequest) => {
if (req.readyState != 4) return;
removeLoader(this._submit);
const valid = req.response["success"] as boolean;
if (valid) {
this._conf.modal.close();
window.notifications.customPositive(this._name + "Verified", "", this._conf.successError);
this._verified = true;
this._pin = this._input.value;
if (this._conf.successFunc) {
this._conf.successFunc();
}
} else {
window.notifications.customError("invalidCodeError", this._conf.invalidCodeError);
this._submit.classList.add("~critical");
this._submit.classList.remove("~info");
setTimeout(() => {
this._submit.classList.add("~info");
this._submit.classList.remove("~critical");
}, 800);
}
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,11 @@
import { _get, _post, _delete, toDateString } from "../modules/common.js"; import { _get, _post, _delete, toDateString } from "../modules/common.js";
import { SearchConfiguration, QueryType, SearchableItem, SearchableItems, SearchableItemDataAttribute } from "../modules/search.js"; import {
SearchConfiguration,
QueryType,
SearchableItem,
SearchableItems,
SearchableItemDataAttribute,
} from "../modules/search.js";
import { accountURLEvent } from "../modules/accounts.js"; import { accountURLEvent } from "../modules/accounts.js";
import { inviteURLEvent } from "../modules/invites.js"; import { inviteURLEvent } from "../modules/invites.js";
import { PaginatedList } from "./list.js"; import { PaginatedList } from "./list.js";
@@ -10,149 +16,151 @@ const ACTIVITY_DEFAULT_SORT_FIELD = "time";
const ACTIVITY_DEFAULT_SORT_ASCENDING = false; const ACTIVITY_DEFAULT_SORT_ASCENDING = false;
export interface activity { export interface activity {
id: string; id: string;
type: string; type: string;
user_id: string; user_id: string;
source_type: string; source_type: string;
source: string; source: string;
invite_code: string; invite_code: string;
value: string; value: string;
time: number; time: number;
username: string; username: string;
source_username: string; source_username: string;
ip: string; ip: string;
} }
var activityTypeMoods = { var activityTypeMoods = {
"creation": 1, creation: 1,
"deletion": -1, deletion: -1,
"disabled": -1, disabled: -1,
"enabled": 1, enabled: 1,
"contactLinked": 1, contactLinked: 1,
"contactUnlinked": -1, contactUnlinked: -1,
"changePassword": 0, changePassword: 0,
"resetPassword": 0, resetPassword: 0,
"createInvite": 1, createInvite: 1,
"deleteInvite": -1 deleteInvite: -1,
}; };
// window.lang doesn't exist at page load, so I made this a function that's invoked by activityList. // window.lang doesn't exist at page load, so I made this a function that's invoked by activityList.
const queries = (): { [field: string]: QueryType } => { return { const queries = (): { [field: string]: QueryType } => {
"id": { return {
name: window.lang.strings("activityID"), id: {
getter: "id", name: window.lang.strings("activityID"),
bool: false, getter: "id",
string: true, bool: false,
date: false string: true,
}, date: false,
"title": { },
name: window.lang.strings("title"), title: {
getter: "title", name: window.lang.strings("title"),
bool: false, getter: "title",
string: true, bool: false,
date: false, string: true,
localOnly: true date: false,
}, localOnly: true,
"user": { },
name: window.lang.strings("usersMentioned"), user: {
getter: "mentionedUsers", name: window.lang.strings("usersMentioned"),
bool: false, getter: "mentionedUsers",
string: true, bool: false,
date: false string: true,
}, date: false,
"actor": { },
name: window.lang.strings("actor"), actor: {
description: window.lang.strings("actorDescription"), name: window.lang.strings("actor"),
getter: "actor", description: window.lang.strings("actorDescription"),
bool: false, getter: "actor",
string: true, bool: false,
date: false string: true,
}, date: false,
"referrer": { },
name: window.lang.strings("referrer"), referrer: {
getter: "referrer", name: window.lang.strings("referrer"),
bool: true, getter: "referrer",
string: true, bool: true,
date: false string: true,
}, date: false,
"time": { },
name: window.lang.strings("date"), time: {
getter: "time", name: window.lang.strings("date"),
bool: false, getter: "time",
string: false, bool: false,
date: true string: false,
}, date: true,
"account-creation": { },
name: window.lang.strings("accountCreationFilter"), "account-creation": {
getter: "accountCreation", name: window.lang.strings("accountCreationFilter"),
bool: true, getter: "accountCreation",
string: false, bool: true,
date: false string: false,
}, date: false,
"account-deletion": { },
name: window.lang.strings("accountDeletionFilter"), "account-deletion": {
getter: "accountDeletion", name: window.lang.strings("accountDeletionFilter"),
bool: true, getter: "accountDeletion",
string: false, bool: true,
date: false string: false,
}, date: false,
"account-disabled": { },
name: window.lang.strings("accountDisabledFilter"), "account-disabled": {
getter: "accountDisabled", name: window.lang.strings("accountDisabledFilter"),
bool: true, getter: "accountDisabled",
string: false, bool: true,
date: false string: false,
}, date: false,
"account-enabled": { },
name: window.lang.strings("accountEnabledFilter"), "account-enabled": {
getter: "accountEnabled", name: window.lang.strings("accountEnabledFilter"),
bool: true, getter: "accountEnabled",
string: false, bool: true,
date: false string: false,
}, date: false,
"contact-linked": { },
name: window.lang.strings("contactLinkedFilter"), "contact-linked": {
getter: "contactLinked", name: window.lang.strings("contactLinkedFilter"),
bool: true, getter: "contactLinked",
string: false, bool: true,
date: false string: false,
}, date: false,
"contact-unlinked": { },
name: window.lang.strings("contactUnlinkedFilter"), "contact-unlinked": {
getter: "contactUnlinked", name: window.lang.strings("contactUnlinkedFilter"),
bool: true, getter: "contactUnlinked",
string: false, bool: true,
date: false string: false,
}, date: false,
"password-change": { },
name: window.lang.strings("passwordChangeFilter"), "password-change": {
getter: "passwordChange", name: window.lang.strings("passwordChangeFilter"),
bool: true, getter: "passwordChange",
string: false, bool: true,
date: false string: false,
}, date: false,
"password-reset": { },
name: window.lang.strings("passwordResetFilter"), "password-reset": {
getter: "passwordReset", name: window.lang.strings("passwordResetFilter"),
bool: true, getter: "passwordReset",
string: false, bool: true,
date: false string: false,
}, date: false,
"invite-created": { },
name: window.lang.strings("inviteCreatedFilter"), "invite-created": {
getter: "inviteCreated", name: window.lang.strings("inviteCreatedFilter"),
bool: true, getter: "inviteCreated",
string: false, bool: true,
date: false string: false,
}, date: false,
"invite-deleted": { },
name: window.lang.strings("inviteDeletedFilter"), "invite-deleted": {
getter: "inviteDeleted", name: window.lang.strings("inviteDeletedFilter"),
bool: true, getter: "inviteDeleted",
string: false, bool: true,
date: false string: false,
} date: false,
}}; },
};
};
// var moodColours = ["~warning", "~neutral", "~urge"]; // var moodColours = ["~warning", "~neutral", "~urge"];
@@ -173,37 +181,58 @@ export class Activity implements activity, SearchableItem {
_genUserText = (): string => { _genUserText = (): string => {
return `<span class="font-medium">${this._act.username || this._act.user_id.substring(0, 5)}</span>`; return `<span class="font-medium">${this._act.username || this._act.user_id.substring(0, 5)}</span>`;
} };
_genSrcUserText = (): string => { _genSrcUserText = (): string => {
return `<span class="font-medium">${this._act.source_username || this._act.source.substring(0, 5)}</span>`; return `<span class="font-medium">${this._act.source_username || this._act.source.substring(0, 5)}</span>`;
} };
_genUserLink = (): string => { _genUserLink = (): string => {
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.pages.Base}${window.pages.Admin}/accounts?user=${this._act.user_id}">${this._genUserText()}</a>`; return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.pages.Base}${window.pages.Admin}/accounts?user=${this._act.user_id}">${this._genUserText()}</a>`;
} };
_genSrcUserLink = (): string => { _genSrcUserLink = (): string => {
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.pages.Base}${window.pages.Admin}/accounts?user=${this._act.source}">${this._genSrcUserText()}</a>`; return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.pages.Base}${window.pages.Admin}/accounts?user=${this._act.source}">${this._genSrcUserText()}</a>`;
} };
private _renderInvText = (): string => { return `<span class="font-medium font-mono">${this.value || this.invite_code || "???"}</span>`; } private _renderInvText = (): string => {
return `<span class="font-medium font-mono">${this.value || this.invite_code || "???"}</span>`;
};
private _genInvLink = (): string => { private _genInvLink = (): string => {
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-invite" data-id="${this.invite_code}" href="${window.pages.Base}${window.pages.Admin}/?invite=${this.invite_code}">${this._renderInvText()}</a>`; return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-invite" data-id="${this.invite_code}" href="${window.pages.Base}${window.pages.Admin}/?invite=${this.invite_code}">${this._renderInvText()}</a>`;
};
get accountCreation(): boolean {
return this.type == "creation";
}
get accountDeletion(): boolean {
return this.type == "deletion";
}
get accountDisabled(): boolean {
return this.type == "disabled";
}
get accountEnabled(): boolean {
return this.type == "enabled";
}
get contactLinked(): boolean {
return this.type == "contactLinked";
}
get contactUnlinked(): boolean {
return this.type == "contactUnlinked";
}
get passwordChange(): boolean {
return this.type == "changePassword";
}
get passwordReset(): boolean {
return this.type == "resetPassword";
}
get inviteCreated(): boolean {
return this.type == "createInvite";
}
get inviteDeleted(): boolean {
return this.type == "deleteInvite";
} }
get accountCreation(): boolean { return this.type == "creation"; }
get accountDeletion(): boolean { return this.type == "deletion"; }
get accountDisabled(): boolean { return this.type == "disabled"; }
get accountEnabled(): boolean { return this.type == "enabled"; }
get contactLinked(): boolean { return this.type == "contactLinked"; }
get contactUnlinked(): boolean { return this.type == "contactUnlinked"; }
get passwordChange(): boolean { return this.type == "changePassword"; }
get passwordReset(): boolean { return this.type == "resetPassword"; }
get inviteCreated(): boolean { return this.type == "createInvite"; }
get inviteDeleted(): boolean { return this.type == "deleteInvite"; }
get mentionedUsers(): string { get mentionedUsers(): string {
return (this.username + " " + this.source_username).toLowerCase(); return (this.username + " " + this.source_username).toLowerCase();
@@ -220,7 +249,9 @@ export class Activity implements activity, SearchableItem {
return this.source_username.toLowerCase(); return this.source_username.toLowerCase();
} }
get type(): string { return this._act.type; } get type(): string {
return this._act.type;
}
set type(v: string) { set type(v: string) {
this._act.type = v; this._act.type = v;
@@ -229,7 +260,7 @@ export class Activity implements activity, SearchableItem {
el.classList.remove("~warning"); el.classList.remove("~warning");
el.classList.remove("~neutral"); el.classList.remove("~neutral");
el.classList.remove("~urge"); el.classList.remove("~urge");
if (mood == -1) { if (mood == -1) {
el.classList.add("~warning"); el.classList.add("~warning");
} else if (mood == 0) { } else if (mood == 0) {
@@ -243,7 +274,7 @@ export class Activity implements activity, SearchableItem {
if (i-1 == mood) this._card.classList.add(moodColours[i]); if (i-1 == mood) this._card.classList.add(moodColours[i]);
else this._card.classList.remove(moodColours[i]); else this._card.classList.remove(moodColours[i]);
} */ } */
// lazy late addition, hide then unhide if needed // lazy late addition, hide then unhide if needed
this._expiryTypeBadge.classList.add("unfocused"); this._expiryTypeBadge.classList.add("unfocused");
if (this.type == "changePassword" || this.type == "resetPassword") { if (this.type == "changePassword" || this.type == "resetPassword") {
@@ -309,13 +340,17 @@ export class Activity implements activity, SearchableItem {
} }
} }
get time(): number { return this._timeUnix; } get time(): number {
return this._timeUnix;
}
set time(v: number) { set time(v: number) {
this._timeUnix = v; this._timeUnix = v;
this._time.textContent = toDateString(new Date(v*1000)); this._time.textContent = toDateString(new Date(v * 1000));
} }
get source_type(): string { return this._act.source_type; } get source_type(): string {
return this._act.source_type;
}
set source_type(v: string) { set source_type(v: string) {
this._act.source_type = v; this._act.source_type = v;
if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") { if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") {
@@ -329,7 +364,9 @@ export class Activity implements activity, SearchableItem {
} }
} }
get ip(): string { return this._act.ip; } get ip(): string {
return this._act.ip;
}
set ip(v: string) { set ip(v: string) {
this._act.ip = v; this._act.ip = v;
if (v) { if (v) {
@@ -341,42 +378,68 @@ export class Activity implements activity, SearchableItem {
} }
} }
get invite_code(): string { return this._act.invite_code; } get invite_code(): string {
return this._act.invite_code;
}
set invite_code(v: string) { set invite_code(v: string) {
this._act.invite_code = v; this._act.invite_code = v;
} }
get value(): string { return this._act.value; } get value(): string {
return this._act.value;
}
set value(v: string) { set value(v: string) {
this._act.value = v; this._act.value = v;
} }
get source(): string { return this._act.source; } get source(): string {
return this._act.source;
}
set source(v: string) { set source(v: string) {
this._act.source = v; this._act.source = v;
if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") { if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") {
this._source.innerHTML = this._genInvLink(); this._source.innerHTML = this._genInvLink();
} else if ((this.source_type == "admin" || this.source_type == "user") && this._act.source != "" && this._act.source_username != "") { } else if (
(this.source_type == "admin" || this.source_type == "user") &&
this._act.source != "" &&
this._act.source_username != ""
) {
this._source.innerHTML = this._genSrcUserLink(); this._source.innerHTML = this._genSrcUserLink();
} }
} }
get id(): string { return this._act.id; } get id(): string {
return this._act.id;
}
set id(v: string) { set id(v: string) {
this._act.id = v; this._act.id = v;
this._card.setAttribute(SearchableItemDataAttribute, v); this._card.setAttribute(SearchableItemDataAttribute, v);
} }
get user_id(): string { return this._act.user_id; } get user_id(): string {
set user_id(v: string) { this._act.user_id = v; } return this._act.user_id;
}
set user_id(v: string) {
this._act.user_id = v;
}
get username(): string { return this._act.username; } get username(): string {
set username(v: string) { this._act.username = v; } return this._act.username;
}
set username(v: string) {
this._act.username = v;
}
get source_username(): string { return this._act.source_username; } get source_username(): string {
set source_username(v: string) { this._act.source_username = v; } return this._act.source_username;
}
set source_username(v: string) {
this._act.source_username = v;
}
get title(): string { return this._title.textContent; } get title(): string {
return this._title.textContent;
}
matchesSearch = (query: string): boolean => { matchesSearch = (query: string): boolean => {
// console.log(this.title, "matches", query, ":", this.title.includes(query)); // console.log(this.title, "matches", query, ":", this.title.includes(query));
@@ -385,7 +448,7 @@ export class Activity implements activity, SearchableItem {
this.username.toLowerCase().includes(query) || this.username.toLowerCase().includes(query) ||
this.source_username.toLowerCase().includes(query) this.source_username.toLowerCase().includes(query)
); );
} };
constructor(act: activity) { constructor(act: activity) {
this._card = document.createElement("div"); this._card = document.createElement("div");
@@ -431,8 +494,12 @@ export class Activity implements activity, SearchableItem {
this.update(act); this.update(act);
const pseudoUsers = this._card.getElementsByClassName("activity-pseudo-link-user") as HTMLCollectionOf<HTMLAnchorElement>; const pseudoUsers = this._card.getElementsByClassName(
const pseudoInvites = this._card.getElementsByClassName("activity-pseudo-link-invite") as HTMLCollectionOf<HTMLAnchorElement>; "activity-pseudo-link-user",
) as HTMLCollectionOf<HTMLAnchorElement>;
const pseudoInvites = this._card.getElementsByClassName(
"activity-pseudo-link-invite",
) as HTMLCollectionOf<HTMLAnchorElement>;
for (let i = 0; i < pseudoUsers.length; i++) { for (let i = 0; i < pseudoUsers.length; i++) {
/*const navigate = (event: Event) => { /*const navigate = (event: Event) => {
@@ -465,24 +532,27 @@ export class Activity implements activity, SearchableItem {
this.time = act.time; this.time = act.time;
this.source = act.source; this.source = act.source;
this.value = act.value; this.value = act.value;
this.type = act.type; this.type = act.type;
this.ip = act.ip; this.ip = act.ip;
} };
delete = () => _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => { delete = () =>
if (req.readyState != 4) return; _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => {
if (req.status == 200) { if (req.readyState != 4) return;
window.notifications.customSuccess("activityDeleted", window.lang.notif("activityDeleted")); if (req.status == 200) {
} window.notifications.customSuccess("activityDeleted", window.lang.notif("activityDeleted"));
document.dispatchEvent(activityReload); }
}); document.dispatchEvent(activityReload);
});
asElement = () => { return this._card; }; asElement = () => {
return this._card;
};
} }
interface ActivitiesReqDTO extends PaginatedReqDTO { interface ActivitiesReqDTO extends PaginatedReqDTO {
type: string[]; type: string[];
}; }
interface ActivitiesDTO extends paginatedDTO { interface ActivitiesDTO extends paginatedDTO {
activities: activity[]; activities: activity[];
@@ -493,15 +563,21 @@ export class activityList extends PaginatedList {
protected _sortDirection = document.getElementById("activity-sort-direction") as HTMLButtonElement; protected _sortDirection = document.getElementById("activity-sort-direction") as HTMLButtonElement;
protected _ascending: boolean; protected _ascending: boolean;
get activities(): { [id: string]: Activity } { return this._search.items as { [id: string]: Activity }; } get activities(): { [id: string]: Activity } {
return this._search.items as { [id: string]: Activity };
}
// set activities(v: { [id: string]: Activity }) { this._search.items = v as SearchableItems; } // set activities(v: { [id: string]: Activity }) { this._search.items = v as SearchableItems; }
constructor() { constructor() {
super({ super({
loader: document.getElementById("activity-loader"), loader: document.getElementById("activity-loader"),
loadMoreButtons: Array.from([document.getElementById("activity-load-more") as HTMLButtonElement]) as Array<HTMLButtonElement>, loadMoreButtons: Array.from([
loadAllButtons: Array.from(document.getElementsByClassName("activity-load-all")) as Array<HTMLButtonElement>, document.getElementById("activity-load-more") as HTMLButtonElement,
]) as Array<HTMLButtonElement>,
loadAllButtons: Array.from(
document.getElementsByClassName("activity-load-all"),
) as Array<HTMLButtonElement>,
refreshButton: document.getElementById("activity-refresh") as HTMLButtonElement, refreshButton: document.getElementById("activity-refresh") as HTMLButtonElement,
filterArea: document.getElementById("activity-filter-area"), filterArea: document.getElementById("activity-filter-area"),
searchOptionsHeader: document.getElementById("activity-search-options-header"), searchOptionsHeader: document.getElementById("activity-search-options-header"),
@@ -513,7 +589,7 @@ export class activityList extends PaginatedList {
maxItemsLoadedForSearch: 200, maxItemsLoadedForSearch: 200,
appendNewItems: (resp: paginatedDTO) => { appendNewItems: (resp: paginatedDTO) => {
let ordering: string[] = this._search.ordering; let ordering: string[] = this._search.ordering;
for (let act of ((resp as ActivitiesDTO).activities || [])) { for (let act of (resp as ActivitiesDTO).activities || []) {
this.activities[act.id] = new Activity(act); this.activities[act.id] = new Activity(act);
ordering.push(act.id); ordering.push(act.id);
} }
@@ -521,7 +597,7 @@ export class activityList extends PaginatedList {
}, },
replaceWithNewItems: (resp: paginatedDTO) => { replaceWithNewItems: (resp: paginatedDTO) => {
// FIXME: Implement updates to existing elements, rather than just wiping each time. // FIXME: Implement updates to existing elements, rather than just wiping each time.
// Remove existing items // Remove existing items
for (let id of Object.keys(this.activities)) { for (let id of Object.keys(this.activities)) {
delete this.activities[id]; delete this.activities[id];
@@ -538,10 +614,10 @@ export class activityList extends PaginatedList {
window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities")); window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities"));
return; return;
} }
} },
}); });
this._container = document.getElementById("activity-card-list") this._container = document.getElementById("activity-card-list");
document.addEventListener("activity-reload", () => this.reload()); document.addEventListener("activity-reload", () => this.reload());
let searchConfig: SearchConfiguration = { let searchConfig: SearchConfiguration = {
@@ -561,25 +637,22 @@ export class activityList extends PaginatedList {
onSearchCallback: null, onSearchCallback: null,
searchServer: null, searchServer: null,
clearServerSearch: null, clearServerSearch: null,
} };
this.initSearch(searchConfig); this.initSearch(searchConfig);
this.ascending = this._c.defaultSortAscending; this.ascending = this._c.defaultSortAscending;
this._sortDirection.addEventListener("click", () => this.ascending = !this.ascending); this._sortDirection.addEventListener("click", () => (this.ascending = !this.ascending));
} }
reload = (callback?: (resp: paginatedDTO) => void) => { reload = (callback?: (resp: paginatedDTO) => void) => {
this._reload(callback); this._reload(callback);
} };
loadMore = (loadAll: boolean = false, callback?: () => void) => { loadMore = (loadAll: boolean = false, callback?: () => void) => {
this._loadMore( this._loadMore(loadAll, callback);
loadAll,
callback
);
}; };
loadAll = (callback?: (resp?: paginatedDTO) => void) => { loadAll = (callback?: (resp?: paginatedDTO) => void) => {
this._loadAll(callback); this._loadAll(callback);
}; };
@@ -616,5 +689,4 @@ export class activityList extends PaginatedList {
this._keepSearchingDescription.classList.add("unfocused"); this._keepSearchingDescription.classList.add("unfocused");
} }
};*/ };*/
} }

View File

@@ -13,9 +13,13 @@ export class Captcha {
reCAPTCHA = false; reCAPTCHA = false;
code = ""; code = "";
get value(): string { return this.input.value; } get value(): string {
return this.input.value;
}
hasChanged = (): boolean => { return this.value != this.previous; } hasChanged = (): boolean => {
return this.value != this.previous;
};
baseValidatorWrapper = (_baseValidator: (oncomplete: (valid: boolean) => void, captchaValid: boolean) => void) => { baseValidatorWrapper = (_baseValidator: (oncomplete: (valid: boolean) => void, captchaValid: boolean) => void) => {
return (oncomplete: (valid: boolean) => void): void => { return (oncomplete: (valid: boolean) => void): void => {
@@ -30,35 +34,47 @@ export class Captcha {
}; };
}; };
verify = (callback: () => void) => _post("/captcha/verify/" + this.code + "/" + this.captchaID + "/" + this.input.value + (this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => { verify = (callback: () => void) =>
if (req.readyState == 4) { _post(
if (req.status == 204) { "/captcha/verify/" +
this.checkbox.innerHTML = `<i class="ri-check-line"></i>`; this.code +
this.checkbox.classList.add("~positive"); "/" +
this.checkbox.classList.remove("~critical"); this.captchaID +
this.verified = true; "/" +
} else { this.input.value +
this.checkbox.innerHTML = `<i class="ri-close-line"></i>`; (this.isPWR ? "?pwr=true" : ""),
this.checkbox.classList.add("~critical"); null,
this.checkbox.classList.remove("~positive"); (req: XMLHttpRequest) => {
this.verified = false; if (req.readyState == 4) {
} if (req.status == 204) {
callback(); this.checkbox.innerHTML = `<i class="ri-check-line"></i>`;
} this.checkbox.classList.add("~positive");
}); this.checkbox.classList.remove("~critical");
this.verified = true;
generate = () => _get("/captcha/gen/"+this.code+(this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => { } else {
if (req.readyState == 4) { this.checkbox.innerHTML = `<i class="ri-close-line"></i>`;
if (req.status == 200) { this.checkbox.classList.add("~critical");
this.captchaID = this.isPWR ? this.code : req.response["id"]; this.checkbox.classList.remove("~positive");
// the Math.random() appearance below is used for PWRs, since they don't have a unique captchaID. The parameter is ignored by the server, but tells the browser to reload the image. this.verified = false;
document.getElementById("captcha-img").innerHTML = ` }
callback();
}
},
);
generate = () =>
_get("/captcha/gen/" + this.code + (this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200) {
this.captchaID = this.isPWR ? this.code : req.response["id"];
// the Math.random() appearance below is used for PWRs, since they don't have a unique captchaID. The parameter is ignored by the server, but tells the browser to reload the image.
document.getElementById("captcha-img").innerHTML = `
<img class="w-full" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf(window.pages.Form))}/captcha/img/${this.code}/${this.isPWR ? Math.random() : this.captchaID}${this.isPWR ? "?pwr=true" : ""}"></img> <img class="w-full" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf(window.pages.Form))}/captcha/img/${this.code}/${this.isPWR ? Math.random() : this.captchaID}${this.isPWR ? "?pwr=true" : ""}"></img>
`; `;
this.input.value = ""; this.input.value = "";
}
} }
} });
});
constructor(code: string, enabled: boolean, reCAPTCHA: boolean, isPWR: boolean) { constructor(code: string, enabled: boolean, reCAPTCHA: boolean, isPWR: boolean) {
this.code = code; this.code = code;
@@ -69,15 +85,17 @@ export class Captcha {
} }
export interface GreCAPTCHA { export interface GreCAPTCHA {
render: (container: HTMLDivElement, parameters: { render: (
sitekey?: string, container: HTMLDivElement,
theme?: string, parameters: {
size?: string, sitekey?: string;
tabindex?: number, theme?: string;
"callback"?: () => void, size?: string;
"expired-callback"?: () => void, tabindex?: number;
"error-callback"?: () => void callback?: () => void;
}) => void; "expired-callback"?: () => void;
"error-callback"?: () => void;
},
) => void;
getResponse: (opt_widget_id?: HTMLDivElement) => string; getResponse: (opt_widget_id?: HTMLDivElement) => string;
} }

View File

@@ -1,6 +1,6 @@
declare var window: GlobalWindow; declare var window: GlobalWindow;
import dateParser from "any-date-parser"; import dateParser from "any-date-parser";
import { Temporal } from 'temporal-polyfill'; import { Temporal } from "temporal-polyfill";
export function toDateString(date: Date): string { export function toDateString(date: Date): string {
const locale = window.language || (window as any).navigator.userLanguage || window.navigator.language; const locale = window.language || (window as any).navigator.userLanguage || window.navigator.language;
@@ -9,7 +9,7 @@ export function toDateString(date: Date): string {
let args1 = {}; let args1 = {};
let args2: Intl.DateTimeFormatOptions = { let args2: Intl.DateTimeFormatOptions = {
hour: "2-digit", hour: "2-digit",
minute: "2-digit" minute: "2-digit",
}; };
if (t12 && t24) { if (t12 && t24) {
if (t12.checked) { if (t12.checked) {
@@ -29,9 +29,9 @@ export const parseDateString = (value: string): ParsedDate => {
// Used just to tell use what fields the user passed. // Used just to tell use what fields the user passed.
attempt: dateParser.attempt(value), attempt: dateParser.attempt(value),
// note Date.fromString is also provided by dateParser. // note Date.fromString is also provided by dateParser.
date: (Date as any).fromString(value) as Date date: (Date as any).fromString(value) as Date,
}; };
if (("invalid" in (out.date as any))) { if ("invalid" in (out.date as any)) {
out.invalid = true; out.invalid = true;
} else { } else {
// getTimezoneOffset returns UTC - Timezone, so invert it to get distance from UTC -to- timezone. // getTimezoneOffset returns UTC - Timezone, so invert it to get distance from UTC -to- timezone.
@@ -40,7 +40,7 @@ export const parseDateString = (value: string): ParsedDate => {
// Month in Date objects is 0-based, so make our parsed date that way too // Month in Date objects is 0-based, so make our parsed date that way too
if ("month" in out.attempt) out.attempt.month -= 1; if ("month" in out.attempt) out.attempt.month -= 1;
return out; return out;
} };
// DateCountdown sets the given el's textContent to the time till the given date (unixSeconds), updating // DateCountdown sets the given el's textContent to the time till the given date (unixSeconds), updating
// every minute. It returns the timeout, so it can be later removed with clearTimeout if desired. // every minute. It returns the timeout, so it can be later removed with clearTimeout if desired.
@@ -53,14 +53,14 @@ export function DateCountdown(el: HTMLElement, unixSeconds: number): ReturnType<
let diff = now.until(then).round({ let diff = now.until(then).round({
largestUnit: "years", largestUnit: "years",
smallestUnit: "minutes", smallestUnit: "minutes",
relativeTo: nowPlain relativeTo: nowPlain,
}); });
// FIXME: I'd really like this to be localized, but don't know of any nice solutions. // FIXME: I'd really like this to be localized, but don't know of any nice solutions.
const fields = [diff.years, diff.months, diff.days, diff.hours, diff.minutes]; const fields = [diff.years, diff.months, diff.days, diff.hours, diff.minutes];
const abbrevs = ["y", "mo", "d", "h", "m"]; const abbrevs = ["y", "mo", "d", "h", "m"];
for (let i = 0; i < fields.length; i++) { for (let i = 0; i < fields.length; i++) {
if (fields[i]) { if (fields[i]) {
out += ""+fields[i] + abbrevs[i] + " "; out += "" + fields[i] + abbrevs[i] + " ";
} }
} }
return out.slice(0, -1); return out.slice(0, -1);
@@ -72,13 +72,20 @@ export function DateCountdown(el: HTMLElement, unixSeconds: number): ReturnType<
return setTimeout(update, 60000); return setTimeout(update, 60000);
} }
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => { export const _get = (
url: string,
data: Object,
onreadystatechange: (req: XMLHttpRequest) => void,
noConnectionError: boolean = false,
): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
if (window.pages) { url = window.pages.Base + url; } if (window.pages) {
url = window.pages.Base + url;
}
req.open("GET", url, true); req.open("GET", url, true);
req.responseType = 'json'; req.responseType = "json";
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
req.onreadystatechange = () => { req.onreadystatechange = () => {
if (req.status == 0) { if (req.status == 0) {
if (!noConnectionError) window.notifications.connectionError(); if (!noConnectionError) window.notifications.connectionError();
@@ -93,11 +100,13 @@ export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHtt
export const _download = (url: string, fname: string): void => { export const _download = (url: string, fname: string): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
if (window.pages) { url = window.pages.Base + url; } if (window.pages) {
url = window.pages.Base + url;
}
req.open("GET", url, true); req.open("GET", url, true);
req.responseType = 'blob'; req.responseType = "blob";
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
req.onload = (e: Event) => { req.onload = (e: Event) => {
let link = document.createElement("a") as HTMLAnchorElement; let link = document.createElement("a") as HTMLAnchorElement;
link.href = URL.createObjectURL(req.response); link.href = URL.createObjectURL(req.response);
@@ -109,25 +118,38 @@ export const _download = (url: string, fname: string): void => {
export const _upload = (url: string, formData: FormData): void => { export const _upload = (url: string, formData: FormData): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
if (window.pages) { url = window.pages.Base + url; } if (window.pages) {
url = window.pages.Base + url;
}
req.open("POST", url, true); req.open("POST", url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
// req.setRequestHeader('Content-Type', 'multipart/form-data'); // req.setRequestHeader('Content-Type', 'multipart/form-data');
req.send(formData); req.send(formData);
}; };
export const _req = (method: string, url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => { export const _req = (
method: string,
url: string,
data: Object,
onreadystatechange: (req: XMLHttpRequest) => void,
response?: boolean,
statusHandler?: (req: XMLHttpRequest) => void,
noConnectionError: boolean = false,
): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
if (window.pages) { url = window.pages.Base + url; } if (window.pages) {
url = window.pages.Base + url;
}
req.open(method, url, true); req.open(method, url, true);
if (response) { if (response) {
req.responseType = 'json'; req.responseType = "json";
} }
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
req.onreadystatechange = () => { req.onreadystatechange = () => {
if (statusHandler) { statusHandler(req); } if (statusHandler) {
else if (req.status == 0) { statusHandler(req);
} else if (req.status == 0) {
if (!noConnectionError) window.notifications.connectionError(); if (!noConnectionError) window.notifications.connectionError();
return; return;
} else if (req.status == 401) { } else if (req.status == 401) {
@@ -138,18 +160,46 @@ export const _req = (method: string, url: string, data: Object, onreadystatechan
req.send(JSON.stringify(data)); req.send(JSON.stringify(data));
}; };
export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("POST", url, data, onreadystatechange, response, statusHandler, noConnectionError); export const _post = (
url: string,
data: Object,
onreadystatechange: (req: XMLHttpRequest) => void,
response?: boolean,
statusHandler?: (req: XMLHttpRequest) => void,
noConnectionError: boolean = false,
): void => _req("POST", url, data, onreadystatechange, response, statusHandler, noConnectionError);
export const _put = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("PUT", url, data, onreadystatechange, response, statusHandler, noConnectionError); export const _put = (
url: string,
data: Object,
onreadystatechange: (req: XMLHttpRequest) => void,
response?: boolean,
statusHandler?: (req: XMLHttpRequest) => void,
noConnectionError: boolean = false,
): void => _req("PUT", url, data, onreadystatechange, response, statusHandler, noConnectionError);
export const _patch = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("PATCH", url, data, onreadystatechange, response, statusHandler, noConnectionError); export const _patch = (
url: string,
data: Object,
onreadystatechange: (req: XMLHttpRequest) => void,
response?: boolean,
statusHandler?: (req: XMLHttpRequest) => void,
noConnectionError: boolean = false,
): void => _req("PATCH", url, data, onreadystatechange, response, statusHandler, noConnectionError);
export function _delete(url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void { export function _delete(
url: string,
data: Object,
onreadystatechange: (req: XMLHttpRequest) => void,
noConnectionError: boolean = false,
): void {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
if (window.pages) { url = window.pages.Base + url; } if (window.pages) {
url = window.pages.Base + url;
}
req.open("DELETE", url, true); req.open("DELETE", url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
req.onreadystatechange = () => { req.onreadystatechange = () => {
if (req.status == 0) { if (req.status == 0) {
if (!noConnectionError) window.notifications.connectionError(); if (!noConnectionError) window.notifications.connectionError();
@@ -162,8 +212,8 @@ export function _delete(url: string, data: Object, onreadystatechange: (req: XML
req.send(JSON.stringify(data)); req.send(JSON.stringify(data));
} }
export function toClipboard (str: string) { export function toClipboard(str: string) {
const el = document.createElement('textarea') as HTMLTextAreaElement; const el = document.createElement("textarea") as HTMLTextAreaElement;
el.value = str; el.value = str;
el.readOnly = true; el.readOnly = true;
el.style.position = "absolute"; el.style.position = "absolute";
@@ -193,45 +243,50 @@ export class notificationBox implements NotificationBox {
static baseClasses = ["aside", "flex", "flex-row", "justify-between", "gap-4"]; static baseClasses = ["aside", "flex", "flex-row", "justify-between", "gap-4"];
private _error = (message: string): HTMLElement => { private _error = (message: string): HTMLElement => {
const noti = document.createElement('aside'); const noti = document.createElement("aside");
noti.classList.add(...notificationBox.baseClasses, "~critical", "@low", "notification-error"); noti.classList.add(...notificationBox.baseClasses, "~critical", "@low", "notification-error");
let error = ""; let error = "";
if (window.lang) { if (window.lang) {
error = window.lang.strings("error") + ":" error = window.lang.strings("error") + ":";
} }
noti.innerHTML = `<div><strong>${error}</strong> ${message}</div>`; noti.innerHTML = `<div><strong>${error}</strong> ${message}</div>`;
const closeButton = document.createElement('span') as HTMLSpanElement; const closeButton = document.createElement("span") as HTMLSpanElement;
closeButton.classList.add("button", "~critical", "@low"); closeButton.classList.add("button", "~critical", "@low");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`; closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => this._close(noti); closeButton.onclick = () => this._close(noti);
noti.classList.add("animate-slide-in"); noti.classList.add("animate-slide-in");
noti.appendChild(closeButton); noti.appendChild(closeButton);
return noti; return noti;
} };
private _positive = (bold: string, message: string): HTMLElement => { private _positive = (bold: string, message: string): HTMLElement => {
const noti = document.createElement('aside'); const noti = document.createElement("aside");
noti.classList.add(...notificationBox.baseClasses, "~positive", "@low", "notification-positive"); noti.classList.add(...notificationBox.baseClasses, "~positive", "@low", "notification-positive");
noti.innerHTML = `<div><strong>${bold}</strong> ${message}</div>`; noti.innerHTML = `<div><strong>${bold}</strong> ${message}</div>`;
const closeButton = document.createElement('span') as HTMLSpanElement; const closeButton = document.createElement("span") as HTMLSpanElement;
closeButton.classList.add("button", "~positive", "@low"); closeButton.classList.add("button", "~positive", "@low");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`; closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => this._close(noti); closeButton.onclick = () => this._close(noti);
noti.classList.add("animate-slide-in"); noti.classList.add("animate-slide-in");
noti.appendChild(closeButton); noti.appendChild(closeButton);
return noti; return noti;
} };
private _close = (noti: HTMLElement) => { private _close = (noti: HTMLElement) => {
noti.classList.remove("animate-slide-in"); noti.classList.remove("animate-slide-in");
noti.classList.add("animate-slide-out"); noti.classList.add("animate-slide-out");
noti.addEventListener(window.animationEvent, () => { noti.addEventListener(
this._box.removeChild(noti); window.animationEvent,
}, false); () => {
} this._box.removeChild(noti);
},
false,
);
};
connectionError = () => {
connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); } this.customError("connectionError", window.lang.notif("errorConnection"));
};
customError = (type: string, message: string) => { customError = (type: string, message: string) => {
this._errorTypes[type] = this._errorTypes[type] || false; this._errorTypes[type] = this._errorTypes[type] || false;
@@ -245,9 +300,14 @@ export class notificationBox implements NotificationBox {
} }
this._box.appendChild(noti); this._box.appendChild(noti);
this._errorTypes[type] = true; this._errorTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._errorTypes[type] = false; } }, this.timeout*1000); setTimeout(() => {
} if (this._box.contains(noti)) {
this._close(noti);
this._errorTypes[type] = false;
}
}, this.timeout * 1000);
};
customPositive = (type: string, bold: string, message: string) => { customPositive = (type: string, bold: string, message: string) => {
this._positiveTypes[type] = this._positiveTypes[type] || false; this._positiveTypes[type] = this._positiveTypes[type] || false;
const noti = this._positive(bold, message); const noti = this._positive(bold, message);
@@ -260,10 +320,16 @@ export class notificationBox implements NotificationBox {
} }
this._box.appendChild(noti); this._box.appendChild(noti);
this._positiveTypes[type] = true; this._positiveTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._positiveTypes[type] = false; } }, this.timeout*1000); setTimeout(() => {
} if (this._box.contains(noti)) {
this._close(noti);
this._positiveTypes[type] = false;
}
}, this.timeout * 1000);
};
customSuccess = (type: string, message: string) => this.customPositive(type, window.lang.strings("success") + ":", message) customSuccess = (type: string, message: string) =>
this.customPositive(type, window.lang.strings("success") + ":", message);
} }
export const whichAnimationEvent = () => { export const whichAnimationEvent = () => {
@@ -272,19 +338,23 @@ export const whichAnimationEvent = () => {
return "animationend"; return "animationend";
} }
return "webkitAnimationEnd"; return "webkitAnimationEnd";
} };
export function toggleLoader(el: HTMLElement, small: boolean = true) { export function toggleLoader(el: HTMLElement, small: boolean = true) {
if (el.classList.contains("loader")) { if (el.classList.contains("loader")) {
el.classList.remove("loader"); el.classList.remove("loader");
el.classList.remove("loader-sm"); el.classList.remove("loader-sm");
const dot = el.querySelector("span.dot"); const dot = el.querySelector("span.dot");
if (dot) { dot.remove(); } if (dot) {
dot.remove();
}
} else { } else {
el.classList.add("loader"); el.classList.add("loader");
if (small) { el.classList.add("loader-sm"); } if (small) {
el.classList.add("loader-sm");
}
const dot = document.createElement("span") as HTMLSpanElement; const dot = document.createElement("span") as HTMLSpanElement;
dot.classList.add("dot") dot.classList.add("dot");
el.appendChild(dot); el.appendChild(dot);
} }
} }
@@ -293,9 +363,11 @@ export function addLoader(el: HTMLElement, small: boolean = true, relative: bool
if (el.classList.contains("loader")) return; if (el.classList.contains("loader")) return;
el.classList.add("loader"); el.classList.add("loader");
if (relative) el.classList.add("rel"); if (relative) el.classList.add("rel");
if (small) { el.classList.add("loader-sm"); } if (small) {
el.classList.add("loader-sm");
}
const dot = document.createElement("span") as HTMLSpanElement; const dot = document.createElement("span") as HTMLSpanElement;
dot.classList.add("dot") dot.classList.add("dot");
el.appendChild(dot); el.appendChild(dot);
} }
@@ -305,7 +377,9 @@ export function removeLoader(el: HTMLElement, small: boolean = true) {
el.classList.remove("loader-sm"); el.classList.remove("loader-sm");
el.classList.remove("rel"); el.classList.remove("rel");
const dot = el.querySelector("span.dot"); const dot = el.querySelector("span.dot");
if (dot) { dot.remove(); } if (dot) {
dot.remove();
}
} }
} }
@@ -329,7 +403,9 @@ export function insertText(textarea: HTMLTextAreaElement, text: string) {
} }
export function bindManualDropdowns() { export function bindManualDropdowns() {
const buttons = Array.from(document.getElementsByClassName("dropdown-manual-toggle") as HTMLCollectionOf<HTMLSpanElement>); const buttons = Array.from(
document.getElementsByClassName("dropdown-manual-toggle") as HTMLCollectionOf<HTMLSpanElement>,
);
for (let button of buttons) { for (let button of buttons) {
const parent = button.closest(".dropdown.manual"); const parent = button.closest(".dropdown.manual");
const display = parent.querySelector(".dropdown-display"); const display = parent.querySelector(".dropdown-display");
@@ -337,7 +413,7 @@ export function bindManualDropdowns() {
const mouseout = () => parent.classList.remove("selected"); const mouseout = () => parent.classList.remove("selected");
button.addEventListener("mouseover", mousein); button.addEventListener("mouseover", mousein);
button.addEventListener("mouseout", mouseout); button.addEventListener("mouseout", mouseout);
display.addEventListener("mouseover", mousein); display.addEventListener("mouseover", mousein);
display.addEventListener("mouseout", mouseout); display.addEventListener("mouseout", mouseout);
button.onclick = () => { button.onclick = () => {
parent.classList.add("selected"); parent.classList.add("selected");
@@ -346,7 +422,12 @@ export function bindManualDropdowns() {
display.removeEventListener("mouseout", mouseout); display.removeEventListener("mouseout", mouseout);
}; };
const outerClickListener = (event: Event) => { const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (display.contains(event.target) || button.contains(event.target)))) { if (
!(
event.target instanceof HTMLElement &&
(display.contains(event.target) || button.contains(event.target))
)
) {
parent.classList.remove("selected"); parent.classList.remove("selected");
document.removeEventListener("click", outerClickListener); document.removeEventListener("click", outerClickListener);
button.addEventListener("mouseout", mouseout); button.addEventListener("mouseout", mouseout);
@@ -372,20 +453,28 @@ export function unicodeB64Encode(s: string): string {
// Only allow running a function every n milliseconds. // Only allow running a function every n milliseconds.
// Source: Clément Prévost at https://stackoverflow.com/questions/27078285/simple-throttle-in-javascript // Source: Clément Prévost at https://stackoverflow.com/questions/27078285/simple-throttle-in-javascript
// function foo<T>(bar: T): T { // function foo<T>(bar: T): T {
export function throttle (callback: () => void, limitMilliseconds: number): () => void { export function throttle(callback: () => void, limitMilliseconds: number): () => void {
var waiting = false; // Initially, we're not waiting var waiting = false; // Initially, we're not waiting
return function () { // We return a throttled function return function () {
if (!waiting) { // If we're not waiting // We return a throttled function
callback.apply(this, arguments); // Execute users function if (!waiting) {
waiting = true; // Prevent future invocations // If we're not waiting
setTimeout(function () { // After a period of time callback.apply(this, arguments); // Execute users function
waiting = false; // And allow future invocations waiting = true; // Prevent future invocations
setTimeout(function () {
// After a period of time
waiting = false; // And allow future invocations
}, limitMilliseconds); }, limitMilliseconds);
} }
} };
} }
export function SetupCopyButton(button: HTMLButtonElement, text: string | (() => string), baseClass?: string, notif?: string) { export function SetupCopyButton(
button: HTMLButtonElement,
text: string | (() => string),
baseClass?: string,
notif?: string,
) {
if (!notif) notif = window.lang.strings("copied"); if (!notif) notif = window.lang.strings("copied");
if (!baseClass) baseClass = "~info"; if (!baseClass) baseClass = "~info";
// script will probably turn this into multiple // script will probably turn this into multiple
@@ -395,8 +484,8 @@ export function SetupCopyButton(button: HTMLButtonElement, text: string | (() =>
button.title = window.lang.strings("copy"); button.title = window.lang.strings("copy");
const icon = document.createElement("i"); const icon = document.createElement("i");
icon.classList.add("icon", "ri-file-copy-line"); icon.classList.add("icon", "ri-file-copy-line");
button.appendChild(icon) button.appendChild(icon);
button.onclick = () => { button.onclick = () => {
if (typeof text === "string") { if (typeof text === "string") {
toClipboard(text); toClipboard(text);
} else { } else {

View File

@@ -1,4 +1,4 @@
import {addLoader, removeLoader, _get} from "../modules/common.js"; import { addLoader, removeLoader, _get } from "../modules/common.js";
declare var window: GlobalWindow; declare var window: GlobalWindow;
@@ -12,7 +12,12 @@ var listeners: { [buttonText: string]: (event: CustomEvent) => void } = {};
export type DiscordSearch = (passData: string) => void; export type DiscordSearch = (passData: string) => void;
export function newDiscordSearch(title: string, description: string, buttonText: string, buttonFunction: (user: DiscordUser, passData: string) => void): DiscordSearch { export function newDiscordSearch(
title: string,
description: string,
buttonText: string,
buttonFunction: (user: DiscordUser, passData: string) => void,
): DiscordSearch {
if (!window.discordEnabled) { if (!window.discordEnabled) {
return () => {}; return () => {};
} }
@@ -62,7 +67,7 @@ export function newDiscordSearch(title: string, description: string, buttonText:
} }
}); });
}, 750); }, 750);
} };
return (passData: string) => { return (passData: string) => {
const input = document.getElementById("discord-search") as HTMLInputElement; const input = document.getElementById("discord-search") as HTMLInputElement;
@@ -79,5 +84,5 @@ export function newDiscordSearch(title: string, description: string, buttonText:
input.addEventListener("keyup", listeners[buttonText].bind(null, { detail: passData })); input.addEventListener("keyup", listeners[buttonText].bind(null, { detail: passData }));
window.modals.discord.show(); window.modals.discord.show();
} };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,37 +24,45 @@ export class lang implements Lang {
} }
get = (sect: string, key: string): string => { get = (sect: string, key: string): string => {
if (sect == "quantityStrings" || sect == "meta") { return ""; } if (sect == "quantityStrings" || sect == "meta") {
return "";
}
return this._lang[sect][key]; return this._lang[sect][key];
} };
strings = (key: string): string => this.get("strings", key) strings = (key: string): string => this.get("strings", key);
notif = (key: string): string => this.get("notifications", key) notif = (key: string): string => this.get("notifications", key);
var = (sect: string, key: string, ...subs: string[]): string => { var = (sect: string, key: string, ...subs: string[]): string => {
if (sect == "quantityStrings" || sect == "meta") { return ""; } if (sect == "quantityStrings" || sect == "meta") {
return "";
}
let str = this._lang[sect][key]; let str = this._lang[sect][key];
for (let sub of subs) { for (let sub of subs) {
str = str.replace("{n}", sub); str = str.replace("{n}", sub);
} }
return str; return str;
} };
template = (sect: string, key: string, subs: { [key: string]: any }): string => { template = (sect: string, key: string, subs: { [key: string]: any }): string => {
if (sect == "quantityStrings" || sect == "meta") { return ""; } if (sect == "quantityStrings" || sect == "meta") {
return "";
}
const map = new Map<string, any>(); const map = new Map<string, any>();
for (let key of Object.keys(subs)) { map.set(key, subs[key]); } for (let key of Object.keys(subs)) {
map.set(key, subs[key]);
}
const [out, err] = Template(this._lang[sect][key], map); const [out, err] = Template(this._lang[sect][key], map);
if (err != null) throw err; if (err != null) throw err;
return out; return out;
} };
quantity = (key: string, number: number): string => { quantity = (key: string, number: number): string => {
if (number == 1) { if (number == 1) {
return this._lang.quantityStrings[key].singular.replace("{n}", ""+number) return this._lang.quantityStrings[key].singular.replace("{n}", "" + number);
} }
return this._lang.quantityStrings[key].plural.replace("{n}", ""+number); return this._lang.quantityStrings[key].plural.replace("{n}", "" + number);
} };
} }
export var TimeFmtChange = new CustomEvent("timefmt-change"); export var TimeFmtChange = new CustomEvent("timefmt-change");
@@ -66,7 +74,7 @@ export const loadLangSelector = (page: string) => {
localStorage.setItem("timefmt", fmt); localStorage.setItem("timefmt", fmt);
}; };
const t12 = document.getElementById("lang-12h") as HTMLInputElement; const t12 = document.getElementById("lang-12h") as HTMLInputElement;
if (typeof(t12) !== "undefined" && t12 != null) { if (typeof t12 !== "undefined" && t12 != null) {
t12.onchange = () => setTimefmt("12h"); t12.onchange = () => setTimefmt("12h");
const t24 = document.getElementById("lang-24h") as HTMLInputElement; const t24 = document.getElementById("lang-24h") as HTMLInputElement;
t24.onchange = () => setTimefmt("24h"); t24.onchange = () => setTimefmt("24h");
@@ -83,21 +91,26 @@ export const loadLangSelector = (page: string) => {
} }
let queryString = new URLSearchParams(window.location.search); let queryString = new URLSearchParams(window.location.search);
if (queryString.has("lang")) queryString.delete("lang"); if (queryString.has("lang")) queryString.delete("lang");
_get("/lang/" + page, null, (req: XMLHttpRequest) => { _get(
if (req.readyState == 4) { "/lang/" + page,
if (req.status != 200) { null,
document.getElementById("lang-dropdown").remove(); (req: XMLHttpRequest) => {
return; if (req.readyState == 4) {
if (req.status != 200) {
document.getElementById("lang-dropdown").remove();
return;
}
const list = document.getElementById("lang-list") as HTMLDivElement;
let innerHTML = "";
for (let code in req.response) {
if (!code || !req.response[code]) continue;
queryString.set("lang", code);
innerHTML += `<a href="?${queryString.toString()}" class="button w-full text-left justify-start ~neutral lang-link">${req.response[code]}</a>`;
queryString.delete("lang");
}
list.innerHTML = innerHTML;
} }
const list = document.getElementById("lang-list") as HTMLDivElement; },
let innerHTML = ''; true,
for (let code in req.response) { );
if (!code || !(req.response[code])) continue;
queryString.set("lang", code);
innerHTML += `<a href="?${queryString.toString()}" class="button w-full text-left justify-start ~neutral lang-link">${req.response[code]}</a>`;
queryString.delete("lang");
}
list.innerHTML = innerHTML;
}
}, true);
}; };

View File

@@ -6,7 +6,7 @@ declare var window: GlobalWindow;
export interface ListItem { export interface ListItem {
asElement: () => HTMLElement; asElement: () => HTMLElement;
}; }
export class RecordCounter { export class RecordCounter {
private _container: HTMLElement; private _container: HTMLElement;
@@ -50,25 +50,33 @@ export class RecordCounter {
}); });
} }
get total(): number { return this._total; } get total(): number {
return this._total;
}
set total(v: number) { set total(v: number) {
this._total = v; this._total = v;
this._totalRecords.textContent = window.lang.var("strings", "totalRecords", `${v}`); this._totalRecords.textContent = window.lang.var("strings", "totalRecords", `${v}`);
} }
get loaded(): number { return this._loaded; } get loaded(): number {
return this._loaded;
}
set loaded(v: number) { set loaded(v: number) {
this._loaded = v; this._loaded = v;
this._loadedRecords.textContent = window.lang.var("strings", "loadedRecords", `${v}`); this._loadedRecords.textContent = window.lang.var("strings", "loadedRecords", `${v}`);
} }
get shown(): number { return this._shown; } get shown(): number {
return this._shown;
}
set shown(v: number) { set shown(v: number) {
this._shown = v; this._shown = v;
this._shownRecords.textContent = window.lang.var("strings", "shownRecords", `${v}`); this._shownRecords.textContent = window.lang.var("strings", "shownRecords", `${v}`);
} }
get selected(): number { return this._selected; } get selected(): number {
return this._selected;
}
set selected(v: number) { set selected(v: number) {
this._selected = v; this._selected = v;
if (v == 0) this._selectedRecords.textContent = ``; if (v == 0) this._selectedRecords.textContent = ``;
@@ -98,7 +106,7 @@ export interface PaginatedListConfig {
export abstract class PaginatedList { export abstract class PaginatedList {
protected _c: PaginatedListConfig; protected _c: PaginatedListConfig;
// Container to append items to. // Container to append items to.
protected _container: HTMLElement; protected _container: HTMLElement;
// List of visible IDs (i.e. those set with setVisibility). // List of visible IDs (i.e. those set with setVisibility).
@@ -119,14 +127,16 @@ export abstract class PaginatedList {
}; };
protected _search: Search; protected _search: Search;
protected _counter: RecordCounter; protected _counter: RecordCounter;
protected _hasLoaded: boolean; protected _hasLoaded: boolean;
protected _lastLoad: number; protected _lastLoad: number;
protected _page: number = 0; protected _page: number = 0;
protected _lastPage: boolean; protected _lastPage: boolean;
get lastPage(): boolean { return this._lastPage }; get lastPage(): boolean {
return this._lastPage;
}
set lastPage(v: boolean) { set lastPage(v: boolean) {
this._lastPage = v; this._lastPage = v;
if (v) { if (v) {
@@ -156,15 +166,15 @@ export abstract class PaginatedList {
limit: 0, limit: 0,
page: 0, page: 0,
sortByField: "", sortByField: "",
ascending: false ascending: false,
}; };
} };
constructor(c: PaginatedListConfig) { constructor(c: PaginatedListConfig) {
this._c = c; this._c = c;
this._counter = new RecordCounter(this._c.recordCounter); this._counter = new RecordCounter(this._c.recordCounter);
this._hasLoaded = false; this._hasLoaded = false;
this._c.loadMoreButtons.forEach((v) => { this._c.loadMoreButtons.forEach((v) => {
v.onclick = () => this.loadMore(false); v.onclick = () => this.loadMore(false);
}); });
@@ -180,7 +190,10 @@ export abstract class PaginatedList {
} }
autoSetServerSearchButtonsDisabled = () => { autoSetServerSearchButtonsDisabled = () => {
const serverSearchSortChanged = this._search.inServerSearch && (this._searchParams.sortByField != this._search.sortField || this._searchParams.ascending != this._search.ascending); const serverSearchSortChanged =
this._search.inServerSearch &&
(this._searchParams.sortByField != this._search.sortField ||
this._searchParams.ascending != this._search.ascending);
if (this._search.inServerSearch) { if (this._search.inServerSearch) {
if (serverSearchSortChanged) { if (serverSearchSortChanged) {
this._search.setServerSearchButtonsDisabled(false); this._search.setServerSearchButtonsDisabled(false);
@@ -189,28 +202,42 @@ export abstract class PaginatedList {
} }
return; return;
} }
if (!this._search.inSearch && this._search.sortField == this._c.defaultSortField && this._search.ascending == this._c.defaultSortAscending) { if (
!this._search.inSearch &&
this._search.sortField == this._c.defaultSortField &&
this._search.ascending == this._c.defaultSortAscending
) {
this._search.setServerSearchButtonsDisabled(true); this._search.setServerSearchButtonsDisabled(true);
return; return;
} }
this._search.setServerSearchButtonsDisabled(false); this._search.setServerSearchButtonsDisabled(false);
} };
initSearch = (searchConfig: SearchConfiguration) => { initSearch = (searchConfig: SearchConfiguration) => {
const previousCallback = searchConfig.onSearchCallback; const previousCallback = searchConfig.onSearchCallback;
searchConfig.onSearchCallback = (newItems: boolean, loadAll: boolean, callback?: (resp: paginatedDTO) => void) => { searchConfig.onSearchCallback = (
newItems: boolean,
loadAll: boolean,
callback?: (resp: paginatedDTO) => void,
) => {
// if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused"); // if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused");
// else this._c.loadAllButton.classList.add("unfocused"); // else this._c.loadAllButton.classList.add("unfocused");
this.autoSetServerSearchButtonsDisabled(); this.autoSetServerSearchButtonsDisabled();
// FIXME: Figure out why this makes sense and make it clearer. // FIXME: Figure out why this makes sense and make it clearer.
if ((this._visible.length < this._c.itemsPerPage && this._counter.loaded < this._c.maxItemsLoadedForSearch && !this.lastPage) || loadAll) { if (
if (!newItems || (this._visible.length < this._c.itemsPerPage &&
this._counter.loaded < this._c.maxItemsLoadedForSearch &&
!this.lastPage) ||
loadAll
) {
if (
!newItems ||
this._previousVisibleItemCount != this._visible.length || this._previousVisibleItemCount != this._visible.length ||
(this._visible.length == 0 && !this.lastPage) || (this._visible.length == 0 && !this.lastPage) ||
loadAll loadAll
) { ) {
this.loadMore(loadAll, callback); this.loadMore(loadAll, callback);
} }
} }
@@ -229,7 +256,7 @@ export abstract class PaginatedList {
console.trace("Clearing server search"); console.trace("Clearing server search");
this._page = 0; this._page = 0;
this.reload(); this.reload();
} };
searchConfig.setVisibility = this.setVisibility; searchConfig.setVisibility = this.setVisibility;
this._search = new Search(searchConfig); this._search = new Search(searchConfig);
this._search.generateFilterList(); this._search.generateFilterList();
@@ -258,7 +285,7 @@ export abstract class PaginatedList {
setVisibility = (elements: string[], visible: boolean, appendedItems: boolean = false) => { setVisibility = (elements: string[], visible: boolean, appendedItems: boolean = false) => {
let timer = this._search.timeSearches ? performance.now() : null; let timer = this._search.timeSearches ? performance.now() : null;
if (visible) this._visible = elements; if (visible) this._visible = elements;
else this._visible = this._search.ordering.filter(v => !elements.includes(v)); else this._visible = this._search.ordering.filter((v) => !elements.includes(v));
// console.log(elements.length, visible, this._visible.length); // console.log(elements.length, visible, this._visible.length);
this._counter.shown = this._visible.length; this._counter.shown = this._visible.length;
if (this._visible.length == 0) { if (this._visible.length == 0) {
@@ -268,56 +295,56 @@ export abstract class PaginatedList {
if (!appendedItems) { if (!appendedItems) {
// Wipe old elements and render 1 new one, so we can take the element height. // Wipe old elements and render 1 new one, so we can take the element height.
this._container.replaceChildren(this._search.items[this._visible[0]].asElement()) this._container.replaceChildren(this._search.items[this._visible[0]].asElement());
} }
this._computeScrollInfo(); this._computeScrollInfo();
// Initial render of min(_visible.length, max(rowsOnPage*renderNExtraScreensWorth, itemsPerPage)), skipping 1 as we already did it. // Initial render of min(_visible.length, max(rowsOnPage*renderNExtraScreensWorth, itemsPerPage)), skipping 1 as we already did it.
this._scroll.initialRenderCount = Math.floor(Math.min( this._scroll.initialRenderCount = Math.floor(
this._visible.length, Math.min(
Math.max( this._visible.length,
((this._scroll.renderNExtraScreensWorth+1)*this._scroll.screenHeight)/this._scroll.rowHeight, Math.max(
this._c.itemsPerPage) ((this._scroll.renderNExtraScreensWorth + 1) * this._scroll.screenHeight) / this._scroll.rowHeight,
)); this._c.itemsPerPage,
),
),
);
let baseIndex = 1; let baseIndex = 1;
if (appendedItems) { if (appendedItems) {
baseIndex = this._scroll.rendered; baseIndex = this._scroll.rendered;
} }
const frag = document.createDocumentFragment() const frag = document.createDocumentFragment();
for (let i = baseIndex; i < this._scroll.initialRenderCount; i++) { for (let i = baseIndex; i < this._scroll.initialRenderCount; i++) {
frag.appendChild(this._search.items[this._visible[i]].asElement()) frag.appendChild(this._search.items[this._visible[i]].asElement());
} }
this._scroll.rendered = Math.max(baseIndex, this._scroll.initialRenderCount); this._scroll.rendered = Math.max(baseIndex, this._scroll.initialRenderCount);
// appendChild over replaceChildren because there's already elements on the DOM // appendChild over replaceChildren because there's already elements on the DOM
this._container.appendChild(frag); this._container.appendChild(frag);
if (this._search.timeSearches) { if (this._search.timeSearches) {
const totalTime = performance.now() - timer; const totalTime = performance.now() - timer;
console.debug(`setVisibility took ${totalTime}ms`); console.debug(`setVisibility took ${totalTime}ms`);
} }
} };
// Computes required scroll info, requiring one on-DOM item. Should be computed on page resize and this._visible change. // Computes required scroll info, requiring one on-DOM item. Should be computed on page resize and this._visible change.
_computeScrollInfo = () => { _computeScrollInfo = () => {
if (this._visible.length == 0) return; if (this._visible.length == 0) return;
this._scroll.screenHeight = Math.max( this._scroll.screenHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
document.documentElement.clientHeight,
window.innerHeight || 0
);
this._scroll.rowHeight = this._search.items[this._visible[0]].asElement().offsetHeight; this._scroll.rowHeight = this._search.items[this._visible[0]].asElement().offsetHeight;
} };
// returns the item index to render up to for the given scroll position. // returns the item index to render up to for the given scroll position.
// might return a value greater than this._visible.length, indicating a need for a page load. // might return a value greater than this._visible.length, indicating a need for a page load.
maximumItemsToRender = (scrollY: number): number => { maximumItemsToRender = (scrollY: number): number => {
const bottomScroll = scrollY + ((this._scroll.renderNExtraScreensWorth+1)*this._scroll.screenHeight); const bottomScroll = scrollY + (this._scroll.renderNExtraScreensWorth + 1) * this._scroll.screenHeight;
const bottomIdx = Math.floor(bottomScroll / this._scroll.rowHeight); const bottomIdx = Math.floor(bottomScroll / this._scroll.rowHeight);
return bottomIdx; return bottomIdx;
} };
private _load = ( private _load = (
itemLimit: number, itemLimit: number,
@@ -325,7 +352,7 @@ export abstract class PaginatedList {
appendFunc: (resp: paginatedDTO) => void, // Function to append/put items in storage. appendFunc: (resp: paginatedDTO) => void, // Function to append/put items in storage.
pre?: (resp: paginatedDTO) => void, pre?: (resp: paginatedDTO) => void,
post?: (resp: paginatedDTO) => void, post?: (resp: paginatedDTO) => void,
failCallback?: (req: XMLHttpRequest) => void failCallback?: (req: XMLHttpRequest) => void,
) => { ) => {
this._lastLoad = Date.now(); this._lastLoad = Date.now();
let params = this._search.inServerSearch ? this._searchParams : this.defaultParams(); let params = this._search.inServerSearch ? this._searchParams : this.defaultParams();
@@ -336,30 +363,35 @@ export abstract class PaginatedList {
params.ascending = this._c.defaultSortAscending; params.ascending = this._c.defaultSortAscending;
} }
_post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => { _post(
if (req.readyState != 4) return; this._c.getPageEndpoint,
if (req.status != 200) { params,
(req: XMLHttpRequest) => {
if (req.readyState != 4) return;
if (req.status != 200) {
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
if (failCallback) failCallback(req);
return;
}
this._hasLoaded = true;
let resp = req.response as paginatedDTO;
if (pre) pre(resp);
this.lastPage = resp.last_page;
appendFunc(resp);
this._counter.loaded = this._search.ordering.length;
if (post) post(resp);
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req); if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
if (failCallback) failCallback(req); },
return; true,
} );
this._hasLoaded = true; };
let resp = req.response as paginatedDTO;
if (pre) pre(resp);
this.lastPage = resp.last_page;
appendFunc(resp);
this._counter.loaded = this._search.ordering.length;
if (post) post(resp);
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
}, true);
}
// Removes all elements, and reloads the first page. // Removes all elements, and reloads the first page.
public abstract reload: (callback?: (resp: paginatedDTO) => void) => void; public abstract reload: (callback?: (resp: paginatedDTO) => void) => void;
protected _reload = (callback?: (resp: paginatedDTO) => void) => { protected _reload = (callback?: (resp: paginatedDTO) => void) => {
@@ -369,7 +401,7 @@ export abstract class PaginatedList {
// Reload all currently visible elements, i.e. Load a new page of size (limit*(page+1)). // Reload all currently visible elements, i.e. Load a new page of size (limit*(page+1)).
let limit = this._c.itemsPerPage; let limit = this._c.itemsPerPage;
if (this._page != 0) { if (this._page != 0) {
limit *= this._page+1; limit *= this._page + 1;
} }
this._load( this._load(
limit, limit,
@@ -378,7 +410,7 @@ export abstract class PaginatedList {
(_0: paginatedDTO) => { (_0: paginatedDTO) => {
// Allow refreshes every 15s // Allow refreshes every 15s
this._c.refreshButton.disabled = true; this._c.refreshButton.disabled = true;
setTimeout(() => this._c.refreshButton.disabled = false, 15000); setTimeout(() => (this._c.refreshButton.disabled = false), 15000);
}, },
(resp: paginatedDTO) => { (resp: paginatedDTO) => {
this._search.onSearchBoxChange(true, false, false); this._search.onSearchBoxChange(true, false, false);
@@ -392,14 +424,14 @@ export abstract class PaginatedList {
if (callback) callback(resp); if (callback) callback(resp);
}, },
); );
} };
// Loads the next page. If "loadAll", all pages will be loaded until the last is reached. // Loads the next page. If "loadAll", all pages will be loaded until the last is reached.
public abstract loadMore: (loadAll?: boolean, callback?: (resp?: paginatedDTO) => void) => void; public abstract loadMore: (loadAll?: boolean, callback?: (resp?: paginatedDTO) => void) => void;
protected _loadMore = (loadAll: boolean = false, callback?: (resp: paginatedDTO) => void) => { protected _loadMore = (loadAll: boolean = false, callback?: (resp: paginatedDTO) => void) => {
this._c.loadMoreButtons.forEach((v) => v.disabled = true); this._c.loadMoreButtons.forEach((v) => (v.disabled = true));
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
this._c.loadMoreButtons.forEach((v) => v.disabled = false); this._c.loadMoreButtons.forEach((v) => (v.disabled = false));
}, 1000); }, 1000);
this._page += 1; this._page += 1;
@@ -430,11 +462,13 @@ export abstract class PaginatedList {
if (callback) callback(resp); if (callback) callback(resp);
}, },
); );
} };
public abstract loadAll: (callback?: (resp?: paginatedDTO) => void) => void; public abstract loadAll: (callback?: (resp?: paginatedDTO) => void) => void;
protected _loadAll = (callback?: (resp?: paginatedDTO) => void) => { protected _loadAll = (callback?: (resp?: paginatedDTO) => void) => {
this._c.loadAllButtons.forEach((v) => { addLoader(v, true); }); this._c.loadAllButtons.forEach((v) => {
addLoader(v, true);
});
this.loadMore(true, callback); this.loadMore(true, callback);
}; };
@@ -442,17 +476,16 @@ export abstract class PaginatedList {
const cb = () => { const cb = () => {
if (this._counter.loaded > n) return; if (this._counter.loaded > n) return;
this.loadMore(false, cb); this.loadMore(false, cb);
} };
cb(); cb();
} };
// As reloading can disrupt long-scrolling, this function will only do it if you're at the top of the page, essentially. // As reloading can disrupt long-scrolling, this function will only do it if you're at the top of the page, essentially.
public reloadIfNotInScroll = () => { public reloadIfNotInScroll = () => {
if (this._visible.length == 0 || this.maximumItemsToRender(window.scrollY) < this._scroll.initialRenderCount) { if (this._visible.length == 0 || this.maximumItemsToRender(window.scrollY) < this._scroll.initialRenderCount) {
return this.reload(); return this.reload();
} }
} };
_detectScroll = () => { _detectScroll = () => {
if (!this._hasLoaded || this._scroll.scrollLoading || this._visible.length == 0) return; if (!this._hasLoaded || this._scroll.scrollLoading || this._visible.length == 0) return;
@@ -462,15 +495,15 @@ export abstract class PaginatedList {
// If you've scrolled back up, do nothing // If you've scrolled back up, do nothing
if (scrollSpeed < 0) return; if (scrollSpeed < 0) return;
let endIdx = this.maximumItemsToRender(scrollY); let endIdx = this.maximumItemsToRender(scrollY);
// Throttling this function means we might not catch up in time if the user scrolls fast, // Throttling this function means we might not catch up in time if the user scrolls fast,
// so we calculate the scroll speed (in rows/call) from the previous scrollY value. // so we calculate the scroll speed (in rows/call) from the previous scrollY value.
// This still might not be enough, so hackily we'll just scale it up. // This still might not be enough, so hackily we'll just scale it up.
// With onscrollend, this is less necessary, but with both I wasn't able to hit the bottom of the page on my mouse. // With onscrollend, this is less necessary, but with both I wasn't able to hit the bottom of the page on my mouse.
const rowsPerScroll = Math.round((scrollSpeed / this._scroll.rowHeight)); const rowsPerScroll = Math.round(scrollSpeed / this._scroll.rowHeight);
// Render extra pages depending on scroll speed // Render extra pages depending on scroll speed
endIdx += rowsPerScroll*2; endIdx += rowsPerScroll * 2;
const realEndIdx = Math.min(endIdx, this._visible.length); const realEndIdx = Math.min(endIdx, this._visible.length);
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
for (let i = this._scroll.rendered; i < realEndIdx; i++) { for (let i = this._scroll.rendered; i < realEndIdx; i++) {
@@ -495,7 +528,7 @@ export abstract class PaginatedList {
cb(); cb();
return; return;
} }
} };
detectScroll = throttle(this._detectScroll, 200); detectScroll = throttle(this._detectScroll, 200);
@@ -515,7 +548,5 @@ export abstract class PaginatedList {
window.removeEventListener("scroll", this.detectScroll); window.removeEventListener("scroll", this.detectScroll);
window.removeEventListener("scrollend", this.detectScroll); window.removeEventListener("scrollend", this.detectScroll);
window.removeEventListener("resize", this.redrawScroll); window.removeEventListener("resize", this.redrawScroll);
} };
} }

View File

@@ -17,7 +17,7 @@ export class Login {
constructor(modal: Modal, endpoint: string, appearance: string) { constructor(modal: Modal, endpoint: string, appearance: string) {
this._endpoint = endpoint; this._endpoint = endpoint;
this._url = window.pages.Base + endpoint; this._url = window.pages.Base + endpoint;
if (this._url[this._url.length-1] != '/') this._url += "/"; if (this._url[this._url.length - 1] != "/") this._url += "/";
this._modal = modal; this._modal = modal;
if (appearance == "opaque") { if (appearance == "opaque") {
@@ -45,29 +45,39 @@ export class Login {
this._logoutButton = button; this._logoutButton = button;
this._logoutButton.classList.add("unfocused"); this._logoutButton.classList.add("unfocused");
const logoutFunc = (url: string, tryAgain: boolean) => { const logoutFunc = (url: string, tryAgain: boolean) => {
_post(url + "logout", null, (req: XMLHttpRequest): boolean => { _post(
if (req.readyState == 4 && req.status == 200) { url + "logout",
window.token = ""; null,
location.reload(); (req: XMLHttpRequest): boolean => {
return false; if (req.readyState == 4 && req.status == 200) {
} window.token = "";
}, false, (req: XMLHttpRequest) => { location.reload();
if (req.readyState == 4 && req.status == 404 && tryAgain) { return false;
console.warn("logout failed, trying without URL Base..."); }
logoutFunc(this._endpoint, false); },
} false,
}); (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 404 && tryAgain) {
console.warn("logout failed, trying without URL Base...");
logoutFunc(this._endpoint, false);
}
},
);
}; };
this._logoutButton.onclick = () => logoutFunc(this._url, true); this._logoutButton.onclick = () => logoutFunc(this._url, true);
}; };
get onLogin() { return this._onLogin; } get onLogin() {
set onLogin(f: (username: string, password: string) => void) { this._onLogin = f; } return this._onLogin;
}
set onLogin(f: (username: string, password: string) => void) {
this._onLogin = f;
}
login = (username: string, password: string, run?: (state?: number) => void) => { login = (username: string, password: string, run?: (state?: number) => void) => {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.responseType = 'json'; req.responseType = "json";
const refresh = (username == "" && password == ""); const refresh = username == "" && password == "";
req.open("GET", this._url + (refresh ? "token/refresh" : "token/login"), true); req.open("GET", this._url + (refresh ? "token/refresh" : "token/login"), true);
if (!refresh) { if (!refresh) {
req.setRequestHeader("Authorization", "Basic " + unicodeB64Encode(username + ":" + password)); req.setRequestHeader("Authorization", "Basic " + unicodeB64Encode(username + ":" + password));
@@ -100,13 +110,13 @@ export class Login {
} }
if (this._hasOpacityWall) this._wall.remove(); if (this._hasOpacityWall) this._wall.remove();
this._modal.close(); this._modal.close();
if (this._logoutButton != null) if (this._logoutButton != null) this._logoutButton.classList.remove("unfocused");
this._logoutButton.classList.remove("unfocused"); }
if (run) {
run(+req.status);
} }
if (run) { run(+req.status); }
} }
}).bind(this, req); }).bind(this, req);
req.send(); req.send();
}; };
} }

View File

@@ -9,14 +9,16 @@ export class Modal implements Modal {
this.modal = modal; this.modal = modal;
this.openEvent = new CustomEvent("modal-open-" + modal.id); this.openEvent = new CustomEvent("modal-open-" + modal.id);
this.closeEvent = new CustomEvent("modal-close-" + modal.id); this.closeEvent = new CustomEvent("modal-close-" + modal.id);
const closeButton = this.modal.querySelector('span.modal-close'); const closeButton = this.modal.querySelector("span.modal-close");
if (closeButton !== null) { if (closeButton !== null) {
this.closeButton = closeButton as HTMLSpanElement; this.closeButton = closeButton as HTMLSpanElement;
this.closeButton.onclick = this.close; this.closeButton.onclick = this.close;
} }
if (!important) { if (!important) {
window.addEventListener('click', (event: Event) => { window.addEventListener("click", (event: Event) => {
if (event.target == this.modal) { this.close(); } if (event.target == this.modal) {
this.close();
}
}); });
} }
} }
@@ -26,36 +28,38 @@ export class Modal implements Modal {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
this.modal.classList.add('animate-fade-out'); this.modal.classList.add("animate-fade-out");
this.modal.classList.remove("animate-fade-in"); this.modal.classList.remove("animate-fade-in");
const modal = this.modal; const modal = this.modal;
const listenerFunc = () => { const listenerFunc = () => {
modal.classList.remove('block'); modal.classList.remove("block");
modal.classList.remove('animate-fade-out'); modal.classList.remove("animate-fade-out");
modal.removeEventListener(window.animationEvent, listenerFunc) modal.removeEventListener(window.animationEvent, listenerFunc);
if (!noDispatch) document.dispatchEvent(this.closeEvent); if (!noDispatch) document.dispatchEvent(this.closeEvent);
}; };
this.modal.addEventListener(window.animationEvent, listenerFunc, false); this.modal.addEventListener(window.animationEvent, listenerFunc, false);
} };
set onopen(f: () => void) { set onopen(f: () => void) {
document.addEventListener("modal-open-"+this.modal.id, f); document.addEventListener("modal-open-" + this.modal.id, f);
} }
set onclose(f: () => void) { set onclose(f: () => void) {
document.addEventListener("modal-close-"+this.modal.id, f); document.addEventListener("modal-close-" + this.modal.id, f);
} }
show = () => { show = () => {
this.modal.classList.add('block', 'animate-fade-in'); this.modal.classList.add("block", "animate-fade-in");
document.dispatchEvent(this.openEvent); document.dispatchEvent(this.openEvent);
} };
toggle = () => { toggle = () => {
if (this.modal.classList.contains('animate-fade-in')) { if (this.modal.classList.contains("animate-fade-in")) {
this.close(); this.close();
} else { } else {
this.show(); this.show();
} }
} };
asElement = () => { return this.modal; } asElement = () => {
return this.modal;
};
} }

View File

@@ -6,7 +6,7 @@ export interface Page {
hide: () => boolean; hide: () => boolean;
shouldSkip: () => boolean; shouldSkip: () => boolean;
index?: number; index?: number;
}; }
export interface PageConfig { export interface PageConfig {
hideOthersOnPageShow: boolean; hideOthersOnPageShow: boolean;
@@ -29,7 +29,7 @@ export class PageManager {
let ev = { state: data as string } as PopStateEvent; let ev = { state: data as string } as PopStateEvent;
window.onpopstate(ev); window.onpopstate(ev);
}; };
} };
private _onpopstate = (event: PopStateEvent) => { private _onpopstate = (event: PopStateEvent) => {
let name = event.state; let name = event.state;
@@ -42,13 +42,13 @@ export class PageManager {
} }
} }
if (!this.pages.has(name)) { if (!this.pages.has(name)) {
name = this.pageList[0] name = this.pageList[0];
} }
let success = this.pages.get(name).show(); let success = this.pages.get(name).show();
if (!success) { if (!success) {
return; return;
} }
if (!(this.hideOthers)) { if (!this.hideOthers) {
return; return;
} }
for (let k of this.pageList) { for (let k of this.pageList) {
@@ -56,15 +56,15 @@ export class PageManager {
this.pages.get(k).hide(); this.pages.get(k).hide();
} }
} }
} };
constructor(c: PageConfig) { constructor(c: PageConfig) {
this.pages = new Map<string, Page>; this.pages = new Map<string, Page>();
this.pageList = []; this.pageList = [];
this.hideOthers = c.hideOthersOnPageShow; this.hideOthers = c.hideOthersOnPageShow;
this.defaultName = c.defaultName; this.defaultName = c.defaultName;
this.defaultTitle = c.defaultTitle; this.defaultTitle = c.defaultTitle;
this._overridePushState(); this._overridePushState();
window.onpopstate = this._onpopstate; window.onpopstate = this._onpopstate;
} }
@@ -77,12 +77,12 @@ export class PageManager {
load(name: string = "") { load(name: string = "") {
name = decodeURI(name); name = decodeURI(name);
if (!this.pages.has(name)) return window.history.pushState(name || this.defaultName, this.defaultTitle, "") if (!this.pages.has(name)) return window.history.pushState(name || this.defaultName, this.defaultTitle, "");
const p = this.pages.get(name); const p = this.pages.get(name);
this.loadPage(p); this.loadPage(p);
} }
loadPage (p: Page) { loadPage(p: Page) {
let url = p.url; let url = p.url;
// Fix ordering of query params and hash // Fix ordering of query params and hash
if (url.includes("#")) { if (url.includes("#")) {
@@ -99,20 +99,20 @@ export class PageManager {
let p = this.pages.get(name); let p = this.pages.get(name);
let shouldSkip = true; let shouldSkip = true;
while (shouldSkip && p.index > 0) { while (shouldSkip && p.index > 0) {
p = this.pages.get(this.pageList[p.index-1]); p = this.pages.get(this.pageList[p.index - 1]);
shouldSkip = p.shouldSkip(); shouldSkip = p.shouldSkip();
} }
this.loadPage(p); this.loadPage(p);
} }
next(name: string = "") { next(name: string = "") {
if (!this.pages.has(name)) return console.error(`previous page ${name} not found`); if (!this.pages.has(name)) return console.error(`previous page ${name} not found`);
let p = this.pages.get(name); let p = this.pages.get(name);
let shouldSkip = true; let shouldSkip = true;
while (shouldSkip && p.index < this.pageList.length) { while (shouldSkip && p.index < this.pageList.length) {
p = this.pages.get(this.pageList[p.index+1]); p = this.pages.get(this.pageList[p.index + 1]);
shouldSkip = p.shouldSkip(); shouldSkip = p.shouldSkip();
} }
this.loadPage(p); this.loadPage(p);
} }
}; }

View File

@@ -1,24 +1,23 @@
import { _get, _post, _delete, toggleLoader, _put } from "../modules/common.js"; import { _get, _post, _delete, toggleLoader, _put } from "../modules/common.js";
import hljs from "highlight.js/lib/core"; import hljs from "highlight.js/lib/core";
import json from 'highlight.js/lib/languages/json'; import json from "highlight.js/lib/languages/json";
import codeInput, { CodeInput } from "@webcoder49/code-input/code-input.mjs"; import codeInput, { CodeInput } from "@webcoder49/code-input/code-input.mjs";
import Template from "@webcoder49/code-input/templates/hljs.mjs"; import Template from "@webcoder49/code-input/templates/hljs.mjs";
import Indent from "@webcoder49/code-input/plugins/indent.mjs"; import Indent from "@webcoder49/code-input/plugins/indent.mjs";
hljs.registerLanguage("json", json); hljs.registerLanguage("json", json);
codeInput.registerTemplate("json-highlighted", codeInput.registerTemplate("json-highlighted", new Template(hljs, [new Indent()]));
new Template(hljs, [new Indent()])
);
declare var window: GlobalWindow; declare var window: GlobalWindow;
export const profileLoadEvent = new CustomEvent("profileLoadEvent"); export const profileLoadEvent = new CustomEvent("profileLoadEvent");
export const reloadProfileNames = (then?: () => void) => _get("/profiles/names", null, (req: XMLHttpRequest) => { export const reloadProfileNames = (then?: () => void) =>
if (req.readyState != 4) return; _get("/profiles/names", null, (req: XMLHttpRequest) => {
window.availableProfiles = req.response["profiles"]; if (req.readyState != 4) return;
document.dispatchEvent(profileLoadEvent); window.availableProfiles = req.response["profiles"];
if (then) then(); document.dispatchEvent(profileLoadEvent);
}); if (then) then();
});
interface Profile { interface Profile {
admin: boolean; admin: boolean;
@@ -44,10 +43,16 @@ class profile implements Profile {
private _referralsEnabled: boolean; private _referralsEnabled: boolean;
private _editButton: HTMLButtonElement; private _editButton: HTMLButtonElement;
get name(): string { return this._name.textContent; } get name(): string {
set name(v: string) { this._name.textContent = v; } return this._name.textContent;
}
set name(v: string) {
this._name.textContent = v;
}
get admin(): boolean { return this._adminChip.classList.contains("chip"); } get admin(): boolean {
return this._adminChip.classList.contains("chip");
}
set admin(state: boolean) { set admin(state: boolean) {
if (state) { if (state) {
this._adminChip.classList.remove("unfocused"); this._adminChip.classList.remove("unfocused");
@@ -60,10 +65,16 @@ class profile implements Profile {
} }
} }
get libraries(): string { return this._libraries.textContent; } get libraries(): string {
set libraries(v: string) { this._libraries.textContent = v; } return this._libraries.textContent;
}
set libraries(v: string) {
this._libraries.textContent = v;
}
get ombi(): boolean { return this._ombi; } get ombi(): boolean {
return this._ombi;
}
set ombi(v: boolean) { set ombi(v: boolean) {
if (!window.ombiEnabled) return; if (!window.ombiEnabled) return;
this._ombi = v; this._ombi = v;
@@ -77,8 +88,10 @@ class profile implements Profile {
this._ombiButton.classList.remove("~critical"); this._ombiButton.classList.remove("~critical");
} }
} }
get jellyseerr(): boolean { return this._jellyseerr; } get jellyseerr(): boolean {
return this._jellyseerr;
}
set jellyseerr(v: boolean) { set jellyseerr(v: boolean) {
if (!window.jellyseerrEnabled) return; if (!window.jellyseerrEnabled) return;
this._jellyseerr = v; this._jellyseerr = v;
@@ -93,10 +106,16 @@ class profile implements Profile {
} }
} }
get fromUser(): string { return this._fromUser.textContent; } get fromUser(): string {
set fromUser(v: string) { this._fromUser.textContent = v; } return this._fromUser.textContent;
}
get referrals_enabled(): boolean { return this._referralsEnabled; } set fromUser(v: string) {
this._fromUser.textContent = v;
}
get referrals_enabled(): boolean {
return this._referralsEnabled;
}
set referrals_enabled(v: boolean) { set referrals_enabled(v: boolean) {
if (!window.referralsEnabled) return; if (!window.referralsEnabled) return;
this._referralsEnabled = v; this._referralsEnabled = v;
@@ -111,8 +130,12 @@ class profile implements Profile {
} }
} }
get default(): boolean { return this._defaultRadio.checked; } get default(): boolean {
set default(v: boolean) { this._defaultRadio.checked = v; } return this._defaultRadio.checked;
}
set default(v: boolean) {
this._defaultRadio.checked = v;
}
constructor(name: string, p: Profile) { constructor(name: string, p: Profile) {
this._row = document.createElement("tr") as HTMLTableRowElement; this._row = document.createElement("tr") as HTMLTableRowElement;
@@ -120,13 +143,16 @@ class profile implements Profile {
<td><div class="flex flex-row items-baseline gap-2"><b class="profile-name"></b> <span class="profile-admin"></span></div></td> <td><div class="flex flex-row items-baseline gap-2"><b class="profile-name"></b> <span class="profile-admin"></span></div></td>
<td><input type="radio" name="profile-default"></td> <td><input type="radio" name="profile-default"></td>
`; `;
if (window.ombiEnabled) innerHTML += ` if (window.ombiEnabled)
innerHTML += `
<td><span class="button @low profile-ombi"></span></td> <td><span class="button @low profile-ombi"></span></td>
`; `;
if (window.jellyseerrEnabled) innerHTML += ` if (window.jellyseerrEnabled)
innerHTML += `
<td><span class="button @low profile-jellyseerr"></span></td> <td><span class="button @low profile-jellyseerr"></span></td>
`; `;
if (window.referralsEnabled) innerHTML += ` if (window.referralsEnabled)
innerHTML += `
<td><span class="button @low profile-referrals"></span></td> <td><span class="button @low profile-referrals"></span></td>
`; `;
innerHTML += ` innerHTML += `
@@ -139,8 +165,7 @@ class profile implements Profile {
this._name = this._row.querySelector("b.profile-name"); this._name = this._row.querySelector("b.profile-name");
this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement; this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement;
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement; this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
if (window.ombiEnabled) if (window.ombiEnabled) this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
if (window.jellyseerrEnabled) if (window.jellyseerrEnabled)
this._jellyseerrButton = this._row.querySelector("span.profile-jellyseerr") as HTMLSpanElement; this._jellyseerrButton = this._row.querySelector("span.profile-jellyseerr") as HTMLSpanElement;
if (window.referralsEnabled) if (window.referralsEnabled)
@@ -148,12 +173,13 @@ class profile implements Profile {
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement; this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
this._editButton = this._row.querySelector(".profile-edit") as HTMLButtonElement; this._editButton = this._row.querySelector(".profile-edit") as HTMLButtonElement;
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement; this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name })); this._defaultRadio.onclick = () =>
document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name }));
(this._row.querySelector("span.\\~critical") as HTMLSpanElement).onclick = this.delete; (this._row.querySelector("span.\\~critical") as HTMLSpanElement).onclick = this.delete;
this.update(name, p); this.update(name, p);
} }
update = (name: string, p: Profile) => { update = (name: string, p: Profile) => {
this.name = name; this.name = name;
this.admin = p.admin; this.admin = p.admin;
@@ -162,26 +188,43 @@ class profile implements Profile {
this.ombi = p.ombi; this.ombi = p.ombi;
this.jellyseerr = p.jellyseerr; this.jellyseerr = p.jellyseerr;
this.referrals_enabled = p.referrals_enabled; this.referrals_enabled = p.referrals_enabled;
} };
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); } setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => {
setJellyseerrFunc = (jellyseerrFunc: (jellyseerr: boolean) => void) => { this._jellyseerrButton.onclick = () => jellyseerrFunc(this._jellyseerr); } this._ombiButton.onclick = () => ombiFunc(this._ombi);
setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); } };
setEditFunc = (editFunc: (name: string) => void) => { this._editButton.onclick = () => editFunc(this.name); } setJellyseerrFunc = (jellyseerrFunc: (jellyseerr: boolean) => void) => {
this._jellyseerrButton.onclick = () => jellyseerrFunc(this._jellyseerr);
};
setReferralFunc = (referralFunc: (enabled: boolean) => void) => {
this._referralsButton.onclick = () => referralFunc(this._referralsEnabled);
};
setEditFunc = (editFunc: (name: string) => void) => {
this._editButton.onclick = () => editFunc(this.name);
};
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); } remove = () => {
document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name }));
this._row.remove();
};
delete = () => _delete("/profiles", { "name": this.name }, (req: XMLHttpRequest) => { delete = () =>
if (req.readyState == 4) { _delete("/profiles", { name: this.name }, (req: XMLHttpRequest) => {
if (req.status == 200 || req.status == 204) { if (req.readyState == 4) {
this.remove(); if (req.status == 200 || req.status == 204) {
} else { this.remove();
window.notifications.customError("profileDelete", window.lang.var("notifications", "errorDeleteProfile", `"${this.name}"`)); } else {
window.notifications.customError(
"profileDelete",
window.lang.var("notifications", "errorDeleteProfile", `"${this.name}"`),
);
}
} }
} });
})
asElement = (): HTMLTableRowElement => { return this._row; } asElement = (): HTMLTableRowElement => {
return this._row;
};
} }
interface profileResp { interface profileResp {
@@ -201,101 +244,119 @@ export class ProfileEditor {
private _profileName = document.getElementById("add-profile-name") as HTMLInputElement; private _profileName = document.getElementById("add-profile-name") as HTMLInputElement;
private _userSelect = document.getElementById("add-profile-user") as HTMLSelectElement; private _userSelect = document.getElementById("add-profile-user") as HTMLSelectElement;
private _storeHomescreen = document.getElementById("add-profile-homescreen") as HTMLInputElement; private _storeHomescreen = document.getElementById("add-profile-homescreen") as HTMLInputElement;
private _createJellyseerrProfile = window.jellyseerrEnabled ? document.getElementById("add-profile-jellyseerr") as HTMLInputElement : null; private _createJellyseerrProfile = window.jellyseerrEnabled
? (document.getElementById("add-profile-jellyseerr") as HTMLInputElement)
: null;
get empty(): boolean { return (Object.keys(this._table.children).length == 0) } get empty(): boolean {
return Object.keys(this._table.children).length == 0;
}
set empty(state: boolean) { set empty(state: boolean) {
if (state) { if (state) {
this._table.innerHTML = `<tr><td class="empty">${window.lang.strings("inviteNoInvites")}</td></tr>` this._table.innerHTML = `<tr><td class="empty">${window.lang.strings("inviteNoInvites")}</td></tr>`;
} else if (this._table.querySelector("td.empty")) { } else if (this._table.querySelector("td.empty")) {
this._table.textContent = ``; this._table.textContent = ``;
} }
} }
get default(): string { return this._default; } get default(): string {
return this._default;
}
set default(v: string) { set default(v: string) {
this._default = v; this._default = v;
if (v != "") { this._profiles[v].default = true; } if (v != "") {
this._profiles[v].default = true;
}
for (let name in this._profiles) { for (let name in this._profiles) {
if (name != v) { this._profiles[name].default = false; } if (name != v) {
this._profiles[name].default = false;
}
} }
} }
load = () => _get("/profiles", null, (req: XMLHttpRequest) => { load = () =>
if (req.readyState == 4) { _get("/profiles", null, (req: XMLHttpRequest) => {
if (req.status == 200) { if (req.readyState == 4) {
let resp = req.response as profileResp; if (req.status == 200) {
if (Object.keys(resp.profiles).length == 0) { let resp = req.response as profileResp;
this.empty = true; if (Object.keys(resp.profiles).length == 0) {
} else { this.empty = true;
this.empty = false; } else {
for (let name in resp.profiles) { this.empty = false;
if (name in this._profiles) { for (let name in resp.profiles) {
this._profiles[name].update(name, resp.profiles[name]); if (name in this._profiles) {
} else { this._profiles[name].update(name, resp.profiles[name]);
this._profiles[name] = new profile(name, resp.profiles[name]); } else {
if (window.ombiEnabled) { this._profiles[name] = new profile(name, resp.profiles[name]);
this._profiles[name].setOmbiFunc((ombi: boolean) => { if (window.ombiEnabled) {
if (ombi) { this._profiles[name].setOmbiFunc((ombi: boolean) => {
this._ombiProfiles.delete(name, (req: XMLHttpRequest) => { if (ombi) {
if (req.readyState == 4) { this._ombiProfiles.delete(name, (req: XMLHttpRequest) => {
if (req.status != 204) { if (req.readyState == 4) {
window.notifications.customError("errorDeleteOmbi", window.lang.notif("errorUnknown")); if (req.status != 204) {
return; window.notifications.customError(
"errorDeleteOmbi",
window.lang.notif("errorUnknown"),
);
return;
}
this._profiles[name].ombi = false;
} }
this._profiles[name].ombi = false; });
} } else {
}); window.modals.profiles.close();
} else { this._ombiProfiles.load(name);
window.modals.profiles.close(); }
this._ombiProfiles.load(name); });
} }
}); if (window.jellyseerrEnabled) {
} this._profiles[name].setJellyseerrFunc((jellyseerr: boolean) => {
if (window.jellyseerrEnabled) { if (jellyseerr) {
this._profiles[name].setJellyseerrFunc((jellyseerr: boolean) => { this._jellyseerrProfiles.delete(name, (req: XMLHttpRequest) => {
if (jellyseerr) { if (req.readyState == 4) {
this._jellyseerrProfiles.delete(name, (req: XMLHttpRequest) => { if (req.status != 204) {
if (req.readyState == 4) { window.notifications.customError(
if (req.status != 204) { "errorDeleteJellyseerr",
window.notifications.customError("errorDeleteJellyseerr", window.lang.notif("errorUnknown")); window.lang.notif("errorUnknown"),
return; );
return;
}
this._profiles[name].jellyseerr = false;
} }
this._profiles[name].jellyseerr = false; });
} } else {
}); window.modals.profiles.close();
} else { this._jellyseerrProfiles.load(name);
window.modals.profiles.close(); }
this._jellyseerrProfiles.load(name); });
} }
}); if (window.referralsEnabled) {
this._profiles[name].setReferralFunc((enabled: boolean) => {
if (enabled) {
this.disableReferrals(name);
} else {
this.enableReferrals(name);
}
});
}
this._profiles[name].setEditFunc(this._loadProfileEditor);
this._table.appendChild(this._profiles[name].asElement());
} }
if (window.referralsEnabled) {
this._profiles[name].setReferralFunc((enabled: boolean) => {
if (enabled) {
this.disableReferrals(name);
} else {
this.enableReferrals(name);
}
});
}
this._profiles[name].setEditFunc(this._loadProfileEditor);
this._table.appendChild(this._profiles[name].asElement());
} }
} }
this.default = resp.default_profile;
window.modals.profiles.show();
} else {
window.notifications.customError("profileEditor", window.lang.notif("errorLoadProfiles"));
} }
this.default = resp.default_profile;
window.modals.profiles.show();
} else {
window.notifications.customError("profileEditor", window.lang.notif("errorLoadProfiles"));
} }
} });
})
disableReferrals = (name: string) => _delete("/profiles/referral/" + name, null, (req: XMLHttpRequest) => { disableReferrals = (name: string) =>
if (req.readyState != 4) return; _delete("/profiles/referral/" + name, null, (req: XMLHttpRequest) => {
this.load(); if (req.readyState != 4) return;
}); this.load();
});
enableReferrals = (name: string) => { enableReferrals = (name: string) => {
const referralsInviteSelect = document.getElementById("enable-referrals-profile-invites") as HTMLSelectElement; const referralsInviteSelect = document.getElementById("enable-referrals-profile-invites") as HTMLSelectElement;
@@ -316,7 +377,7 @@ export class ProfileEditor {
} else { } else {
innerHTML += `<option>${window.lang.strings("inviteNoInvites")}</option>`; innerHTML += `<option>${window.lang.strings("inviteNoInvites")}</option>`;
} }
referralsInviteSelect.innerHTML = innerHTML; referralsInviteSelect.innerHTML = innerHTML;
}); });
@@ -327,22 +388,34 @@ export class ProfileEditor {
toggleLoader(button); toggleLoader(button);
let send = { let send = {
"profile": name, profile: name,
"invite": referralsInviteSelect.value invite: referralsInviteSelect.value,
}; };
_post("/profiles/referral/" + send["profile"] + "/" + send["invite"] + "/" + (referralsExpiry.checked ? "with-expiry" : "none"), send, (req: XMLHttpRequest) => { _post(
if (req.readyState == 4) { "/profiles/referral/" +
toggleLoader(button); send["profile"] +
if (req.status == 400) { "/" +
window.notifications.customError("unknownError", window.lang.notif("errorUnknown")); send["invite"] +
} else if (req.status == 200 || req.status == 204) { "/" +
window.notifications.customSuccess("enableReferralsSuccess", window.lang.notif("referralsEnabled")); (referralsExpiry.checked ? "with-expiry" : "none"),
send,
(req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 400) {
window.notifications.customError("unknownError", window.lang.notif("errorUnknown"));
} else if (req.status == 200 || req.status == 204) {
window.notifications.customSuccess(
"enableReferralsSuccess",
window.lang.notif("referralsEnabled"),
);
}
window.modals.enableReferralsProfile.close();
this.load();
} }
window.modals.enableReferralsProfile.close(); },
this.load(); );
}
});
}; };
referralsExpiry.checked = false; referralsExpiry.checked = false;
window.modals.profiles.close(); window.modals.profiles.close();
@@ -372,7 +445,7 @@ export class ProfileEditor {
let send: any; let send: any;
try { try {
send = JSON.parse(editor.value); send = JSON.parse(editor.value);
} catch(e: any) { } catch (e: any) {
submit.classList.add("~critical"); submit.classList.add("~critical");
submit.classList.remove("~urge"); submit.classList.remove("~urge");
window.notifications.customError("errorInvalidJSON", window.lang.notif("errorInvalidJSON")); window.notifications.customError("errorInvalidJSON", window.lang.notif("errorInvalidJSON"));
@@ -389,7 +462,7 @@ export class ProfileEditor {
// a 201 implies the profile was renamed. Since reloading profiles doesn't delete missing ones, // a 201 implies the profile was renamed. Since reloading profiles doesn't delete missing ones,
// we should delete the old one ourselves. // we should delete the old one ourselves.
if (req.status == 201) { if (req.status == 201) {
this._profiles[name].remove() this._profiles[name].remove();
delete this._profiles[name]; delete this._profiles[name];
} }
} else { } else {
@@ -403,16 +476,15 @@ export class ProfileEditor {
window.modals.profiles.close(); window.modals.profiles.close();
window.modals.editProfile.show(); window.modals.editProfile.show();
}) });
};
}
constructor() { constructor() {
(document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load; (document.getElementById("setting-profiles") as HTMLSpanElement).onclick = this.load;
document.addEventListener("profiles-default", (event: CustomEvent) => { document.addEventListener("profiles-default", (event: CustomEvent) => {
const prevDefault = this.default; const prevDefault = this.default;
const newDefault = event.detail; const newDefault = event.detail;
_post("/profiles/default", { "name": newDefault }, (req: XMLHttpRequest) => { _post("/profiles/default", { name: newDefault }, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 200 || req.status == 204) { if (req.status == 200 || req.status == 204) {
this.default = newDefault; this.default = newDefault;
@@ -428,38 +500,37 @@ export class ProfileEditor {
this.load(); this.load();
}); });
if (window.ombiEnabled) if (window.ombiEnabled) this._ombiProfiles = new ombiProfiles();
this._ombiProfiles = new ombiProfiles(); if (window.jellyseerrEnabled) this._jellyseerrProfiles = new jellyseerrProfiles();
if (window.jellyseerrEnabled)
this._jellyseerrProfiles = new jellyseerrProfiles();
this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => { this._createButton.onclick = () =>
if (req.readyState == 4) { _get("/users", null, (req: XMLHttpRequest) => {
if (req.status == 200 || req.status == 204) { if (req.readyState == 4) {
let innerHTML = ``; if (req.status == 200 || req.status == 204) {
for (let user of req.response["users"]) { let innerHTML = ``;
innerHTML += `<option value="${user['id']}">${user['name']}</option>`; for (let user of req.response["users"]) {
innerHTML += `<option value="${user["id"]}">${user["name"]}</option>`;
}
this._userSelect.innerHTML = innerHTML;
this._storeHomescreen.checked = true;
if (this._createJellyseerrProfile) this._createJellyseerrProfile.checked = true;
window.modals.profiles.close();
window.modals.addProfile.show();
} else {
window.notifications.customError("loadUsers", window.lang.notif("errorLoadUsers"));
} }
this._userSelect.innerHTML = innerHTML;
this._storeHomescreen.checked = true;
if (this._createJellyseerrProfile) this._createJellyseerrProfile.checked = true;
window.modals.profiles.close();
window.modals.addProfile.show();
} else {
window.notifications.customError("loadUsers", window.lang.notif("errorLoadUsers"));
} }
} });
});
this._createForm.onsubmit = (event: SubmitEvent) => { this._createForm.onsubmit = (event: SubmitEvent) => {
event.preventDefault(); event.preventDefault();
const button = this._createForm.querySelector("span.submit") as HTMLSpanElement; const button = this._createForm.querySelector("span.submit") as HTMLSpanElement;
toggleLoader(button); toggleLoader(button);
let send = { let send = {
"homescreen": this._storeHomescreen.checked, homescreen: this._storeHomescreen.checked,
"id": this._userSelect.value, id: this._userSelect.value,
"name": this._profileName.value name: this._profileName.value,
} };
if (this._createJellyseerrProfile) send["jellyseerr"] = this._createJellyseerrProfile.checked; if (this._createJellyseerrProfile) send["jellyseerr"] = this._createJellyseerrProfile.checked;
_post("/profiles", send, (req: XMLHttpRequest) => { _post("/profiles", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
@@ -467,15 +538,20 @@ export class ProfileEditor {
window.modals.addProfile.close(); window.modals.addProfile.close();
if (req.status == 200 || req.status == 204) { if (req.status == 200 || req.status == 204) {
this.load(); this.load();
window.notifications.customSuccess("createProfile", window.lang.var("notifications", "createProfile", `"${send['name']}"`)); window.notifications.customSuccess(
"createProfile",
window.lang.var("notifications", "createProfile", `"${send["name"]}"`),
);
} else { } else {
window.notifications.customError("createProfile", window.lang.var("notifications", "errorCreateProfile", `"${send['name']}"`)); window.notifications.customError(
"createProfile",
window.lang.var("notifications", "errorCreateProfile", `"${send["name"]}"`),
);
} }
window.modals.profiles.show(); window.modals.profiles.show();
} }
}) });
}; };
} }
} }
@@ -501,27 +577,32 @@ export class ombiProfiles {
let resp = {} as ombiUser; let resp = {} as ombiUser;
resp.id = this._select.value; resp.id = this._select.value;
resp.name = this._users[resp.id]; resp.name = this._users[resp.id];
_post("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)), resp, (req: XMLHttpRequest) => { _post(
if (req.readyState == 4) { "/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)),
toggleLoader(button); resp,
if (req.status == 200 || req.status == 204) { (req: XMLHttpRequest) => {
window.notifications.customSuccess("ombiDefaults", window.lang.notif("setOmbiProfile")); if (req.readyState == 4) {
} else { toggleLoader(button);
window.notifications.customError("ombiDefaults", window.lang.notif("errorSetOmbiProfile")); if (req.status == 200 || req.status == 204) {
window.notifications.customSuccess("ombiDefaults", window.lang.notif("setOmbiProfile"));
} else {
window.notifications.customError("ombiDefaults", window.lang.notif("errorSetOmbiProfile"));
}
window.modals.ombiProfile.close();
} }
window.modals.ombiProfile.close(); },
} );
}); };
}
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post); delete = (profile: string, post?: (req: XMLHttpRequest) => void) =>
_delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
load = (profile: string) => { load = (profile: string) => {
this._currentProfile = profile; this._currentProfile = profile;
_get("/ombi/users", null, (req: XMLHttpRequest) => { _get("/ombi/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 200 && "users" in req.response) { if (req.status == 200 && "users" in req.response) {
const users = req.response["users"] as ombiUser[]; const users = req.response["users"] as ombiUser[];
let innerHTML = ""; let innerHTML = "";
for (let user of users) { for (let user of users) {
this._users[user.id] = user.name; this._users[user.id] = user.name;
@@ -530,11 +611,11 @@ export class ombiProfiles {
this._select.innerHTML = innerHTML; this._select.innerHTML = innerHTML;
window.modals.ombiProfile.show(); window.modals.ombiProfile.show();
} else { } else {
window.notifications.customError("ombiLoadError", window.lang.notif("errorLoadOmbiUsers")) window.notifications.customError("ombiLoadError", window.lang.notif("errorLoadOmbiUsers"));
} }
} }
}); });
} };
} }
export class jellyseerrProfiles { export class jellyseerrProfiles {
@@ -563,16 +644,17 @@ export class jellyseerrProfiles {
window.modals.jellyseerrProfile.close(); window.modals.jellyseerrProfile.close();
} }
}); });
} };
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/jellyseerr/" + encodeURIComponent(encodeURIComponent(profile)), null, post); delete = (profile: string, post?: (req: XMLHttpRequest) => void) =>
_delete("/profiles/jellyseerr/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
load = (profile: string) => { load = (profile: string) => {
this._currentProfile = profile; this._currentProfile = profile;
_get("/jellyseerr/users", null, (req: XMLHttpRequest) => { _get("/jellyseerr/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 200 && "users" in req.response) { if (req.status == 200 && "users" in req.response) {
const users = req.response["users"] as ombiUser[]; const users = req.response["users"] as ombiUser[];
let innerHTML = ""; let innerHTML = "";
for (let user of users) { for (let user of users) {
this._users[user.id] = user.name; this._users[user.id] = user.name;
@@ -581,9 +663,9 @@ export class jellyseerrProfiles {
this._select.innerHTML = innerHTML; this._select.innerHTML = innerHTML;
window.modals.jellyseerrProfile.show(); window.modals.jellyseerrProfile.show();
} else { } else {
window.notifications.customError("jellyseerrLoadError", window.lang.notif("errorLoadUsers")) window.notifications.customError("jellyseerrLoadError", window.lang.notif("errorLoadUsers"));
} }
} }
}); });
} };
} }

View File

@@ -6,7 +6,7 @@ declare var window: GlobalWindow;
export enum QueryOperator { export enum QueryOperator {
Greater = ">", Greater = ">",
Lower = "<", Lower = "<",
Equal = "=" Equal = "=",
} }
export function QueryOperatorToDateText(op: QueryOperator): string { export function QueryOperatorToDateText(op: QueryOperator): string {
@@ -29,7 +29,7 @@ export interface QueryType {
date: boolean; date: boolean;
dependsOnElement?: string; // Format for querySelector dependsOnElement?: string; // Format for querySelector
show?: boolean; show?: boolean;
localOnly?: boolean // Indicates can't be performed server-side. localOnly?: boolean; // Indicates can't be performed server-side.
} }
export interface SearchConfiguration { export interface SearchConfiguration {
@@ -62,7 +62,7 @@ export interface QueryDTO {
field: string; field: string;
operator: QueryOperator; operator: QueryOperator;
value: boolean | string | DateAttempt; value: boolean | string | DateAttempt;
}; }
export abstract class Query { export abstract class Query {
protected _subject: QueryType; protected _subject: QueryType;
@@ -83,8 +83,10 @@ export abstract class Query {
this._card.addEventListener("click", v); this._card.addEventListener("click", v);
} }
asElement(): HTMLElement { return this._card; } asElement(): HTMLElement {
return this._card;
}
public abstract compare(subjectValue: any): boolean; public abstract compare(subjectValue: any): boolean;
asDTO(): QueryDTO | null { asDTO(): QueryDTO | null {
@@ -95,7 +97,9 @@ export abstract class Query {
return out; return out;
} }
get subject(): QueryType { return this._subject; } get subject(): QueryType {
return this._subject;
}
getValueFromItem(item: SearchableItem): any { getValueFromItem(item: SearchableItem): any {
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(item), this.subject.getter).get.call(item); return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(item), this.subject.getter).get.call(item);
@@ -105,7 +109,9 @@ export abstract class Query {
return this.compare(this.getValueFromItem(item)); return this.compare(this.getValueFromItem(item));
} }
get localOnly(): boolean { return this._subject.localOnly ? true : false; } get localOnly(): boolean {
return this._subject.localOnly ? true : false;
}
} }
export class BoolQuery extends Query { export class BoolQuery extends Query {
@@ -122,7 +128,7 @@ export class BoolQuery extends Query {
} }
this._card.innerHTML = ` this._card.innerHTML = `
<span class="font-bold">${subject.name}</span> <span class="font-bold">${subject.name}</span>
<i class="text-2xl ri-${this._value? "checkbox" : "close"}-circle-fill"></i> <i class="text-2xl ri-${this._value ? "checkbox" : "close"}-circle-fill"></i>
`; `;
} }
@@ -136,14 +142,16 @@ export class BoolQuery extends Query {
isBool = true; isBool = true;
boolState = false; boolState = false;
} }
return [boolState, isBool] return [boolState, isBool];
} }
get value(): boolean { return this._value; } get value(): boolean {
return this._value;
}
// Ripped from old code. Why it's like this, I don't know // Ripped from old code. Why it's like this, I don't know
public compare(subjectBool: boolean): boolean { public compare(subjectBool: boolean): boolean {
return ((subjectBool && this._value) || (!subjectBool && !this._value)) return (subjectBool && this._value) || (!subjectBool && !this._value);
} }
asDTO(): QueryDTO | null { asDTO(): QueryDTO | null {
@@ -167,12 +175,14 @@ export class StringQuery extends Query {
`; `;
} }
get value(): string { return this._value; } get value(): string {
return this._value;
}
public compare(subjectString: string): boolean { public compare(subjectString: string): boolean {
return subjectString.toLowerCase().includes(this._value); return subjectString.toLowerCase().includes(this._value);
} }
asDTO(): QueryDTO | null { asDTO(): QueryDTO | null {
let out = super.asDTO(); let out = super.asDTO();
if (out === null) return null; if (out === null) return null;
@@ -211,7 +221,7 @@ export class DateQuery extends Query {
this._card.classList.add("button", "~neutral", "@low", "center", "flex", "flex-row", "gap-2"); this._card.classList.add("button", "~neutral", "@low", "center", "flex", "flex-row", "gap-2");
let dateText = QueryOperatorToDateText(operator); let dateText = QueryOperatorToDateText(operator);
this._card.innerHTML = ` this._card.innerHTML = `
<span class="font-bold">${subject.name}:</span> ${dateText != "" ? dateText+" " : ""}${value.text} <span class="font-bold">${subject.name}:</span> ${dateText != "" ? dateText + " " : ""}${value.text}
`; `;
} }
@@ -225,11 +235,13 @@ export class DateQuery extends Query {
let out = parseDateString(valueString); let out = parseDateString(valueString);
let isValid = true; let isValid = true;
if (out.invalid) isValid = false; if (out.invalid) isValid = false;
return [out, op, isValid]; return [out, op, isValid];
} }
get value(): ParsedDate { return this._value; } get value(): ParsedDate {
return this._value;
}
public compare(subjectDate: Date): boolean { public compare(subjectDate: Date): boolean {
// We want to compare only the fields given in this._value, // We want to compare only the fields given in this._value,
@@ -237,10 +249,7 @@ export class DateQuery extends Query {
const temp = new Date(subjectDate.valueOf()); const temp = new Date(subjectDate.valueOf());
for (let [field] of dateGetters) { for (let [field] of dateGetters) {
if (field in this._value.attempt) { if (field in this._value.attempt) {
dateSetters.get(field).call( dateSetters.get(field).call(temp, dateGetters.get(field).call(this._value.date));
temp,
dateGetters.get(field).call(this._value.date)
);
} }
} }
@@ -251,7 +260,7 @@ export class DateQuery extends Query {
} }
return subjectDate > temp; return subjectDate > temp;
} }
asDTO(): QueryDTO | null { asDTO(): QueryDTO | null {
let out = super.asDTO(); let out = super.asDTO();
if (out === null) return null; if (out === null) return null;
@@ -272,7 +281,7 @@ export type SearchableItems = { [id: string]: SearchableItem };
export class Search { export class Search {
private _c: SearchConfiguration; private _c: SearchConfiguration;
private _sortField: string = ""; private _sortField: string = "";
private _ascending: boolean = true; private _ascending: boolean = true;
private _ordering: string[] = []; private _ordering: string[] = [];
private _items: SearchableItems = {}; private _items: SearchableItems = {};
// Search queries (filters) // Search queries (filters)
@@ -281,7 +290,9 @@ export class Search {
private _searchTerms: string[] = []; private _searchTerms: string[] = [];
inSearch: boolean = false; inSearch: boolean = false;
private _inServerSearch: boolean = false; private _inServerSearch: boolean = false;
get inServerSearch(): boolean { return this._inServerSearch; } get inServerSearch(): boolean {
return this._inServerSearch;
}
set inServerSearch(v: boolean) { set inServerSearch(v: boolean) {
const previous = this._inServerSearch; const previous = this._inServerSearch;
this._inServerSearch = v; this._inServerSearch = v;
@@ -316,14 +327,14 @@ export class Search {
} }
} }
if (query[i] == " " || i == query.length-1) { if (query[i] == " " || i == query.length - 1) {
if (lastQuote != -1) { if (lastQuote != -1) {
continue; continue;
} else { } else {
let end = i+1; let end = i + 1;
if (query[i] == " ") { if (query[i] == " ") {
end = i; end = i;
while (i+1 < query.length && query[i+1] == " ") { while (i + 1 < query.length && query[i + 1] == " ") {
i += 1; i += 1;
} }
} }
@@ -333,7 +344,7 @@ export class Search {
} }
} }
return words; return words;
} };
parseTokens = (tokens: string[]): [string[], Query[]] => { parseTokens = (tokens: string[]): [string[], Query[]] => {
let queries: Query[] = []; let queries: Query[] = [];
@@ -346,8 +357,8 @@ export class Search {
continue; continue;
} }
// 2. A filter query of some sort. // 2. A filter query of some sort.
const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)]; const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":") + 1)];
if (!(split[0] in this._c.queries)) continue; if (!(split[0] in this._c.queries)) continue;
const queryFormat = this._c.queries[split[0]]; const queryFormat = this._c.queries[split[0]];
@@ -360,9 +371,12 @@ export class Search {
q = new BoolQuery(queryFormat, boolState); q = new BoolQuery(queryFormat, boolState);
q.onclick = () => { q.onclick = () => {
for (let quote of [`"`, `'`, ``]) { for (let quote of [`"`, `'`, ``]) {
this._c.search.value = this._c.search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); this._c.search.value = this._c.search.value.replace(
split[0] + ":" + quote + split[1] + quote,
"",
);
} }
this._c.search.oninput((null as Event)); this._c.search.oninput(null as Event);
}; };
queries.push(q); queries.push(q);
continue; continue;
@@ -376,8 +390,8 @@ export class Search {
let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig");
this._c.search.value = this._c.search.value.replace(regex, ""); this._c.search.value = this._c.search.value.replace(regex, "");
} }
this._c.search.oninput((null as Event)); this._c.search.oninput(null as Event);
} };
queries.push(q); queries.push(q);
continue; continue;
} }
@@ -385,23 +399,23 @@ export class Search {
let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]); let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]);
if (!isDate) continue; if (!isDate) continue;
q = new DateQuery(queryFormat, op, parsedDate); q = new DateQuery(queryFormat, op, parsedDate);
q.onclick = () => { q.onclick = () => {
for (let quote of [`"`, `'`, ``]) { for (let quote of [`"`, `'`, ``]) {
let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig");
this._c.search.value = this._c.search.value.replace(regex, ""); this._c.search.value = this._c.search.value.replace(regex, "");
} }
this._c.search.oninput((null as Event)); this._c.search.oninput(null as Event);
} };
queries.push(q); queries.push(q);
continue; continue;
} }
// if (q != null) queries.push(q); // if (q != null) queries.push(q);
} }
return [searchTerms, queries]; return [searchTerms, queries];
} };
// Returns a list of identifiers (used as keys in items, values in ordering). // Returns a list of identifiers (used as keys in items, values in ordering).
searchParsed = (searchTerms: string[], queries: Query[]): string[] => { searchParsed = (searchTerms: string[], queries: Query[]): string[] => {
let result: string[] = [...this._ordering]; let result: string[] = [...this._ordering];
@@ -432,7 +446,7 @@ export class Search {
for (let q of queries) { for (let q of queries) {
this._c.filterArea.appendChild(q.asElement()); this._c.filterArea.appendChild(q.asElement());
// Skip if this query has already been performed by the server. // Skip if this query has already been performed by the server.
if (this.inServerSearch && !(q.localOnly)) continue; if (this.inServerSearch && !q.localOnly) continue;
let cachedResult = [...result]; let cachedResult = [...result];
if (q.type == "bool") { if (q.type == "bool") {
@@ -463,7 +477,7 @@ export class Search {
result.splice(result.indexOf(id), 1); result.splice(result.indexOf(id), 1);
continue; continue;
} }
let value = new Date(unixValue*1000); let value = new Date(unixValue * 1000);
if (!q.compare(value)) { if (!q.compare(value)) {
result.splice(result.indexOf(id), 1); result.splice(result.indexOf(id), 1);
@@ -472,17 +486,17 @@ export class Search {
} }
} }
return result; return result;
} };
// Returns a list of identifiers (used as keys in items, values in ordering). // Returns a list of identifiers (used as keys in items, values in ordering).
search = (query: string): string[] => { search = (query: string): string[] => {
let timer = this.timeSearches ? performance.now() : null; let timer = this.timeSearches ? performance.now() : null;
this._c.filterArea.textContent = ""; this._c.filterArea.textContent = "";
const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query)); const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query));
let result = this.searchParsed(searchTerms, queries); let result = this.searchParsed(searchTerms, queries);
this._queries = queries; this._queries = queries;
this._searchTerms = searchTerms; this._searchTerms = searchTerms;
@@ -491,44 +505,57 @@ export class Search {
console.debug(`Search took ${totalTime}ms`); console.debug(`Search took ${totalTime}ms`);
} }
return result; return result;
} };
// postServerSearch performs local-only queries after a server search if necessary. // postServerSearch performs local-only queries after a server search if necessary.
postServerSearch = () => { postServerSearch = () => {
this.searchParsed(this._searchTerms, this._queries); this.searchParsed(this._searchTerms, this._queries);
}; };
showHideSearchOptionsHeader = () => { showHideSearchOptionsHeader = () => {
let sortingBy = false; let sortingBy = false;
if (this._c.sortingByButton) sortingBy = !(this._c.sortingByButton.classList.contains("hidden")); if (this._c.sortingByButton) sortingBy = !this._c.sortingByButton.classList.contains("hidden");
const hasFilters = this._c.filterArea.textContent != ""; const hasFilters = this._c.filterArea.textContent != "";
if (sortingBy || hasFilters) { if (sortingBy || hasFilters) {
this._c.searchOptionsHeader.classList.remove("hidden"); this._c.searchOptionsHeader.classList.remove("hidden");
} else { } else {
this._c.searchOptionsHeader.classList.add("hidden"); this._c.searchOptionsHeader.classList.add("hidden");
} }
} };
// -all- elements. // -all- elements.
get items(): { [id: string]: SearchableItem } { return this._items; } get items(): { [id: string]: SearchableItem } {
return this._items;
}
// set items(v: { [id: string]: SearchableItem }) { // set items(v: { [id: string]: SearchableItem }) {
// this._items = v; // this._items = v;
// } // }
// The order of -all- elements (even those hidden), by their identifier. // The order of -all- elements (even those hidden), by their identifier.
get ordering(): string[] { return this._ordering; } get ordering(): string[] {
return this._ordering;
}
// Specifically dis-allow setting ordering itself, so that setOrdering is used instead (for the field and ascending params). // Specifically dis-allow setting ordering itself, so that setOrdering is used instead (for the field and ascending params).
// set ordering(v: string[]) { this._ordering = v; } // set ordering(v: string[]) { this._ordering = v; }
setOrdering = (v: string[], field: string, ascending: boolean) => { setOrdering = (v: string[], field: string, ascending: boolean) => {
this._ordering = v; this._ordering = v;
this._sortField = field; this._sortField = field;
this._ascending = ascending; this._ascending = ascending;
};
get sortField(): string {
return this._sortField;
}
get ascending(): boolean {
return this._ascending;
} }
get sortField(): string { return this._sortField; } onSearchBoxChange = (
get ascending(): boolean { return this._ascending; } newItems: boolean = false,
appendedItems: boolean = false,
onSearchBoxChange = (newItems: boolean = false, appendedItems: boolean = false, loadAll: boolean = false, callback?: (resp: paginatedDTO) => void) => { loadAll: boolean = false,
callback?: (resp: paginatedDTO) => void,
) => {
const query = this._c.search.value; const query = this._c.search.value;
if (!query) { if (!query) {
this.inSearch = false; this.inSearch = false;
@@ -554,7 +581,7 @@ export class Search {
this.showHideSearchOptionsHeader(); this.showHideSearchOptionsHeader();
this.setNotFoundPanelVisibility(results.length == 0); this.setNotFoundPanelVisibility(results.length == 0);
if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0); if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0);
} };
setNotFoundPanelVisibility = (visible: boolean) => { setNotFoundPanelVisibility = (visible: boolean) => {
if (this._inServerSearch || !this.inSearch) { if (this._inServerSearch || !this.inSearch) {
@@ -567,14 +594,13 @@ export class Search {
} else { } else {
this._c.notFoundPanel.classList.add("unfocused"); this._c.notFoundPanel.classList.add("unfocused");
} }
} };
fillInFilter = (name: string, value: string, offset?: number) => { fillInFilter = (name: string, value: string, offset?: number) => {
this._c.search.value = name + ":" + value + " " + this._c.search.value; this._c.search.value = name + ":" + value + " " + this._c.search.value;
this._c.search.focus(); this._c.search.focus();
let newPos = name.length + 1 + value.length; let newPos = name.length + 1 + value.length;
if (typeof offset !== 'undefined') if (typeof offset !== "undefined") newPos += offset;
newPos += offset;
this._c.search.setSelectionRange(newPos, newPos); this._c.search.setSelectionRange(newPos, newPos);
this._c.search.oninput(null as any); this._c.search.oninput(null as any);
}; };
@@ -592,7 +618,16 @@ export class Search {
} }
const container = document.createElement("span") as HTMLSpanElement; const container = document.createElement("span") as HTMLSpanElement;
container.classList.add("button", "button-xl", "~neutral", "@low", "align-bottom", "flex", "flex-row", "gap-2"); container.classList.add(
"button",
"button-xl",
"~neutral",
"@low",
"align-bottom",
"flex",
"flex-row",
"gap-2",
);
container.innerHTML = ` container.innerHTML = `
<div class="flex flex-col"> <div class="flex flex-col">
<span>${query.name}</span> <span>${query.name}</span>
@@ -624,7 +659,7 @@ export class Search {
// Position cursor between quotes // Position cursor between quotes
button.addEventListener("click", () => this.fillInFilter(queryName, `""`, -1)); button.addEventListener("click", () => this.fillInFilter(queryName, `""`, -1));
container.appendChild(button); container.appendChild(button);
} }
if (query.date) { if (query.date) {
@@ -645,27 +680,26 @@ export class Search {
afterDate.classList.add("button", "~urge", "flex", "flex-row", "gap-2"); afterDate.classList.add("button", "~urge", "flex", "flex-row", "gap-2");
afterDate.innerHTML = `<i class="ri-calendar-check-line"></i>After Date`; afterDate.innerHTML = `<i class="ri-calendar-check-line"></i>After Date`;
afterDate.addEventListener("click", () => this.fillInFilter(queryName, `">"`, -1)); afterDate.addEventListener("click", () => this.fillInFilter(queryName, `">"`, -1));
container.appendChild(onDate); container.appendChild(onDate);
container.appendChild(beforeDate); container.appendChild(beforeDate);
container.appendChild(afterDate); container.appendChild(afterDate);
} }
filterListContainer.appendChild(container); filterListContainer.appendChild(container);
} }
this._c.filterList.appendChild(filterListContainer) this._c.filterList.appendChild(filterListContainer);
} };
onServerSearch = () => { onServerSearch = () => {
const newServerSearch = !this.inServerSearch; const newServerSearch = !this.inServerSearch;
this.inServerSearch = true; this.inServerSearch = true;
this.searchServer(newServerSearch); this.searchServer(newServerSearch);
} };
searchServer = (newServerSearch: boolean) => { searchServer = (newServerSearch: boolean) => {
this._c.searchServer(this.serverSearchParams(this._searchTerms, this._queries), newServerSearch); this._c.searchServer(this.serverSearchParams(this._searchTerms, this._queries), newServerSearch);
} };
serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => { serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => {
let req: ServerSearchReqDTO = { let req: ServerSearchReqDTO = {
@@ -674,18 +708,18 @@ export class Search {
limit: -1, limit: -1,
page: 0, page: 0,
sortByField: this.sortField, sortByField: this.sortField,
ascending: this.ascending ascending: this.ascending,
}; };
for (const q of queries) { for (const q of queries) {
const dto = q.asDTO(); const dto = q.asDTO();
if (dto !== null) req.queries.push(dto); if (dto !== null) req.queries.push(dto);
} }
return req; return req;
} };
setServerSearchButtonsDisabled = (disabled: boolean) => { setServerSearchButtonsDisabled = (disabled: boolean) => {
this._serverSearchButtons.forEach((v: HTMLButtonElement) => v.disabled = disabled); this._serverSearchButtons.forEach((v: HTMLButtonElement) => (v.disabled = disabled));
} };
constructor(c: SearchConfiguration) { constructor(c: SearchConfiguration) {
this._c = c; this._c = c;
@@ -693,14 +727,16 @@ export class Search {
this._c.search.oninput = () => { this._c.search.oninput = () => {
this.inServerSearch = false; this.inServerSearch = false;
this.onSearchBoxChange(); this.onSearchBoxChange();
} };
this._c.search.addEventListener("keyup", (ev: KeyboardEvent) => { this._c.search.addEventListener("keyup", (ev: KeyboardEvent) => {
if (ev.key == "Enter") { if (ev.key == "Enter") {
this.onServerSearch(); this.onServerSearch();
} }
}); });
const clearSearchButtons = Array.from(document.querySelectorAll(this._c.clearSearchButtonSelector)) as Array<HTMLSpanElement>; const clearSearchButtons = Array.from(
document.querySelectorAll(this._c.clearSearchButtonSelector),
) as Array<HTMLSpanElement>;
for (let b of clearSearchButtons) { for (let b of clearSearchButtons) {
b.addEventListener("click", () => { b.addEventListener("click", () => {
this._c.search.value = ""; this._c.search.value = "";
@@ -708,8 +744,10 @@ export class Search {
this.onSearchBoxChange(); this.onSearchBoxChange();
}); });
} }
this._serverSearchButtons = Array.from(document.querySelectorAll(this._c.serverSearchButtonSelector)) as Array<HTMLSpanElement>; this._serverSearchButtons = Array.from(
document.querySelectorAll(this._c.serverSearchButtonSelector),
) as Array<HTMLSpanElement>;
for (let b of this._serverSearchButtons) { for (let b of this._serverSearchButtons) {
b.addEventListener("click", () => { b.addEventListener("click", () => {
this.onServerSearch(); this.onServerSearch();

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,38 @@
const removeMd = require("remove-markdown"); const removeMd = require("remove-markdown");
function stripAltText(md: string): string { function stripAltText(md: string): string {
let altStart = -1; // Start of alt text (between '[' & ']') let altStart = -1; // Start of alt text (between '[' & ']')
let urlStart = -1; // Start of url (between '(' & ')') let urlStart = -1; // Start of url (between '(' & ')')
let urlEnd = -1; let urlEnd = -1;
let prevURLEnd = -2; let prevURLEnd = -2;
let out = ""; let out = "";
for (let i = 0; i < md.length; i++) { for (let i = 0; i < md.length; i++) {
if (altStart != -1 && urlStart != -1 && md.charAt(i) == ')') { if (altStart != -1 && urlStart != -1 && md.charAt(i) == ")") {
urlEnd = i - 1; urlEnd = i - 1;
out += md.substring(prevURLEnd+2, altStart-1) + md.substring(urlStart, urlEnd+1); out += md.substring(prevURLEnd + 2, altStart - 1) + md.substring(urlStart, urlEnd + 1);
prevURLEnd = urlEnd; prevURLEnd = urlEnd;
altStart = -1; altStart = -1;
urlStart = -1; urlStart = -1;
urlEnd = -1; urlEnd = -1;
continue; continue;
} }
if (md.charAt(i) == '[' && altStart == -1) { if (md.charAt(i) == "[" && altStart == -1) {
altStart = i + 1 altStart = i + 1;
if (i > 0 && md.charAt(i-1) == '!') { if (i > 0 && md.charAt(i - 1) == "!") {
altStart-- altStart--;
} }
} }
if (i > 0 && md.charAt(i-1) == ']' && md.charAt(i) == '(' && urlStart == -1) { if (i > 0 && md.charAt(i - 1) == "]" && md.charAt(i) == "(" && urlStart == -1) {
urlStart = i + 1 urlStart = i + 1;
} }
} }
if (prevURLEnd + 1 != md.length - 1) { if (prevURLEnd + 1 != md.length - 1) {
out += md.substring(prevURLEnd+2) out += md.substring(prevURLEnd + 2);
} }
if (out == "") { if (out == "") {
return md return md;
} }
return out return out;
} }
export function stripMarkdown(md: string): string { export function stripMarkdown(md: string): string {

View File

@@ -8,15 +8,14 @@ export interface Tab {
postFunc?: () => void; postFunc?: () => void;
} }
export class Tabs implements Tabs { export class Tabs implements Tabs {
private _current: string = ""; private _current: string = "";
private _baseOffset = -1; private _baseOffset = -1;
tabs: Map<string, Tab>; tabs: Map<string, Tab>;
pages: PageManager; pages: PageManager;
constructor() { constructor() {
this.tabs = new Map<string, Tab>; this.tabs = new Map<string, Tab>();
this.pages = new PageManager({ this.pages = new PageManager({
hideOthersOnPageShow: true, hideOthersOnPageShow: true,
defaultName: "invites", defaultName: "invites",
@@ -24,7 +23,13 @@ export class Tabs implements Tabs {
}); });
} }
addTab = (tabID: string, url: string, preFunc = () => void {}, postFunc = () => void {}, unloadFunc = () => void {}) => { addTab = (
tabID: string,
url: string,
preFunc = () => void {},
postFunc = () => void {},
unloadFunc = () => void {},
) => {
let tab: Tab = { let tab: Tab = {
page: null, page: null,
tabEl: document.getElementById("tab-" + tabID) as HTMLDivElement, tabEl: document.getElementById("tab-" + tabID) as HTMLDivElement,
@@ -37,12 +42,12 @@ export class Tabs implements Tabs {
} }
tab.page = { tab.page = {
name: tabID, name: tabID,
title: document.title, /*FIXME: Get actual names from translations*/ title: document.title /*FIXME: Get actual names from translations*/,
url: url, url: url,
show: () => { show: () => {
tab.buttonEl.classList.add("active", "~urge"); tab.buttonEl.classList.add("active", "~urge");
tab.tabEl.classList.remove("unfocused"); tab.tabEl.classList.remove("unfocused");
tab.buttonEl.parentElement.scrollTo(tab.buttonEl.offsetLeft-this._baseOffset, 0); tab.buttonEl.parentElement.scrollTo(tab.buttonEl.offsetLeft - this._baseOffset, 0);
document.dispatchEvent(new CustomEvent("tab-change", { detail: tabID })); document.dispatchEvent(new CustomEvent("tab-change", { detail: tabID }));
return true; return true;
}, },
@@ -56,23 +61,33 @@ export class Tabs implements Tabs {
shouldSkip: () => false, shouldSkip: () => false,
}; };
this.pages.setPage(tab.page); this.pages.setPage(tab.page);
tab.buttonEl.onclick = () => { this.switch(tabID); }; tab.buttonEl.onclick = () => {
this.switch(tabID);
};
this.tabs.set(tabID, tab); this.tabs.set(tabID, tab);
} };
get current(): string { return this._current; } get current(): string {
set current(tabID: string) { this.switch(tabID); } return this._current;
}
set current(tabID: string) {
this.switch(tabID);
}
switch = (tabID: string, noRun: boolean = false) => { switch = (tabID: string, noRun: boolean = false) => {
let t = this.tabs.get(tabID); let t = this.tabs.get(tabID);
if (t == undefined) { if (t == undefined) {
[t] = this.tabs.values(); [t] = this.tabs.values();
} }
this._current = t.page.name; this._current = t.page.name;
if (t.preFunc && !noRun) { t.preFunc(); } if (t.preFunc && !noRun) {
t.preFunc();
}
this.pages.load(tabID); this.pages.load(tabID);
if (t.postFunc && !noRun) { t.postFunc(); } if (t.postFunc && !noRun) {
} t.postFunc();
}
};
} }

View File

@@ -1,21 +1,19 @@
export class ThemeManager { export class ThemeManager {
private _themeButton: HTMLElement = null; private _themeButton: HTMLElement = null;
private _metaTag: HTMLMetaElement; private _metaTag: HTMLMetaElement;
private _cssLightFiles: HTMLLinkElement[]; private _cssLightFiles: HTMLLinkElement[];
private _cssDarkFiles: HTMLLinkElement[]; private _cssDarkFiles: HTMLLinkElement[];
private _beforeTransition = () => { private _beforeTransition = () => {
const doc = document.documentElement; const doc = document.documentElement;
const onTransitionDone = () => { const onTransitionDone = () => {
doc.classList.remove('nightwind'); doc.classList.remove("nightwind");
doc.removeEventListener('transitionend', onTransitionDone); doc.removeEventListener("transitionend", onTransitionDone);
} };
doc.addEventListener('transitionend', onTransitionDone); doc.addEventListener("transitionend", onTransitionDone);
if (!doc.classList.contains('nightwind')) { if (!doc.classList.contains("nightwind")) {
doc.classList.add('nightwind'); doc.classList.add("nightwind");
} }
}; };
@@ -40,20 +38,24 @@ export class ThemeManager {
this._themeButton = button; this._themeButton = button;
this._themeButton.onclick = this.toggle; this._themeButton.onclick = this.toggle;
this._updateThemeIcon(); this._updateThemeIcon();
} };
toggle = () => { toggle = () => {
this._toggle(); this._toggle();
if (this._themeButton) { if (this._themeButton) {
this._updateThemeIcon(); this._updateThemeIcon();
} }
} };
constructor(button?: HTMLElement) { constructor(button?: HTMLElement) {
this._metaTag = document.querySelector("meta[name=color-scheme]") as HTMLMetaElement; this._metaTag = document.querySelector("meta[name=color-scheme]") as HTMLMetaElement;
this._cssLightFiles = Array.from(document.head.querySelectorAll("link[data-theme=light]")) as Array<HTMLLinkElement>; this._cssLightFiles = Array.from(
this._cssDarkFiles = Array.from(document.head.querySelectorAll("link[data-theme=dark]")) as Array<HTMLLinkElement>; document.head.querySelectorAll("link[data-theme=light]"),
) as Array<HTMLLinkElement>;
this._cssDarkFiles = Array.from(
document.head.querySelectorAll("link[data-theme=dark]"),
) as Array<HTMLLinkElement>;
this._cssLightFiles.forEach((el) => el.remove()); this._cssLightFiles.forEach((el) => el.remove());
this._cssDarkFiles.forEach((el) => el.remove()); this._cssDarkFiles.forEach((el) => el.remove());
const theme = localStorage.getItem("theme"); const theme = localStorage.getItem("theme");
@@ -61,12 +63,11 @@ export class ThemeManager {
this._enable(true); this._enable(true);
} else if (theme == "light") { } else if (theme == "light") {
this._enable(false); this._enable(false);
} else if (window.matchMedia('(prefers-color-scheme: dark)').media !== 'not all') { } else if (window.matchMedia("(prefers-color-scheme: dark)").media !== "not all") {
this._enable(true); this._enable(true);
} }
if (button) if (button) this.bindButton(button);
this.bindButton(button);
} }
private _toggle = () => { private _toggle = () => {
@@ -74,25 +75,25 @@ export class ThemeManager {
this._beforeTransition(); this._beforeTransition();
const dark = !document.documentElement.classList.contains("dark"); const dark = !document.documentElement.classList.contains("dark");
if (dark) { if (dark) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add("dark");
metaValue = "dark light"; metaValue = "dark light";
this._cssLightFiles.forEach((el) => el.remove()); this._cssLightFiles.forEach((el) => el.remove());
this._cssDarkFiles.forEach((el) => document.head.appendChild(el)); this._cssDarkFiles.forEach((el) => document.head.appendChild(el));
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove("dark");
this._cssDarkFiles.forEach((el) => el.remove()); this._cssDarkFiles.forEach((el) => el.remove());
this._cssLightFiles.forEach((el) => document.head.appendChild(el)); this._cssLightFiles.forEach((el) => document.head.appendChild(el));
} }
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? "dark" : "light"); localStorage.setItem("theme", document.documentElement.classList.contains("dark") ? "dark" : "light");
// this._metaTag.setAttribute("content", metaValue); // this._metaTag.setAttribute("content", metaValue);
}; };
private _enable = (dark: boolean) => { private _enable = (dark: boolean) => {
const mode = dark ? "dark" : "light"; const mode = dark ? "dark" : "light";
const opposite = dark ? "light" : "dark"; const opposite = dark ? "light" : "dark";
localStorage.setItem('theme', dark ? "dark" : "light"); localStorage.setItem("theme", dark ? "dark" : "light");
this._beforeTransition(); this._beforeTransition();
@@ -100,7 +101,7 @@ export class ThemeManager {
document.documentElement.classList.remove(opposite); document.documentElement.classList.remove(opposite);
} }
document.documentElement.classList.add(mode); document.documentElement.classList.add(mode);
if (dark) { if (dark) {
this._cssLightFiles.forEach((el) => el.remove()); this._cssLightFiles.forEach((el) => el.remove());
this._cssDarkFiles.forEach((el) => document.head.appendChild(el)); this._cssDarkFiles.forEach((el) => document.head.appendChild(el));
@@ -117,4 +118,4 @@ export class ThemeManager {
this._updateThemeIcon(); this._updateThemeIcon();
} }
}; };
} }

View File

@@ -5,7 +5,7 @@ export interface HiddenInputConf {
customContainerHTML?: string; customContainerHTML?: string;
input?: string; input?: string;
clickAwayShouldSave?: boolean; clickAwayShouldSave?: boolean;
}; }
export class HiddenInputField { export class HiddenInputField {
public static editClass = "ri-edit-line"; public static editClass = "ri-edit-line";
@@ -13,17 +13,17 @@ export class HiddenInputField {
private _c: HiddenInputConf; private _c: HiddenInputConf;
private _input: HTMLInputElement; private _input: HTMLInputElement;
private _content: HTMLElement private _content: HTMLElement;
private _toggle: HTMLElement; private _toggle: HTMLElement;
previous: string; previous: string;
constructor(c: HiddenInputConf) { constructor(c: HiddenInputConf) {
this._c = c; this._c = c;
if (!(this._c.customContainerHTML)) { if (!this._c.customContainerHTML) {
this._c.customContainerHTML = `<span class="hidden-input-content"></span>`; this._c.customContainerHTML = `<span class="hidden-input-content"></span>`;
} }
if (!(this._c.input)) { if (!this._c.input) {
this._c.input = `<input type="text" class="field ~neutral @low max-w-24 hidden-input-input">`; this._c.input = `<input type="text" class="field ~neutral @low max-w-24 hidden-input-input">`;
} }
this._c.container.innerHTML = ` this._c.container.innerHTML = `
@@ -40,7 +40,7 @@ export class HiddenInputField {
this._input.classList.add("py-0.5", "px-1", "hidden"); this._input.classList.add("py-0.5", "px-1", "hidden");
this._toggle = this._c.container.querySelector(".hidden-input-toggle"); this._toggle = this._c.container.querySelector(".hidden-input-toggle");
this._content = this._c.container.querySelector(".hidden-input-content"); this._content = this._c.container.querySelector(".hidden-input-content");
this._toggle.onclick = () => { this._toggle.onclick = () => {
this.editing = !this.editing; this.editing = !this.editing;
}; };
@@ -49,22 +49,29 @@ export class HiddenInputField {
e.preventDefault(); e.preventDefault();
this._toggle.click(); this._toggle.click();
} }
}) });
this.setEditing(false, true); this.setEditing(false, true);
} }
// FIXME: not working // FIXME: not working
outerClickListener = ((event: Event) => { outerClickListener = ((event: Event) => {
if (!(event.target instanceof HTMLElement && (this._input.contains(event.target) || this._toggle.contains(event.target)))) { if (
this.toggle(!(this._c.clickAwayShouldSave)); !(
event.target instanceof HTMLElement &&
(this._input.contains(event.target) || this._toggle.contains(event.target))
)
) {
this.toggle(!this._c.clickAwayShouldSave);
} }
}).bind(this); }).bind(this);
get editing(): boolean { return this._toggle.classList.contains(HiddenInputField.saveClass); } get editing(): boolean {
set editing(e: boolean) { this.setEditing(e); } return this._toggle.classList.contains(HiddenInputField.saveClass);
}
set editing(e: boolean) {
this.setEditing(e);
}
setEditing(e: boolean, noEvent: boolean = false, noSave: boolean = false) { setEditing(e: boolean, noEvent: boolean = false, noSave: boolean = false) {
if (e) { if (e) {
@@ -84,11 +91,13 @@ export class HiddenInputField {
// done by set value() // done by set value()
// this._content.classList.remove("hidden"); // this._content.classList.remove("hidden");
this._input.classList.add("hidden"); this._input.classList.add("hidden");
if (this.value != this.previous && !noEvent && !noSave) this._c.onSet() if (this.value != this.previous && !noEvent && !noSave) this._c.onSet();
} }
} }
get value(): string { return this._content.textContent; }; get value(): string {
return this._content.textContent;
}
set value(v: string) { set value(v: string) {
this._content.textContent = v; this._content.textContent = v;
this._input.value = v; this._input.value = v;
@@ -96,5 +105,7 @@ export class HiddenInputField {
else this._content.classList.remove("hidden"); else this._content.classList.remove("hidden");
} }
toggle(noSave: boolean = false) { this.setEditing(!this.editing, false, noSave); } toggle(noSave: boolean = false) {
this.setEditing(!this.editing, false, noSave);
}
} }

View File

@@ -13,28 +13,35 @@ export class Updater implements updater {
private _date: number; private _date: number;
updateAvailable = false; updateAvailable = false;
checkForUpdates = (run?: (req: XMLHttpRequest) => void) => _get("/config/update", null, (req: XMLHttpRequest) => { checkForUpdates = (run?: (req: XMLHttpRequest) => void) =>
if (req.readyState == 4) { _get("/config/update", null, (req: XMLHttpRequest) => {
if (req.status != 200) { if (req.readyState == 4) {
window.notifications.customError("errorCheckUpdate", window.lang.notif("errorCheckUpdate")); if (req.status != 200) {
return window.notifications.customError("errorCheckUpdate", window.lang.notif("errorCheckUpdate"));
return;
}
let resp = req.response as updateDTO;
if (resp.new) {
this.update = resp.update;
if (run) {
run(req);
}
// } else {
// window.notifications.customPositive("noUpdatesAvailable", "", window.lang.notif("noUpdatesAvailable"));
}
} }
let resp = req.response as updateDTO; });
if (resp.new) { get date(): number {
this.update = resp.update; return this._date;
if (run) { run(req); } }
// } else {
// window.notifications.customPositive("noUpdatesAvailable", "", window.lang.notif("noUpdatesAvailable"));
}
}
});
get date(): number { return this._date; }
set date(unix: number) { set date(unix: number) {
this._date = unix; this._date = unix;
document.getElementById("update-date").textContent = toDateString(new Date(this._date * 1000)); document.getElementById("update-date").textContent = toDateString(new Date(this._date * 1000));
} }
get description(): string { return this._update.description; } get description(): string {
return this._update.description;
}
set description(description: string) { set description(description: string) {
this._update.description = description; this._update.description = description;
const el = document.getElementById("update-description") as HTMLParagraphElement; const el = document.getElementById("update-description") as HTMLParagraphElement;
@@ -46,35 +53,49 @@ export class Updater implements updater {
} }
} }
get changelog(): string { return this._update.changelog; } get changelog(): string {
return this._update.changelog;
}
set changelog(changelog: string) { set changelog(changelog: string) {
this._update.changelog = changelog; this._update.changelog = changelog;
document.getElementById("update-changelog").innerHTML = Marked.parse(changelog); document.getElementById("update-changelog").innerHTML = Marked.parse(changelog);
} }
get version(): string { return this._update.version; } get version(): string {
return this._update.version;
}
set version(version: string) { set version(version: string) {
this._update.version = version; this._update.version = version;
document.getElementById("update-version").textContent = version; document.getElementById("update-version").textContent = version;
} }
get commit(): string { return this._update.commit; } get commit(): string {
return this._update.commit;
}
set commit(commit: string) { set commit(commit: string) {
this._update.commit = commit; this._update.commit = commit;
document.getElementById("update-commit").textContent = commit.slice(0, 7); document.getElementById("update-commit").textContent = commit.slice(0, 7);
} }
get link(): string { return this._update.link; } get link(): string {
return this._update.link;
}
set link(link: string) { set link(link: string) {
this._update.link = link; this._update.link = link;
(document.getElementById("update-version") as HTMLAnchorElement).href = link; (document.getElementById("update-version") as HTMLAnchorElement).href = link;
} }
get download_link(): string { return this._update.download_link; } get download_link(): string {
set download_link(link: string) { this._update.download_link = link; } return this._update.download_link;
}
set download_link(link: string) {
this._update.download_link = link;
}
get can_update(): boolean { return this._update.can_update; } get can_update(): boolean {
return this._update.can_update;
}
set can_update(can: boolean) { set can_update(can: boolean) {
this._update.can_update = can; this._update.can_update = can;
const download = document.getElementById("update-download") as HTMLSpanElement; const download = document.getElementById("update-download") as HTMLSpanElement;
@@ -89,7 +110,9 @@ export class Updater implements updater {
} }
} }
get update(): Update { return this._update; } get update(): Update {
return this._update;
}
set update(update: Update) { set update(update: Update) {
this._update = update; this._update = update;
this.version = update.version; this.version = update.version;
@@ -106,24 +129,36 @@ export class Updater implements updater {
const update = document.getElementById("update-update") as HTMLSpanElement; const update = document.getElementById("update-update") as HTMLSpanElement;
update.onclick = () => { update.onclick = () => {
toggleLoader(update); toggleLoader(update);
_post("/config/update", null, (req: XMLHttpRequest) => { _post(
if (req.readyState == 4) { "/config/update",
toggleLoader(update); null,
const success = req.response["success"] as Boolean; (req: XMLHttpRequest) => {
if (req.status == 500 && success) { if (req.readyState == 4) {
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh")); toggleLoader(update);
} else if (req.status != 200) { const success = req.response["success"] as Boolean;
window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate")); if (req.status == 500 && success) {
} else { window.notifications.customSuccess(
"applyUpdate",
window.lang.notif("updateAppliedRefresh"),
);
} else if (req.status != 200) {
window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate"));
} else {
window.notifications.customSuccess(
"applyUpdate",
window.lang.notif("updateAppliedRefresh"),
);
}
window.modals.updateInfo.close();
}
},
true,
(req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh")); window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
} }
window.modals.updateInfo.close(); },
} );
}, true, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
}
});
}; };
this.checkForUpdates(() => { this.checkForUpdates(() => {
this.updateAvailable = true; this.updateAvailable = true;

View File

@@ -20,7 +20,7 @@ interface pwValStrings {
lowercase: pwValString; lowercase: pwValString;
number: pwValString; number: pwValString;
special: pwValString; special: pwValString;
[ type: string ]: pwValString; [type: string]: pwValString;
} }
declare var window: valWindow; declare var window: valWindow;
@@ -32,7 +32,9 @@ class Requirement {
private _valid: HTMLSpanElement; private _valid: HTMLSpanElement;
private _li: HTMLLIElement; private _li: HTMLLIElement;
get valid(): boolean { return this._valid.classList.contains("~positive"); } get valid(): boolean {
return this._valid.classList.contains("~positive");
}
set valid(state: boolean) { set valid(state: boolean) {
if (state) { if (state) {
this._valid.classList.add("~positive"); this._valid.classList.add("~positive");
@@ -57,12 +59,14 @@ class Requirement {
if (this._minCount == 1) { if (this._minCount == 1) {
text = window.validationStrings[this._name].singular.replace("{n}", "1"); text = window.validationStrings[this._name].singular.replace("{n}", "1");
} else { } else {
text = window.validationStrings[this._name].plural.replace("{n}", ""+this._minCount); text = window.validationStrings[this._name].plural.replace("{n}", "" + this._minCount);
} }
this._content.textContent = text; this._content.textContent = text;
} }
validate = (count: number) => { this.valid = (count >= this._minCount); } validate = (count: number) => {
this.valid = count >= this._minCount;
};
} }
export interface ValidatorConf { export interface ValidatorConf {
@@ -73,8 +77,12 @@ export interface ValidatorConf {
validatorFunc?: (oncomplete: (valid: boolean) => void) => void; validatorFunc?: (oncomplete: (valid: boolean) => void) => void;
} }
export interface Validation { [name: string]: number } export interface Validation {
export interface Requirements { [category: string]: Requirement }; [name: string]: number;
}
export interface Requirements {
[category: string]: Requirement;
}
export class Validator { export class Validator {
private _conf: ValidatorConf; private _conf: ValidatorConf;
@@ -82,29 +90,29 @@ export class Validator {
private _defaultPwValStrings: pwValStrings = { private _defaultPwValStrings: pwValStrings = {
length: { length: {
singular: "Must have at least {n} character", singular: "Must have at least {n} character",
plural: "Must have at least {n} characters" plural: "Must have at least {n} characters",
}, },
uppercase: { uppercase: {
singular: "Must have at least {n} uppercase character", singular: "Must have at least {n} uppercase character",
plural: "Must have at least {n} uppercase characters" plural: "Must have at least {n} uppercase characters",
}, },
lowercase: { lowercase: {
singular: "Must have at least {n} lowercase character", singular: "Must have at least {n} lowercase character",
plural: "Must have at least {n} lowercase characters" plural: "Must have at least {n} lowercase characters",
}, },
number: { number: {
singular: "Must have at least {n} number", singular: "Must have at least {n} number",
plural: "Must have at least {n} numbers" plural: "Must have at least {n} numbers",
}, },
special: { special: {
singular: "Must have at least {n} special character", singular: "Must have at least {n} special character",
plural: "Must have at least {n} special characters" plural: "Must have at least {n} special characters",
} },
}; };
private _checkPasswords = () => { private _checkPasswords = () => {
return this._conf.passwordField.value == this._conf.rePasswordField.value; return this._conf.passwordField.value == this._conf.rePasswordField.value;
} };
validate = () => { validate = () => {
const pw = this._checkPasswords(); const pw = this._checkPasswords();
@@ -125,51 +133,66 @@ export class Validator {
}); });
}; };
private _isInt = (s: string): boolean => { return (s >= '0' && s <= '9'); } private _isInt = (s: string): boolean => {
return s >= "0" && s <= "9";
};
private _testStrings = (f: pwValString): boolean => { private _testStrings = (f: pwValString): boolean => {
const testString = (s: string): boolean => { const testString = (s: string): boolean => {
if (s == "" || !s.includes("{n}")) { return false; } if (s == "" || !s.includes("{n}")) {
return false;
}
return true; return true;
} };
return testString(f.singular) && testString(f.plural); return testString(f.singular) && testString(f.plural);
} };
private _validate = (s: string): Validation => { private _validate = (s: string): Validation => {
let v: Validation = {}; let v: Validation = {};
for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { v[criteria] = 0; } for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) {
v[criteria] = 0;
}
v["length"] = s.length; v["length"] = s.length;
for (let c of s) { for (let c of s) {
if (this._isInt(c)) { v["number"]++; } if (this._isInt(c)) {
else { v["number"]++;
} else {
const upper = c.toUpperCase(); const upper = c.toUpperCase();
if (upper == c.toLowerCase()) { v["special"]++; } if (upper == c.toLowerCase()) {
else { v["special"]++;
if (upper == c) { v["uppercase"]++; } } else {
else if (upper != c) { v["lowercase"]++; } if (upper == c) {
v["uppercase"]++;
} else if (upper != c) {
v["lowercase"]++;
}
} }
} }
} }
return v return v;
} };
private _bindRequirements = () => { private _bindRequirements = () => {
for (let category in window.validationStrings) { for (let category in window.validationStrings) {
if (!this._testStrings(window.validationStrings[category])) { if (!this._testStrings(window.validationStrings[category])) {
window.validationStrings[category] = this._defaultPwValStrings[category]; window.validationStrings[category] = this._defaultPwValStrings[category];
} }
const el = document.getElementById("requirement-" + category); const el = document.getElementById("requirement-" + category);
if (typeof(el) === 'undefined' || el == null) continue; if (typeof el === "undefined" || el == null) continue;
this._requirements[category] = new Requirement(category, el as HTMLLIElement); this._requirements[category] = new Requirement(category, el as HTMLLIElement);
} }
}; };
get requirements(): Requirements { return this._requirements }; get requirements(): Requirements {
return this._requirements;
}
constructor(conf: ValidatorConf) { constructor(conf: ValidatorConf) {
this._conf = conf; this._conf = conf;
if (!(this._conf.validatorFunc)) { if (!this._conf.validatorFunc) {
this._conf.validatorFunc = (oncomplete: (valid: boolean) => void) => { oncomplete(true); }; this._conf.validatorFunc = (oncomplete: (valid: boolean) => void) => {
oncomplete(true);
};
} }
this._conf.rePasswordField.addEventListener("keyup", this.validate); this._conf.rePasswordField.addEventListener("keyup", this.validate);
this._conf.passwordField.addEventListener("keyup", this.validate); this._conf.passwordField.addEventListener("keyup", this.validate);

View File

@@ -10,7 +10,7 @@ interface formWindow extends Window {
telegramModal: Modal; telegramModal: Modal;
discordModal: Modal; discordModal: Modal;
matrixModal: Modal; matrixModal: Modal;
confirmationModal: Modal confirmationModal: Modal;
code: string; code: string;
messages: { [key: string]: string }; messages: { [key: string]: string };
confirmation: boolean; confirmation: boolean;
@@ -66,7 +66,7 @@ let validatorConf: ValidatorConf = {
rePasswordField: rePasswordField, rePasswordField: rePasswordField,
submitInput: submitInput, submitInput: submitInput,
submitButton: submitSpan, submitButton: submitSpan,
validatorFunc: baseValidator validatorFunc: baseValidator,
}; };
var validator = new Validator(validatorConf); var validator = new Validator(validatorConf);
@@ -90,7 +90,7 @@ form.onsubmit = (event: Event) => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
let send: sendDTO = { let send: sendDTO = {
pin: params.get("pin"), pin: params.get("pin"),
password: passwordField.value password: passwordField.value,
}; };
if (window.captcha) { if (window.captcha) {
if (window.reCAPTCHA) { if (window.reCAPTCHA) {
@@ -99,13 +99,34 @@ form.onsubmit = (event: Event) => {
send.captcha_text = captcha.input.value; send.captcha_text = captcha.input.value;
} }
} }
_post("/reset", send, (req: XMLHttpRequest) => { _post(
if (req.readyState == 4) { "/reset",
removeLoader(submitSpan); send,
if (req.status == 400) { (req: XMLHttpRequest) => {
if (req.response["error"] as string) { if (req.readyState == 4) {
removeLoader(submitSpan);
if (req.status == 400) {
if (req.response["error"] as string) {
const old = submitSpan.textContent;
submitSpan.textContent = window.messages[req.response["error"]];
submitSpan.classList.add("~critical");
submitSpan.classList.remove("~urge");
setTimeout(() => {
submitSpan.classList.add("~urge");
submitSpan.classList.remove("~critical");
submitSpan.textContent = old;
}, 2000);
} else {
for (let type in req.response) {
if (requirements[type]) {
requirements[type].valid = req.response[type] as boolean;
}
}
}
return;
} else if (req.status != 200) {
const old = submitSpan.textContent; const old = submitSpan.textContent;
submitSpan.textContent = window.messages[req.response["error"]]; submitSpan.textContent = window.messages["errorUnknown"];
submitSpan.classList.add("~critical"); submitSpan.classList.add("~critical");
submitSpan.classList.remove("~urge"); submitSpan.classList.remove("~urge");
setTimeout(() => { setTimeout(() => {
@@ -114,26 +135,12 @@ form.onsubmit = (event: Event) => {
submitSpan.textContent = old; submitSpan.textContent = old;
}, 2000); }, 2000);
} else { } else {
for (let type in req.response) { window.successModal.show();
if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; }
}
} }
return;
} else if (req.status != 200) {
const old = submitSpan.textContent;
submitSpan.textContent = window.messages["errorUnknown"];
submitSpan.classList.add("~critical");
submitSpan.classList.remove("~urge");
setTimeout(() => {
submitSpan.classList.add("~urge");
submitSpan.classList.remove("~critical");
submitSpan.textContent = old;
}, 2000);
} else {
window.successModal.show();
} }
} },
}, true); true,
);
}; };
validator.validate(); validator.validate();

View File

@@ -11,13 +11,15 @@ declare var window: sWindow;
const theme = new ThemeManager(document.getElementById("button-theme")); const theme = new ThemeManager(document.getElementById("button-theme"));
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement, 5);
const get = (id: string): HTMLElement => document.getElementById(id); const get = (id: string): HTMLElement => document.getElementById(id);
const text = (id: string, val: string) => { document.getElementById(id).textContent = val; }; const text = (id: string, val: string) => {
const html = (id: string, val: string) => { document.getElementById(id).innerHTML = val; }; document.getElementById(id).textContent = val;
};
const html = (id: string, val: string) => {
document.getElementById(id).innerHTML = val;
};
// FIXME: Reuse setting types from ts/modules/settings.ts // FIXME: Reuse setting types from ts/modules/settings.ts
interface boolEvent extends Event { interface boolEvent extends Event {
@@ -26,18 +28,35 @@ interface boolEvent extends Event {
class Input { class Input {
private _el: HTMLInputElement; private _el: HTMLInputElement;
get value(): string { return ""+this._el.value; } get value(): string {
set value(v: string) { this._el.value = v; } return "" + this._el.value;
}
set value(v: string) {
this._el.value = v;
}
// Nothing depends on input, but we add an empty broadcast function so we can just loop over all settings to fix dependents on start. // Nothing depends on input, but we add an empty broadcast function so we can just loop over all settings to fix dependents on start.
broadcast = () => {} broadcast = () => {};
constructor(el: HTMLElement, placeholder?: any, value?: any, depends?: string, dependsTrue?: boolean, section?: string) { constructor(
el: HTMLElement,
placeholder?: any,
value?: any,
depends?: string,
dependsTrue?: boolean,
section?: string,
) {
this._el = el as HTMLInputElement; this._el = el as HTMLInputElement;
if (placeholder) { this._el.placeholder = placeholder; } if (placeholder) {
if (value) { this.value = value; } this._el.placeholder = placeholder;
}
if (value) {
this.value = value;
}
if (depends) { if (depends) {
document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
let el = this._el as HTMLElement; let el = this._el as HTMLElement;
if (el.parentElement.tagName == "LABEL") { el = el.parentElement; } if (el.parentElement.tagName == "LABEL") {
el = el.parentElement;
}
if (event.detail !== dependsTrue) { if (event.detail !== dependsTrue) {
el.classList.add("unfocused"); el.classList.add("unfocused");
} else { } else {
@@ -51,9 +70,13 @@ class Input {
class Checkbox { class Checkbox {
private _el: HTMLInputElement; private _el: HTMLInputElement;
private _hideEl: HTMLElement; private _hideEl: HTMLElement;
get value(): string { return this._el.checked ? "true" : "false"; } get value(): string {
set value(v: string) { this._el.checked = (v == "true") ? true : false; } return this._el.checked ? "true" : "false";
}
set value(v: string) {
this._el.checked = v == "true" ? true : false;
}
private _section: string; private _section: string;
private _setting: string; private _setting: string;
broadcast = () => { broadcast = () => {
@@ -62,10 +85,10 @@ class Checkbox {
state = false; state = false;
} }
if (this._section && this._setting) { if (this._section && this._setting) {
const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": state }) const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { detail: state });
document.dispatchEvent(ev); document.dispatchEvent(ev);
} }
} };
set onchange(f: () => void) { set onchange(f: () => void) {
this._el.addEventListener("change", f); this._el.addEventListener("change", f);
} }
@@ -85,7 +108,7 @@ class Checkbox {
if (section && setting) { if (section && setting) {
this._section = section; this._section = section;
this._setting = setting; this._setting = setting;
this._el.onchange = this.broadcast; this._el.onchange = this.broadcast;
} }
if (depends) { if (depends) {
document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
@@ -110,21 +133,23 @@ class Checkbox {
class BoolRadios { class BoolRadios {
private _els: NodeListOf<HTMLInputElement>; private _els: NodeListOf<HTMLInputElement>;
get value(): string { return this._els[0].checked ? "true" : "false" } get value(): string {
set value(v: string) { return this._els[0].checked ? "true" : "false";
const bool = (v == "true") ? true : false; }
set value(v: string) {
const bool = v == "true" ? true : false;
this._els[0].checked = bool; this._els[0].checked = bool;
this._els[1].checked = !bool; this._els[1].checked = !bool;
} }
private _section: string; private _section: string;
private _setting: string; private _setting: string;
broadcast = () => { broadcast = () => {
if (this._section && this._setting) { if (this._section && this._setting) {
const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": this._els[0].checked }) const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { detail: this._els[0].checked });
document.dispatchEvent(ev); document.dispatchEvent(ev);
} }
} };
constructor(name: string, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { constructor(name: string, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) {
this._els = document.getElementsByName(name) as NodeListOf<HTMLInputElement>; this._els = document.getElementsByName(name) as NodeListOf<HTMLInputElement>;
if (section && setting) { if (section && setting) {
@@ -177,26 +202,32 @@ class BoolRadios {
class Select { class Select {
private _el: HTMLSelectElement; private _el: HTMLSelectElement;
get value(): string { return this._el.value; } get value(): string {
set value(v: string) { this._el.value = v; } return this._el.value;
}
set value(v: string) {
this._el.value = v;
}
add = (val: string, label: string) => { add = (val: string, label: string) => {
const item = document.createElement("option") as HTMLOptionElement; const item = document.createElement("option") as HTMLOptionElement;
item.value = val; item.value = val;
item.textContent = label; item.textContent = label;
this._el.appendChild(item); this._el.appendChild(item);
} };
set onchange(f: () => void) { set onchange(f: () => void) {
this._el.addEventListener("change", f); this._el.addEventListener("change", f);
} }
private _section: string; private _section: string;
private _setting: string; private _setting: string;
broadcast = () => { broadcast = () => {
if (this._section && this._setting) { if (this._section && this._setting) {
const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": this.value ? true : false }) const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, {
detail: this.value ? true : false,
});
document.dispatchEvent(ev); document.dispatchEvent(ev);
} }
} };
constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) {
this._el = el as HTMLSelectElement; this._el = el as HTMLSelectElement;
if (section && setting) { if (section && setting) {
@@ -221,137 +252,194 @@ class Select {
} }
class LangSelect extends Select { class LangSelect extends Select {
constructor(page: string, el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { constructor(
page: string,
el: HTMLElement,
depends?: string,
dependsTrue?: boolean,
section?: string,
setting?: string,
) {
super(el, depends, dependsTrue, section, setting); super(el, depends, dependsTrue, section, setting);
_get("/lang/" + page, null, (req: XMLHttpRequest) => { _get(
if (req.readyState == 4 && req.status == 200) { "/lang/" + page,
for (let code in req.response) { null,
this.add(code, req.response[code]); (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) {
for (let code in req.response) {
this.add(code, req.response[code]);
}
this.value = "en-us";
} }
this.value = "en-us"; },
} true,
}, true); );
} }
} }
const replaceLink = (elName: string, sect: string, name: string, url: string, text: string) => html(elName, window.lang.var(sect, name, `<a class="underline" target="_blank" href="${url}">${text}</a>`)); const replaceLink = (elName: string, sect: string, name: string, url: string, text: string) =>
html(elName, window.lang.var(sect, name, `<a class="underline" target="_blank" href="${url}">${text}</a>`));
window.lang = new lang(window.langFile as LangFile); window.lang = new lang(window.langFile as LangFile);
replaceLink("language-description", "language", "description", "https://weblate.jfa-go.com", "Weblate"); replaceLink("language-description", "language", "description", "https://weblate.jfa-go.com", "Weblate");
replaceLink("email-description", "email", "description", "https://mailgun.com", "Mailgun"); replaceLink("email-description", "email", "description", "https://mailgun.com", "Mailgun");
replaceLink("email-dateformat-notice", "email", "dateFormatNotice", "https://strftime.timpetricola.com/", "strftime.timpetricola.com"); replaceLink(
"email-dateformat-notice",
"email",
"dateFormatNotice",
"https://strftime.timpetricola.com/",
"strftime.timpetricola.com",
);
replaceLink("updates-description", "updates", "description", "https://builds.hrfee.dev/view/hrfee/jfa-go", "buildrone"); replaceLink("updates-description", "updates", "description", "https://builds.hrfee.dev/view/hrfee/jfa-go", "buildrone");
replaceLink("messages-description", "messages", "description", "https://wiki.jfa-go.com", "Wiki"); replaceLink("messages-description", "messages", "description", "https://wiki.jfa-go.com", "Wiki");
replaceLink("password_resets-more-info", "passwordResets", "moreInfo", "https://wiki.jfa-go.com/docs/pwr/", "wiki.jfa-go.com"); replaceLink(
replaceLink("ombi-stability-warning", "ombi", "stabilityWarning", "https://wiki.jfa-go.com/docs/ombi/", "wiki.jfa-go.com"); "password_resets-more-info",
"passwordResets",
"moreInfo",
"https://wiki.jfa-go.com/docs/pwr/",
"wiki.jfa-go.com",
);
replaceLink(
"ombi-stability-warning",
"ombi",
"stabilityWarning",
"https://wiki.jfa-go.com/docs/ombi/",
"wiki.jfa-go.com",
);
const settings = { const settings = {
"jellyfin": { jellyfin: {
"type": new Select(get("jellyfin-type")), type: new Select(get("jellyfin-type")),
"server": new Input(get("jellyfin-server")), server: new Input(get("jellyfin-server")),
"public_server": new Input(get("jellyfin-public_server")), public_server: new Input(get("jellyfin-public_server")),
"username": new Input(get("jellyfin-username")), username: new Input(get("jellyfin-username")),
"password": new Input(get("jellyfin-password")), password: new Input(get("jellyfin-password")),
"substitute_jellyfin_strings": new Input(get("jellyfin-substitute_jellyfin_strings")) substitute_jellyfin_strings: new Input(get("jellyfin-substitute_jellyfin_strings")),
}, },
"updates": { updates: {
"enabled": new Checkbox(get("updates-enabled"), "", false, "updates", "enabled"), enabled: new Checkbox(get("updates-enabled"), "", false, "updates", "enabled"),
"channel": new Select(get("updates-channel"), "enabled", true, "updates") channel: new Select(get("updates-channel"), "enabled", true, "updates"),
}, },
"ui": { ui: {
"host": new Input(get("ui-host")), host: new Input(get("ui-host")),
"port": new Input(get("ui-port")), port: new Input(get("ui-port")),
"url_base": new Input(get("ui-url_base")), url_base: new Input(get("ui-url_base")),
"jfa_url": new Input(get("ui-jfa_url")), jfa_url: new Input(get("ui-jfa_url")),
"theme": new Select(get("ui-theme")), theme: new Select(get("ui-theme")),
"language-form": new LangSelect("form", get("ui-language-form")), "language-form": new LangSelect("form", get("ui-language-form")),
"language-admin": new LangSelect("admin", get("ui-language-admin")), "language-admin": new LangSelect("admin", get("ui-language-admin")),
"jellyfin_login": new BoolRadios("ui-jellyfin_login", "", false, "ui", "jellyfin_login"), jellyfin_login: new BoolRadios("ui-jellyfin_login", "", false, "ui", "jellyfin_login"),
"admin_only": new Checkbox(get("ui-admin_only"), "jellyfin_login", true, "ui"), admin_only: new Checkbox(get("ui-admin_only"), "jellyfin_login", true, "ui"),
"allow_all": new Checkbox(get("ui-allow_all"), "jellyfin_login", true, "ui"), allow_all: new Checkbox(get("ui-allow_all"), "jellyfin_login", true, "ui"),
"username": new Input(get("ui-username"), "", "", "jellyfin_login", false, "ui"), username: new Input(get("ui-username"), "", "", "jellyfin_login", false, "ui"),
"password": new Input(get("ui-password"), "", "", "jellyfin_login", false, "ui"), password: new Input(get("ui-password"), "", "", "jellyfin_login", false, "ui"),
"email": new Input(get("ui-email"), "", "", "jellyfin_login", false, "ui"), email: new Input(get("ui-email"), "", "", "jellyfin_login", false, "ui"),
"contact_message": new Input(get("ui-contact_message"), window.messages["ui"]["contact_message"]), contact_message: new Input(get("ui-contact_message"), window.messages["ui"]["contact_message"]),
"help_message": new Input(get("ui-help_message"), window.messages["ui"]["help_message"]), help_message: new Input(get("ui-help_message"), window.messages["ui"]["help_message"]),
"success_message": new Input(get("ui-success_message"), window.messages["ui"]["success_message"]) success_message: new Input(get("ui-success_message"), window.messages["ui"]["success_message"]),
}, },
"password_validation": { password_validation: {
"enabled": new Checkbox(get("password_validation-enabled"), "", false, "password_validation", "enabled"), enabled: new Checkbox(get("password_validation-enabled"), "", false, "password_validation", "enabled"),
"min_length": new Input(get("password_validation-min_length"), "", 8, "enabled", true, "password_validation"), min_length: new Input(get("password_validation-min_length"), "", 8, "enabled", true, "password_validation"),
"upper": new Input(get("password_validation-upper"), "", 1, "enabled", true, "password_validation"), upper: new Input(get("password_validation-upper"), "", 1, "enabled", true, "password_validation"),
"lower": new Input(get("password_validation-lower"), "", 0, "enabled", true, "password_validation"), lower: new Input(get("password_validation-lower"), "", 0, "enabled", true, "password_validation"),
"number": new Input(get("password_validation-number"), "", 1, "enabled", true, "password_validation"), number: new Input(get("password_validation-number"), "", 1, "enabled", true, "password_validation"),
"special": new Input(get("password_validation-special"), "", 0, "enabled", true, "password_validation") special: new Input(get("password_validation-special"), "", 0, "enabled", true, "password_validation"),
}, },
"messages": { messages: {
"enabled": new Checkbox(get("messages-enabled"), "", false, "messages", "enabled"), enabled: new Checkbox(get("messages-enabled"), "", false, "messages", "enabled"),
"use_24h": new BoolRadios("email-24h", "enabled", true, "messages"), use_24h: new BoolRadios("email-24h", "enabled", true, "messages"),
"date_format": new Input(get("email-date_format"), "", "%d/%m/%y", "enabled", true, "messages"), date_format: new Input(get("email-date_format"), "", "%d/%m/%y", "enabled", true, "messages"),
"message": new Input(get("email-message"), window.messages["messages"]["message"], "", "enabled", true, "messages") message: new Input(
get("email-message"),
window.messages["messages"]["message"],
"",
"enabled",
true,
"messages",
),
}, },
"email": { email: {
"language": new LangSelect("email", get("email-language")), language: new LangSelect("email", get("email-language")),
"no_username": new Checkbox(get("email-no_username"), "method", true, "email"), no_username: new Checkbox(get("email-no_username"), "method", true, "email"),
"method": new Select(get("email-method"), "", false, "email", "method"), method: new Select(get("email-method"), "", false, "email", "method"),
"address": new Input(get("email-address"), "jellyfin@jellyf.in", "", "method", true, "email"), address: new Input(get("email-address"), "jellyfin@jellyf.in", "", "method", true, "email"),
"from": new Input(get("email-from"), "", "Jellyfin", "method", true, "email") from: new Input(get("email-from"), "", "Jellyfin", "method", true, "email"),
}, },
"password_resets": { password_resets: {
"enabled": new Checkbox(get("password_resets-enabled"), "", false, "password_resets", "enabled"), enabled: new Checkbox(get("password_resets-enabled"), "", false, "password_resets", "enabled"),
"watch_directory": new Input(get("password_resets-watch_directory"), "", "", "enabled", true, "password_resets"), watch_directory: new Input(get("password_resets-watch_directory"), "", "", "enabled", true, "password_resets"),
"subject": new Input(get("password_resets-subject"), "", "", "enabled", true, "password_resets"), subject: new Input(get("password_resets-subject"), "", "", "enabled", true, "password_resets"),
"link_reset": new Checkbox(get("password_resets-link_reset"), "enabled", true, "password_resets", "link_reset"), link_reset: new Checkbox(get("password_resets-link_reset"), "enabled", true, "password_resets", "link_reset"),
"language": new LangSelect("pwr", get("password_resets-language"), "link_reset", true, "password_resets", "language"), language: new LangSelect(
"set_password": new Checkbox(get("password_resets-set_password"), "link_reset", true, "password_resets", "set_password") "pwr",
get("password_resets-language"),
"link_reset",
true,
"password_resets",
"language",
),
set_password: new Checkbox(
get("password_resets-set_password"),
"link_reset",
true,
"password_resets",
"set_password",
),
}, },
"notifications": { notifications: {
"enabled": new Checkbox(get("notifications-enabled")) enabled: new Checkbox(get("notifications-enabled")),
}, },
"user_page": { user_page: {
"enabled": new Checkbox(get("userpage-enabled")) enabled: new Checkbox(get("userpage-enabled")),
}, },
"welcome_email": { welcome_email: {
"enabled": new Checkbox(get("welcome_email-enabled"), "", false, "welcome_email", "enabled"), enabled: new Checkbox(get("welcome_email-enabled"), "", false, "welcome_email", "enabled"),
"subject": new Input(get("welcome_email-subject"), "", "", "enabled", true, "welcome_email") subject: new Input(get("welcome_email-subject"), "", "", "enabled", true, "welcome_email"),
}, },
"invite_emails": { invite_emails: {
"enabled": new Checkbox(get("invite_emails-enabled"), "", false, "invite_emails", "enabled"), enabled: new Checkbox(get("invite_emails-enabled"), "", false, "invite_emails", "enabled"),
"subject": new Input(get("invite_emails-subject"), "", "", "enabled", true, "invite_emails"), subject: new Input(get("invite_emails-subject"), "", "", "enabled", true, "invite_emails"),
}, },
"mailgun": { mailgun: {
"api_url": new Input(get("mailgun-api_url")), api_url: new Input(get("mailgun-api_url")),
"api_key": new Input(get("mailgun-api_key")) api_key: new Input(get("mailgun-api_key")),
}, },
"smtp": { smtp: {
"username": new Input(get("smtp-username")), username: new Input(get("smtp-username")),
"encryption": new Select(get("smtp-encryption")), encryption: new Select(get("smtp-encryption")),
"server": new Input(get("smtp-server")), server: new Input(get("smtp-server")),
"port": new Input(get("smtp-port")), port: new Input(get("smtp-port")),
"password": new Input(get("smtp-password")) password: new Input(get("smtp-password")),
}, },
"ombi": { ombi: {
"enabled": new Checkbox(get("ombi-enabled"), "", false, "ombi", "enabled"), enabled: new Checkbox(get("ombi-enabled"), "", false, "ombi", "enabled"),
"server": new Input(get("ombi-server"), "", "", "enabled", true, "ombi"), server: new Input(get("ombi-server"), "", "", "enabled", true, "ombi"),
"api_key": new Input(get("ombi-api_key"), "", "", "enabled", true, "ombi") api_key: new Input(get("ombi-api_key"), "", "", "enabled", true, "ombi"),
}, },
"jellyseerr": { jellyseerr: {
"enabled": new Checkbox(get("jellyseerr-enabled"), "", false, "jellyseerr", "enabled"), enabled: new Checkbox(get("jellyseerr-enabled"), "", false, "jellyseerr", "enabled"),
"server": new Input(get("jellyseerr-server"), "", "", "enabled", true, "jellyseerr"), server: new Input(get("jellyseerr-server"), "", "", "enabled", true, "jellyseerr"),
"api_key": new Input(get("jellyseerr-api_key"), "", "", "enabled", true, "jellyseerr"), api_key: new Input(get("jellyseerr-api_key"), "", "", "enabled", true, "jellyseerr"),
"import_existing": new Checkbox(get("jellyseerr-import_existing"), "enabled", true, "jellyseerr", "import_existing") import_existing: new Checkbox(
get("jellyseerr-import_existing"),
"enabled",
true,
"jellyseerr",
"import_existing",
),
},
advanced: {
tls: new Checkbox(get("advanced-tls"), "", false, "advanced", "tls"),
tls_port: new Input(get("advanced-tls_port"), "", "", "tls", true, "advanced"),
tls_cert: new Input(get("advanced-tls_cert"), "", "", "tls", true, "advanced"),
tls_key: new Input(get("advanced-tls_key"), "", "", "tls", true, "advanced"),
proxy: new Checkbox(get("advanced-proxy"), "", false, "advanced", "proxy"),
proxy_protocol: new Select(get("advanced-proxy_protocol"), "proxy", true, "advanced"),
proxy_address: new Input(get("advanced-proxy_address"), "", "", "proxy", true, "advanced"),
proxy_user: new Input(get("advanced-proxy_user"), "", "", "proxy", true, "advanced"),
proxy_password: new Input(get("advanced-proxy_password"), "", "", "proxy", true, "advanced"),
}, },
"advanced": {
"tls": new Checkbox(get("advanced-tls"), "", false, "advanced", "tls"),
"tls_port": new Input(get("advanced-tls_port"), "", "", "tls", true, "advanced"),
"tls_cert": new Input(get("advanced-tls_cert"), "", "", "tls", true, "advanced"),
"tls_key": new Input(get("advanced-tls_key"), "", "", "tls", true, "advanced"),
"proxy": new Checkbox(get("advanced-proxy"), "", false, "advanced", "proxy"),
"proxy_protocol": new Select(get("advanced-proxy_protocol"), "proxy", true, "advanced"),
"proxy_address": new Input(get("advanced-proxy_address"), "", "", "proxy", true, "advanced"),
"proxy_user": new Input(get("advanced-proxy_user"), "", "", "proxy", true, "advanced"),
"proxy_password": new Input(get("advanced-proxy_password"), "", "", "proxy", true, "advanced")
}
}; };
const checkTheme = () => { const checkTheme = () => {
if (settings["ui"]["theme"].value.includes("Dark")) { if (settings["ui"]["theme"].value.includes("Dark")) {
@@ -366,7 +454,7 @@ settings["ui"]["theme"].onchange = checkTheme;
checkTheme(); checkTheme();
const fixFullURL = (v: string): string => { const fixFullURL = (v: string): string => {
if (!(v.startsWith("http://")) && !(v.startsWith("https://"))) { if (!v.startsWith("http://") && !v.startsWith("https://")) {
v = "http://" + v; v = "http://" + v;
} }
return v; return v;
@@ -374,9 +462,11 @@ const fixFullURL = (v: string): string => {
const formatSubpath = (v: string): string => { const formatSubpath = (v: string): string => {
if (v == "/") return ""; if (v == "/") return "";
if (v.charAt(-1) == "/") { v = v.slice(0, -1); } if (v.charAt(-1) == "/") {
v = v.slice(0, -1);
}
return v; return v;
} };
const constructNewURLs = (): string[] => { const constructNewURLs = (): string[] => {
let local = settings["ui"]["host"].value + ":" + settings["ui"]["port"].value; let local = settings["ui"]["host"].value + ":" + settings["ui"]["port"].value;
@@ -390,7 +480,7 @@ const constructNewURLs = (): string[] => {
} }
remote = fixFullURL(remote); remote = fixFullURL(remote);
return [local, remote]; return [local, remote];
} };
const restartButton = document.getElementById("restart") as HTMLSpanElement; const restartButton = document.getElementById("restart") as HTMLSpanElement;
const serialize = () => { const serialize = () => {
@@ -405,54 +495,63 @@ const serialize = () => {
} }
} }
config["restart-program"] = true; config["restart-program"] = true;
_post("/config", config, (req: XMLHttpRequest) => { _post(
if (req.readyState == 4) { "/config",
toggleLoader(restartButton); config,
if (req.status == 500) { (req: XMLHttpRequest) => {
if (req.response == null) { if (req.readyState == 4) {
const old = restartButton.textContent; toggleLoader(restartButton);
restartButton.classList.add("~critical"); if (req.status == 500) {
restartButton.classList.remove("~urge"); if (req.response == null) {
restartButton.textContent = window.lang.strings("errorUnknown"); const old = restartButton.textContent;
setTimeout(() => { restartButton.classList.add("~critical");
restartButton.classList.add("~urge"); restartButton.classList.remove("~urge");
restartButton.classList.remove("~critical"); restartButton.textContent = window.lang.strings("errorUnknown");
restartButton.textContent = old; setTimeout(() => {
}, 5000); restartButton.classList.add("~urge");
return; restartButton.classList.remove("~critical");
} restartButton.textContent = old;
if (req.response["error"] as string) { }, 5000);
const old = restartButton.textContent; return;
restartButton.classList.add("~critical"); }
restartButton.classList.remove("~urge"); if (req.response["error"] as string) {
restartButton.textContent = req.response["error"]; const old = restartButton.textContent;
setTimeout(() => { restartButton.classList.add("~critical");
restartButton.classList.add("~urge"); restartButton.classList.remove("~urge");
restartButton.classList.remove("~critical"); restartButton.textContent = req.response["error"];
restartButton.textContent = old; setTimeout(() => {
}, 5000); restartButton.classList.add("~urge");
return; restartButton.classList.remove("~critical");
restartButton.textContent = old;
}, 5000);
return;
}
} }
restartButton.parentElement.querySelector("span.back").classList.add("unfocused");
restartButton.classList.add("unfocused");
const refreshURLs = constructNewURLs();
const refreshButtons = [
document.getElementById("refresh-internal") as HTMLAnchorElement,
document.getElementById("refresh-external") as HTMLAnchorElement,
];
["internal", "external"].forEach((urltype, i) => {
const button = refreshButtons[i];
button.classList.remove("unfocused");
button.href = refreshURLs[i];
button.innerHTML = `<span>${urltype.charAt(0).toUpperCase() + urltype.slice(1)}:</span><i class="italic underline">${button.href}</i>`;
// skip external if it isn't set
if (refreshURLs.length == 1) return;
});
} }
restartButton.parentElement.querySelector("span.back").classList.add("unfocused"); },
restartButton.classList.add("unfocused"); true,
const refreshURLs = constructNewURLs(); (req: XMLHttpRequest) => {
const refreshButtons = [document.getElementById("refresh-internal") as HTMLAnchorElement, document.getElementById("refresh-external") as HTMLAnchorElement]; if (req.status == 0) {
["internal", "external"].forEach((urltype, i) => { window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused"));
const button = refreshButtons[i]; }
button.classList.remove("unfocused"); },
button.href = refreshURLs[i]; );
button.innerHTML = `<span>${urltype.charAt(0).toUpperCase() + urltype.slice(1)}:</span><i class="italic underline">${button.href}</i>`; };
// skip external if it isn't set
if (refreshURLs.length == 1) return;
});
}
}, true, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused"));
}
});
}
restartButton.onclick = serialize; restartButton.onclick = serialize;
const relatedToEmail = Array.from(document.getElementsByClassName("related-to-email")); const relatedToEmail = Array.from(document.getElementsByClassName("related-to-email"));
@@ -503,7 +602,7 @@ const getParentCard = (el: HTMLElement): HTMLDivElement => {
const jellyfinLoginAccessChange = () => { const jellyfinLoginAccessChange = () => {
const adminOnly = settings["ui"]["admin_only"].value == "true"; const adminOnly = settings["ui"]["admin_only"].value == "true";
const allowAll = settings["ui"]["allow_all"].value == "true"; const allowAll = settings["ui"]["allow_all"].value == "true";
const adminOnlyEl = document.getElementById("ui-admin_only") as HTMLInputElement; const adminOnlyEl = document.getElementById("ui-admin_only") as HTMLInputElement;
const allowAllEl = document.getElementById("ui-allow_all") as HTMLInputElement; const allowAllEl = document.getElementById("ui-allow_all") as HTMLInputElement;
const nextButton = getParentCard(adminOnlyEl).querySelector("span.next") as HTMLSpanElement; const nextButton = getParentCard(adminOnlyEl).querySelector("span.next") as HTMLSpanElement;
@@ -515,10 +614,10 @@ const jellyfinLoginAccessChange = () => {
adminOnlyEl.disabled = true; adminOnlyEl.disabled = true;
allowAllEl.disabled = false; allowAllEl.disabled = false;
nextButton.removeAttribute("disabled"); nextButton.removeAttribute("disabled");
} else { } else {
adminOnlyEl.disabled = false; adminOnlyEl.disabled = false;
allowAllEl.disabled = false; allowAllEl.disabled = false;
nextButton.setAttribute("disabled", "true") nextButton.setAttribute("disabled", "true");
} }
}; };
@@ -534,7 +633,7 @@ const embyHidePWR = () => {
} else if (val == "emby") { } else if (val == "emby") {
pwr.classList.add("hidden"); pwr.classList.add("hidden");
} }
} };
settings["jellyfin"]["type"].onchange = embyHidePWR; settings["jellyfin"]["type"].onchange = embyHidePWR;
embyHidePWR(); embyHidePWR();
@@ -552,7 +651,9 @@ let pages = new PageManager({
defaultTitle: "Setup - jfa-go", defaultTitle: "Setup - jfa-go",
}); });
const cards = Array.from(document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned")) as Array<HTMLDivElement>; const cards = Array.from(
document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned"),
) as Array<HTMLDivElement>;
(window as any).cards = cards; (window as any).cards = cards;
(() => { (() => {
@@ -582,10 +683,11 @@ const cards = Array.from(document.getElementsByClassName("page-container")[0].qu
}, },
}); });
if (back) back.addEventListener("click", () => pages.prev(title)); if (back) back.addEventListener("click", () => pages.prev(title));
if (next) next.addEventListener("click", () => { if (next)
if (next.hasAttribute("disabled")) return; next.addEventListener("click", () => {
pages.next(title); if (next.hasAttribute("disabled")) return;
}); pages.next(title);
});
} }
})(); })();
@@ -596,51 +698,57 @@ const cards = Array.from(document.getElementsByClassName("page-container")[0].qu
button.onclick = () => { button.onclick = () => {
toggleLoader(button); toggleLoader(button);
let send = { let send = {
"type": settings["jellyfin"]["type"].value, type: settings["jellyfin"]["type"].value,
"server": settings["jellyfin"]["server"].value, server: settings["jellyfin"]["server"].value,
"username": settings["jellyfin"]["username"].value, username: settings["jellyfin"]["username"].value,
"password": settings["jellyfin"]["password"].value, password: settings["jellyfin"]["password"].value,
"proxy": settings["advanced"]["proxy"].value == "true", proxy: settings["advanced"]["proxy"].value == "true",
"proxy_protocol": settings["advanced"]["proxy_protocol"].value, proxy_protocol: settings["advanced"]["proxy_protocol"].value,
"proxy_address": settings["advanced"]["proxy_address"].value, proxy_address: settings["advanced"]["proxy_address"].value,
"proxy_user": settings["advanced"]["proxy_user"].value, proxy_user: settings["advanced"]["proxy_user"].value,
"proxy_password": settings["advanced"]["proxy_password"].value proxy_password: settings["advanced"]["proxy_password"].value,
}; };
_post("/jellyfin/test", send, (req: XMLHttpRequest) => { _post(
if (req.readyState == 4) { "/jellyfin/test",
toggleLoader(button); send,
if (req.status != 200) { (req: XMLHttpRequest) => {
nextButton.setAttribute("disabled", ""); if (req.readyState == 4) {
button.classList.add("~critical"); toggleLoader(button);
if (req.status != 200) {
nextButton.setAttribute("disabled", "");
button.classList.add("~critical");
button.classList.remove("~urge");
setTimeout(() => {
button.textContent = ogText;
button.classList.add("~urge");
button.classList.remove("~critical");
}, 5000);
const errorMsg = req.response["error"] as string;
if (!errorMsg) {
button.textContent = window.lang.strings("error");
} else {
button.textContent = window.lang.strings(errorMsg);
}
return;
}
nextButton.removeAttribute("disabled");
button.textContent = window.lang.strings("success");
button.classList.add("~positive");
button.classList.remove("~urge"); button.classList.remove("~urge");
setTimeout(() => { setTimeout(() => {
button.textContent = ogText; button.textContent = ogText;
button.classList.add("~urge"); button.classList.add("~urge");
button.classList.remove("~critical"); button.classList.remove("~positive");
}, 5000); }, 5000);
const errorMsg = req.response["error"] as string;
if (!errorMsg) {
button.textContent = window.lang.strings("error");
} else {
button.textContent = window.lang.strings(errorMsg);
}
return;
} }
nextButton.removeAttribute("disabled"); },
button.textContent = window.lang.strings("success"); true,
button.classList.add("~positive"); (req: XMLHttpRequest) => {
button.classList.remove("~urge"); if (req.status == 0) {
setTimeout(() => { window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused"));
button.textContent = ogText; }
button.classList.add("~urge"); },
button.classList.remove("~positive"); );
}, 5000);
}
}, true, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.customError("connectionError", window.lang.strings("errorConnectionRefused"));
}
});
}; };
})(); })();

View File

@@ -1,6 +1,6 @@
declare interface Modal { declare interface Modal {
modal: HTMLElement; modal: HTMLElement;
closeButton: HTMLSpanElement closeButton: HTMLSpanElement;
show: () => void; show: () => void;
close: (event?: Event, noDispatch?: boolean) => void; close: (event?: Event, noDispatch?: boolean) => void;
toggle: () => void; toggle: () => void;
@@ -58,12 +58,12 @@ declare interface GlobalWindow extends Window {
jfAdminOnly: boolean; jfAdminOnly: boolean;
jfAllowAll: boolean; jfAllowAll: boolean;
referralsEnabled: boolean; referralsEnabled: boolean;
loginAppearance: string; loginAppearance: string;
} }
declare interface InviteList { declare interface InviteList {
empty: boolean; empty: boolean;
invites: { [code: string]: Invite } invites: { [code: string]: Invite };
add: (invite: Invite) => void; add: (invite: Invite) => void;
reload: (callback?: () => void) => void; reload: (callback?: () => void) => void;
isInviteURL: () => boolean; isInviteURL: () => boolean;
@@ -71,25 +71,25 @@ declare interface InviteList {
} }
declare interface Invite { declare interface Invite {
code: string; // Invite code code: string; // Invite code
valid_till: number; // Unix timestamp of expiry valid_till: number; // Unix timestamp of expiry
user_expiry: boolean; // Whether or not user expiry is enabled user_expiry: boolean; // Whether or not user expiry is enabled
user_months?: number; // Number of months till user expiry user_months?: number; // Number of months till user expiry
user_days?: number; // Number of days till user expiry user_days?: number; // Number of days till user expiry
user_hours?: number; // Number of hours till user expiry user_hours?: number; // Number of hours till user expiry
user_minutes?: number; // Number of minutes till user expiry user_minutes?: number; // Number of minutes till user expiry
created: number; // Date of creation (unix timestamp) created: number; // Date of creation (unix timestamp)
profile: string; // Profile used on this invite profile: string; // Profile used on this invite
used_by?: { [user: string]: number }; // Users who have used this invite mapped to their creation time in Epoch/Unix time used_by?: { [user: string]: number }; // Users who have used this invite mapped to their creation time in Epoch/Unix time
no_limit: boolean; // If true, invite can be used any number of times no_limit: boolean; // If true, invite can be used any number of times
remaining_uses?: number; // Remaining number of uses (if applicable) remaining_uses?: number; // Remaining number of uses (if applicable)
send_to?: string; // DEPRECATED Email/Discord username the invite was sent to (if applicable) send_to?: string; // DEPRECATED Email/Discord username the invite was sent to (if applicable)
sent_to?: SentToList; // Email/Discord usernames attempts were made to send this invite to, and a failure reason if failed. sent_to?: SentToList; // Email/Discord usernames attempts were made to send this invite to, and a failure reason if failed.
notify_expiry?: boolean; // Whether to notify the requesting user of expiry or not notify_expiry?: boolean; // Whether to notify the requesting user of expiry or not
notify_creation?: boolean; // Whether to notify the requesting user of account creation or not notify_creation?: boolean; // Whether to notify the requesting user of account creation or not
label?: string; // Optional label for the invite label?: string; // Optional label for the invite
user_label?: string; // Label to apply to users created w/ this invite. user_label?: string; // Label to apply to users created w/ this invite.
} }
declare interface SendFailure { declare interface SendFailure {
@@ -103,9 +103,9 @@ declare interface SentToList {
} }
declare interface Update { declare interface Update {
version: string; version: string;
commit: string; commit: string;
date: number; date: number;
description: string; description: string;
changelog: string; changelog: string;
link: string; link: string;
@@ -131,7 +131,7 @@ declare interface Lang {
declare interface NotificationBox { declare interface NotificationBox {
connectionError: () => void; connectionError: () => void;
customError: (type: string, message: string) => void; customError: (type: string, message: string) => void;
customPositive: (type: string, bold: string, message: string) => void; customPositive: (type: string, bold: string, message: string) => void;
customSuccess: (type: string, message: string) => void; customSuccess: (type: string, message: string) => void;
} }
@@ -182,7 +182,7 @@ interface PaginatedReqDTO {
page: number; page: number;
sortByField: string; sortByField: string;
ascending: boolean; ascending: boolean;
}; }
interface DateAttempt { interface DateAttempt {
year?: number; year?: number;
@@ -198,7 +198,7 @@ interface ParsedDate {
date: Date; date: Date;
text: string; text: string;
invalid?: boolean; invalid?: boolean;
}; }
declare var config: Object; declare var config: Object;
declare var modifiedConfig: Object; declare var modifiedConfig: Object;

View File

@@ -1,7 +1,17 @@
import { ThemeManager } from "./modules/theme.js"; import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, addLoader, removeLoader, toClipboard } from "./modules/common.js"; import {
_get,
_post,
_delete,
notificationBox,
whichAnimationEvent,
toDateString,
addLoader,
removeLoader,
toClipboard,
} from "./modules/common.js";
import { Login } from "./modules/login.js"; import { Login } from "./modules/login.js";
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
@@ -77,7 +87,7 @@ pages.setPage({
pages.setPage({ pages.setPage({
name: "reset", name: "reset",
title: document.title, title: document.title,
url: basePath+"/password/reset", url: basePath + "/password/reset",
show: () => { show: () => {
const usernameInput = document.getElementById("login-user") as HTMLInputElement; const usernameInput = document.getElementById("login-user") as HTMLInputElement;
const input = document.getElementById("pwr-address") as HTMLInputElement; const input = document.getElementById("pwr-address") as HTMLInputElement;
@@ -98,11 +108,11 @@ pages.setPage({
const resetButton = document.getElementById("modal-login-pwr"); const resetButton = document.getElementById("modal-login-pwr");
resetButton.onclick = () => { resetButton.onclick = () => {
pages.load("reset"); pages.load("reset");
} };
} }
})(); })();
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement, 5);
if (window.pwrEnabled && window.linkResetEnabled) { if (window.pwrEnabled && window.linkResetEnabled) {
const submitButton = document.getElementById("pwr-submit"); const submitButton = document.getElementById("pwr-submit");
@@ -113,7 +123,7 @@ if (window.pwrEnabled && window.linkResetEnabled) {
if (req.readyState != 4) return; if (req.readyState != 4) return;
removeLoader(submitButton); removeLoader(submitButton);
if (req.status != 204) { if (req.status != 204) {
window.notifications.customError("unkownError", window.lang.notif("errorUnknown"));; window.notifications.customError("unkownError", window.lang.notif("errorUnknown"));
window.modals.pwr.close(); window.modals.pwr.close();
return; return;
} }
@@ -168,9 +178,9 @@ interface ContactDTO {
class ContactMethods { class ContactMethods {
private _card: HTMLElement; private _card: HTMLElement;
private _content: HTMLElement; private _content: HTMLElement;
private _buttons: { [name: string]: { element: HTMLElement, details: MyDetailsContactMethod } }; private _buttons: { [name: string]: { element: HTMLElement; details: MyDetailsContactMethod } };
constructor (card: HTMLElement) { constructor(card: HTMLElement) {
this._card = card; this._card = card;
this._content = this._card.querySelector(".content"); this._content = this._card.querySelector(".content");
this._buttons = {}; this._buttons = {};
@@ -179,9 +189,15 @@ class ContactMethods {
clear = () => { clear = () => {
this._content.textContent = ""; this._content.textContent = "";
this._buttons = {}; this._buttons = {};
} };
append = (name: string, details: MyDetailsContactMethod, icon: string, addEditFunc?: (add: boolean) => void, required?: boolean) => { append = (
name: string,
details: MyDetailsContactMethod,
icon: string,
addEditFunc?: (add: boolean) => void,
required?: boolean,
) => {
const row = document.createElement("div"); const row = document.createElement("div");
row.classList.add("flex", "flex-row", "justify-between", "gap-2", "flex-nowrap"); row.classList.add("flex", "flex-row", "justify-between", "gap-2", "flex-nowrap");
let innerHTML = ` let innerHTML = `
@@ -191,7 +207,7 @@ class ContactMethods {
${icon} ${icon}
</span> </span>
</span> </span>
<span class="font-bold text-ellipsis overflow-hidden">${(details.value == "") ? window.lang.strings("notSet") : details.value}</span> <span class="font-bold text-ellipsis overflow-hidden">${details.value == "" ? window.lang.strings("notSet") : details.value}</span>
</div> </div>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<div class="flex items-center flex-row gap-2"> <div class="flex items-center flex-row gap-2">
@@ -224,12 +240,12 @@ class ContactMethods {
`; `;
row.innerHTML = innerHTML; row.innerHTML = innerHTML;
this._buttons[name] = { this._buttons[name] = {
element: row, element: row,
details: details details: details,
}; };
const button = row.querySelector(".user-contact-enabled-disabled") as HTMLButtonElement; const button = row.querySelector(".user-contact-enabled-disabled") as HTMLButtonElement;
const checkbox = button.querySelector("input[type=checkbox]") as HTMLInputElement; const checkbox = button.querySelector("input[type=checkbox]") as HTMLInputElement;
const setButtonAppearance = () => { const setButtonAppearance = () => {
@@ -260,13 +276,14 @@ class ContactMethods {
const addEditButton = row.querySelector(".user-contact-edit") as HTMLButtonElement; const addEditButton = row.querySelector(".user-contact-edit") as HTMLButtonElement;
addEditButton.onclick = () => addEditFunc(details.value == ""); addEditButton.onclick = () => addEditFunc(details.value == "");
} }
if (!required && details.value != "") { if (!required && details.value != "") {
const deleteButton = row.querySelector(".user-contact-delete") as HTMLButtonElement; const deleteButton = row.querySelector(".user-contact-delete") as HTMLButtonElement;
deleteButton.onclick = () => _delete("/my/" + name, null, (req: XMLHttpRequest) => { deleteButton.onclick = () =>
if (req.readyState != 4) return; _delete("/my/" + name, null, (req: XMLHttpRequest) => {
document.dispatchEvent(new CustomEvent("details-reload")); if (req.readyState != 4) return;
}); document.dispatchEvent(new CustomEvent("details-reload"));
});
} }
this._content.appendChild(row); this._content.appendChild(row);
@@ -305,46 +322,52 @@ class ReferralCard {
private _expiryEl: HTMLSpanElement; private _expiryEl: HTMLSpanElement;
private _descriptionEl: HTMLSpanElement; private _descriptionEl: HTMLSpanElement;
get code(): string { return this._code; } get code(): string {
return this._code;
}
set code(c: string) { set code(c: string) {
this._code = c; this._code = c;
// let u = new URL(window.location.href); // let u = new URL(window.location.href);
// const path = window.pages.Base + window.pages.Form + "/" + this._code; // const path = window.pages.Base + window.pages.Form + "/" + this._code;
// //
// u.pathname = path; // u.pathname = path;
// u.hash = ""; // u.hash = "";
// u.search = ""; // u.search = "";
// this._url = u.toString(); // this._url = u.toString();
this._url = generateCodeLink(this._code); this._url = generateCodeLink(this._code);
} }
get remaining_uses(): number { return this._remainingUses; } get remaining_uses(): number {
set remaining_uses(v: number) { return this._remainingUses;
}
set remaining_uses(v: number) {
this._remainingUses = v; this._remainingUses = v;
if (v > 0 && !(this._noLimit)) if (v > 0 && !this._noLimit) this._remainingUsesEl.textContent = `${v}`;
this._remainingUsesEl.textContent = `${v}`;
} }
get no_limit(): boolean { return this._noLimit; } get no_limit(): boolean {
return this._noLimit;
}
set no_limit(v: boolean) { set no_limit(v: boolean) {
this._noLimit = v; this._noLimit = v;
if (v) if (v) this._remainingUsesEl.textContent = ``;
this._remainingUsesEl.textContent = ``; else this._remainingUsesEl.textContent = `${this._remainingUses}`;
else
this._remainingUsesEl.textContent = `${this._remainingUses}`;
} }
get expiry(): Date { return this._expiry; }; get expiry(): Date {
return this._expiry;
}
set expiry(expiryUnix: number) { set expiry(expiryUnix: number) {
this._expiryUnix = expiryUnix; this._expiryUnix = expiryUnix;
this._expiry = new Date(expiryUnix * 1000); this._expiry = new Date(expiryUnix * 1000);
this._expiryEl.textContent = toDateString(this._expiry); this._expiryEl.textContent = toDateString(this._expiry);
} }
get use_expiry(): boolean { return this._useExpiry; } get use_expiry(): boolean {
return this._useExpiry;
}
set use_expiry(v: boolean) { set use_expiry(v: boolean) {
this._useExpiry = v; this._useExpiry = v;
if (v) { if (v) {
@@ -353,7 +376,7 @@ class ReferralCard {
this._descriptionEl.textContent = window.lang.strings("referralsDescription"); this._descriptionEl.textContent = window.lang.strings("referralsDescription");
} }
} }
constructor(card: HTMLElement) { constructor(card: HTMLElement) {
this._card = card; this._card = card;
this._button = this._card.querySelector(".user-referrals-button") as HTMLButtonElement; this._button = this._card.querySelector(".user-referrals-button") as HTMLButtonElement;
@@ -372,10 +395,10 @@ class ReferralCard {
<div> <div>
</div> </div>
`; `;
this._remainingUsesEl = this._infoArea.querySelector(".referral-remaining-uses") as HTMLSpanElement; this._remainingUsesEl = this._infoArea.querySelector(".referral-remaining-uses") as HTMLSpanElement;
this._expiryEl = this._infoArea.querySelector(".referral-expiry") as HTMLSpanElement; this._expiryEl = this._infoArea.querySelector(".referral-expiry") as HTMLSpanElement;
document.addEventListener("timefmt-change", () => { document.addEventListener("timefmt-change", () => {
this.expiry = this._expiryUnix; this.expiry = this._expiryUnix;
}); });
@@ -432,25 +455,25 @@ class ExpiryCard {
let ymd = [0, 0, 0]; let ymd = [0, 0, 0];
while (now.getFullYear() != this._expiry.getFullYear()) { while (now.getFullYear() != this._expiry.getFullYear()) {
ymd[0] += 1; ymd[0] += 1;
now.setFullYear(now.getFullYear()+1); now.setFullYear(now.getFullYear() + 1);
} }
if (now.getMonth() > this._expiry.getMonth()) { if (now.getMonth() > this._expiry.getMonth()) {
ymd[0] -=1; ymd[0] -= 1;
now.setFullYear(now.getFullYear()-1); now.setFullYear(now.getFullYear() - 1);
} }
while (now.getMonth() != this._expiry.getMonth()) { while (now.getMonth() != this._expiry.getMonth()) {
ymd[1] += 1; ymd[1] += 1;
now.setMonth(now.getMonth() + 1); now.setMonth(now.getMonth() + 1);
} }
if (now.getDate() > this._expiry.getDate()) { if (now.getDate() > this._expiry.getDate()) {
ymd[1] -=1; ymd[1] -= 1;
now.setMonth(now.getMonth()-1); now.setMonth(now.getMonth() - 1);
} }
while (now.getDate() != this._expiry.getDate()) { while (now.getDate() != this._expiry.getDate()) {
ymd[2] += 1; ymd[2] += 1;
now.setDate(now.getDate() + 1); now.setDate(now.getDate() + 1);
} }
const langKeys = ["year", "month", "day"]; const langKeys = ["year", "month", "day"];
let innerHTML = ``; let innerHTML = ``;
for (let i = 0; i < langKeys.length; i++) { for (let i = 0; i < langKeys.length; i++) {
@@ -467,7 +490,9 @@ class ExpiryCard {
this._countdown.innerHTML = innerHTML; this._countdown.innerHTML = innerHTML;
}; };
get expiry(): Date { return this._expiry; }; get expiry(): Date {
return this._expiry;
}
set expiry(expiryUnix: number) { set expiry(expiryUnix: number) {
if (this._interval !== null) { if (this._interval !== null) {
window.clearInterval(this._interval); window.clearInterval(this._interval);
@@ -479,10 +504,12 @@ class ExpiryCard {
return; return;
} }
this._expiry = new Date(expiryUnix * 1000); this._expiry = new Date(expiryUnix * 1000);
this._aside.textContent = window.lang.strings("yourAccountIsValidUntil").replace("{date}", toDateString(this._expiry)); this._aside.textContent = window.lang
.strings("yourAccountIsValidUntil")
.replace("{date}", toDateString(this._expiry));
this._card.classList.remove("unfocused"); this._card.classList.remove("unfocused");
this._interval = window.setInterval(this._drawCountdown, 60*1000); this._interval = window.setInterval(this._drawCountdown, 60 * 1000);
this._drawCountdown(); this._drawCountdown();
} }
} }
@@ -496,37 +523,45 @@ var contactMethodList = new ContactMethods(contactCard);
const addEditEmail = (add: boolean): void => { const addEditEmail = (add: boolean): void => {
const heading = window.modals.email.modal.querySelector(".heading"); const heading = window.modals.email.modal.querySelector(".heading");
heading.innerHTML = (add ? window.lang.strings("addContactMethod") : window.lang.strings("editContactMethod")) + `<span class="modal-close">&times;</span>`; heading.innerHTML =
(add ? window.lang.strings("addContactMethod") : window.lang.strings("editContactMethod")) +
`<span class="modal-close">&times;</span>`;
const input = document.getElementById("modal-email-input") as HTMLInputElement; const input = document.getElementById("modal-email-input") as HTMLInputElement;
input.value = ""; input.value = "";
const confirmationRequired = window.modals.email.modal.querySelector(".confirmation-required"); const confirmationRequired = window.modals.email.modal.querySelector(".confirmation-required");
confirmationRequired.classList.add("unfocused"); confirmationRequired.classList.add("unfocused");
const content = window.modals.email.modal.querySelector(".content"); const content = window.modals.email.modal.querySelector(".content");
content.classList.remove("unfocused"); content.classList.remove("unfocused");
const submit = window.modals.email.modal.querySelector(".modal-submit") as HTMLButtonElement; const submit = window.modals.email.modal.querySelector(".modal-submit") as HTMLButtonElement;
submit.onclick = () => { submit.onclick = () => {
addLoader(submit); addLoader(submit);
_post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => { _post(
if (req.readyState != 4) return; "/my/email",
removeLoader(submit); { email: input.value },
if (req.status == 303 || req.status == 200) { (req: XMLHttpRequest) => {
document.dispatchEvent(new CustomEvent("details-reload")); if (req.readyState != 4) return;
window.modals.email.close(); removeLoader(submit);
} if (req.status == 303 || req.status == 200) {
}, true, (req: XMLHttpRequest) => { document.dispatchEvent(new CustomEvent("details-reload"));
if (req.readyState != 4) return; window.modals.email.close();
removeLoader(submit); }
if (req.status == 401) { },
content.classList.add("unfocused"); true,
confirmationRequired.classList.remove("unfocused"); (req: XMLHttpRequest) => {
} if (req.readyState != 4) return;
}); removeLoader(submit);
} if (req.status == 401) {
content.classList.add("unfocused");
confirmationRequired.classList.remove("unfocused");
}
},
);
};
window.modals.email.show(); window.modals.email.show();
} };
const discordConf: ServiceConfiguration = { const discordConf: ServiceConfiguration = {
modal: window.modals.discord as Modal, modal: window.modals.discord as Modal,
@@ -539,7 +574,7 @@ const discordConf: ServiceConfiguration = {
successError: window.lang.notif("verified"), successError: window.lang.notif("verified"),
successFunc: (modalClosed: boolean) => { successFunc: (modalClosed: boolean) => {
if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload")); if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload"));
} },
}; };
let discord: Discord; let discord: Discord;
@@ -555,7 +590,7 @@ const telegramConf: ServiceConfiguration = {
successError: window.lang.notif("verified"), successError: window.lang.notif("verified"),
successFunc: (modalClosed: boolean) => { successFunc: (modalClosed: boolean) => {
if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload")); if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload"));
} },
}; };
let telegram: Telegram; let telegram: Telegram;
@@ -571,13 +606,12 @@ const matrixConf: MatrixConfiguration = {
successError: window.lang.notif("verified"), successError: window.lang.notif("verified"),
successFunc: () => { successFunc: () => {
setTimeout(() => document.dispatchEvent(new CustomEvent("details-reload")), 1200); setTimeout(() => document.dispatchEvent(new CustomEvent("details-reload")), 1200);
} },
}; };
let matrix: Matrix; let matrix: Matrix;
if (window.matrixEnabled) matrix = new Matrix(matrixConf); if (window.matrixEnabled) matrix = new Matrix(matrixConf);
const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement; const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement;
const newPasswordField = document.getElementById("user-new-password") as HTMLInputElement; const newPasswordField = document.getElementById("user-new-password") as HTMLInputElement;
const rePasswordField = document.getElementById("user-reenter-new-password") as HTMLInputElement; const rePasswordField = document.getElementById("user-reenter-new-password") as HTMLInputElement;
@@ -592,7 +626,7 @@ let validatorConf: ValidatorConf = {
passwordField: newPasswordField, passwordField: newPasswordField,
rePasswordField: rePasswordField, rePasswordField: rePasswordField,
submitButton: changePasswordButton, submitButton: changePasswordButton,
validatorFunc: baseValidator validatorFunc: baseValidator,
}; };
let validator = new Validator(validatorConf); let validator = new Validator(validatorConf);
@@ -601,25 +635,33 @@ let validator = new Validator(validatorConf);
oldPasswordField.addEventListener("keyup", validator.validate); oldPasswordField.addEventListener("keyup", validator.validate);
changePasswordButton.addEventListener("click", () => { changePasswordButton.addEventListener("click", () => {
addLoader(changePasswordButton); addLoader(changePasswordButton);
_post("/my/password", { old: oldPasswordField.value, new: newPasswordField.value }, (req: XMLHttpRequest) => { _post(
if (req.readyState != 4) return; "/my/password",
removeLoader(changePasswordButton); { old: oldPasswordField.value, new: newPasswordField.value },
if (req.status == 400) { (req: XMLHttpRequest) => {
window.notifications.customError("errorPassword", window.lang.notif("errorPassword")); if (req.readyState != 4) return;
} else if (req.status == 500) { removeLoader(changePasswordButton);
window.notifications.customError("errorUnknown", window.lang.notif("errorUnknown")); if (req.status == 400) {
} else if (req.status == 204) { window.notifications.customError("errorPassword", window.lang.notif("errorPassword"));
window.notifications.customSuccess("passwordChanged", window.lang.notif("passwordChanged")); } else if (req.status == 500) {
setTimeout(() => { window.location.reload() }, 2000); window.notifications.customError("errorUnknown", window.lang.notif("errorUnknown"));
} } else if (req.status == 204) {
}, true, (req: XMLHttpRequest) => { window.notifications.customSuccess("passwordChanged", window.lang.notif("passwordChanged"));
if (req.readyState != 4) return; setTimeout(() => {
removeLoader(changePasswordButton); window.location.reload();
if (req.status == 401) { }, 2000);
window.notifications.customError("oldPasswordError", window.lang.notif("errorOldPassword")); }
return; },
} true,
}); (req: XMLHttpRequest) => {
if (req.readyState != 4) return;
removeLoader(changePasswordButton);
if (req.status == 401) {
window.notifications.customError("oldPasswordError", window.lang.notif("errorOldPassword"));
return;
}
},
);
}); });
document.addEventListener("details-reload", () => { document.addEventListener("details-reload", () => {
@@ -644,20 +686,56 @@ document.addEventListener("details-reload", () => {
rootCard.querySelector(".heading").innerHTML = innerHTML; rootCard.querySelector(".heading").innerHTML = innerHTML;
contactMethodList.clear(); contactMethodList.clear();
// Note the weird format of the functions for discord/telegram: // Note the weird format of the functions for discord/telegram:
// "this" was being redefined within the onclick() method, so // "this" was being redefined within the onclick() method, so
// they had to be wrapped in an anonymous function. // they had to be wrapped in an anonymous function.
const contactMethods: { name: string, icon: string, f: (add: boolean) => void, required: boolean, enabled: boolean }[] = [ const contactMethods: {
{name: "email", icon: `<i class="ri-mail-fill ri-lg"></i>`, f: addEditEmail, required: true, enabled: true}, name: string;
{name: "discord", icon: `<i class="ri-discord-fill ri-lg"></i>`, f: (add: boolean) => { discord.onclick(); }, required: window.discordRequired, enabled: window.discordEnabled}, icon: string;
{name: "telegram", icon: `<i class="ri-telegram-fill ri-lg"></i>`, f: (add: boolean) => { telegram.onclick() }, required: window.telegramRequired, enabled: window.telegramEnabled}, f: (add: boolean) => void;
{name: "matrix", icon: `<span class="font-bold">[m]</span>`, f: (add: boolean) => { matrix.show(); }, required: window.matrixRequired, enabled: window.matrixEnabled} required: boolean;
enabled: boolean;
}[] = [
{
name: "email",
icon: `<i class="ri-mail-fill ri-lg"></i>`,
f: addEditEmail,
required: true,
enabled: true,
},
{
name: "discord",
icon: `<i class="ri-discord-fill ri-lg"></i>`,
f: (add: boolean) => {
discord.onclick();
},
required: window.discordRequired,
enabled: window.discordEnabled,
},
{
name: "telegram",
icon: `<i class="ri-telegram-fill ri-lg"></i>`,
f: (add: boolean) => {
telegram.onclick();
},
required: window.telegramRequired,
enabled: window.telegramEnabled,
},
{
name: "matrix",
icon: `<span class="font-bold">[m]</span>`,
f: (add: boolean) => {
matrix.show();
},
required: window.matrixRequired,
enabled: window.matrixEnabled,
},
]; ];
for (let method of contactMethods) { for (let method of contactMethods) {
if (!(method.enabled)) continue; if (!method.enabled) continue;
if (method.name in details) { if (method.name in details) {
contactMethodList.append(method.name, details[method.name], method.icon, method.f, method.required); contactMethodList.append(method.name, details[method.name], method.icon, method.f, method.required);
} }
@@ -671,7 +749,7 @@ document.addEventListener("details-reload", () => {
let messageCard = document.getElementById("card-message"); let messageCard = document.getElementById("card-message");
if (details.accounts_admin) { if (details.accounts_admin) {
adminBackButton.classList.remove("unfocused"); adminBackButton.classList.remove("unfocused");
if (typeof(messageCard) == "undefined" || messageCard == null) { if (typeof messageCard == "undefined" || messageCard == null) {
messageCard = document.createElement("div"); messageCard = document.createElement("div");
messageCard.classList.add("card", "@low", "dark:~d_neutral", "content"); messageCard.classList.add("card", "@low", "dark:~d_neutral", "content");
messageCard.id = "card-message"; messageCard.id = "card-message";
@@ -685,7 +763,7 @@ document.addEventListener("details-reload", () => {
} }
} }
if (typeof(messageCard) != "undefined" && messageCard != null) { if (typeof messageCard != "undefined" && messageCard != null) {
messageCard.innerHTML = messageCard.innerHTML.replace(new RegExp("{username}", "g"), details.username); messageCard.innerHTML = messageCard.innerHTML.replace(new RegExp("{username}", "g"), details.username);
// setBestRowSpan(messageCard, false); // setBestRowSpan(messageCard, false);
// contactCard.querySelector(".content").classList.add("h-100"); // contactCard.querySelector(".content").classList.add("h-100");
@@ -696,7 +774,7 @@ document.addEventListener("details-reload", () => {
if (window.referralsEnabled) { if (window.referralsEnabled) {
if (details.has_referrals) { if (details.has_referrals) {
_get("/my/referral", null, (req: XMLHttpRequest) => { _get("/my/referral", null, (req: XMLHttpRequest) => {
if (req.readyState != 4 || req.status != 200) return; if (req.readyState != 4 || req.status != 200) return;
const referral: MyReferral = req.response as MyReferral; const referral: MyReferral = req.response as MyReferral;
referralCard.update(referral); referralCard.update(referral);
setCardOrder(messageCard); setCardOrder(messageCard);
@@ -715,9 +793,9 @@ document.addEventListener("details-reload", () => {
const setCardOrder = (messageCard: HTMLElement) => { const setCardOrder = (messageCard: HTMLElement) => {
const cards = document.getElementById("user-cardlist"); const cards = document.getElementById("user-cardlist");
const children = Array.from(cards.children); const children = Array.from(cards.children);
const idxs = [...Array(cards.childElementCount).keys()] const idxs = [...Array(cards.childElementCount).keys()];
// The message card is the first element and should always be so, so remove it from the list. // The message card is the first element and should always be so, so remove it from the list.
const hasMessageCard = !(typeof(messageCard) == "undefined" || messageCard == null); const hasMessageCard = !(typeof messageCard == "undefined" || messageCard == null);
if (hasMessageCard) idxs.shift(); if (hasMessageCard) idxs.shift();
const perms = generatePermutations(idxs); const perms = generatePermutations(idxs);
let minHeight = 999999; let minHeight = 999999;
@@ -781,8 +859,7 @@ const setBestRowSpan = (el: HTMLElement, setOnParent: boolean) => {
let rowSpan = Math.ceil(computeRealHeight(el) / largestNonMessageCardHeight); let rowSpan = Math.ceil(computeRealHeight(el) / largestNonMessageCardHeight);
if (rowSpan > 0) if (rowSpan > 0) (setOnParent ? el.parentElement : el).style.gridRow = `span ${rowSpan}`;
(setOnParent ? el.parentElement : el).style.gridRow = `span ${rowSpan}`;
}; };
const computeRealHeight = (el: HTMLElement): number => { const computeRealHeight = (el: HTMLElement): number => {
@@ -801,12 +878,12 @@ const computeRealHeight = (el: HTMLElement): number => {
} }
} }
return total; return total;
} };
const generatePermutations = (xs: number[]): [number[], number[]][] => { const generatePermutations = (xs: number[]): [number[], number[]][] => {
const l = xs.length; const l = xs.length;
let out: [number[], number[]][] = []; let out: [number[], number[]][] = [];
for (let i = 0; i < (l << 1); i++) { for (let i = 0; i < l << 1; i++) {
let incl = []; let incl = [];
let excl = []; let excl = [];
for (let j = 0; j < l; j++) { for (let j = 0; j < l; j++) {
@@ -819,7 +896,7 @@ const generatePermutations = (xs: number[]): [number[], number[]][] => {
out.push([incl, excl]); out.push([incl, excl]);
} }
return out; return out;
} };
login.bindLogout(document.getElementById("logout-button")); login.bindLogout(document.getElementById("logout-button"));