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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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;
@@ -12,7 +12,12 @@ var listeners: { [buttonText: string]: (event: CustomEvent) => void } = {};
export type DiscordSearch = (passData: string) => void;
export function newDiscordSearch(title: string, description: string, buttonText: string, buttonFunction: (user: DiscordUser, passData: string) => void): DiscordSearch {
export function newDiscordSearch(
title: string,
description: string,
buttonText: string,
buttonFunction: (user: DiscordUser, passData: string) => void,
): DiscordSearch {
if (!window.discordEnabled) {
return () => {};
}
@@ -62,7 +67,7 @@ export function newDiscordSearch(title: string, description: string, buttonText:
}
});
}, 750);
}
};
return (passData: string) => {
const input = document.getElementById("discord-search") as HTMLInputElement;
@@ -79,5 +84,5 @@ export function newDiscordSearch(title: string, description: string, buttonText:
input.addEventListener("keyup", listeners[buttonText].bind(null, { detail: passData }));
window.modals.discord.show();
}
};
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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