All checks were successful
Build and Push Docker Image / build (push) Successful in 14s
315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
$(document).ready(async function() {
|
|
initSubscriptionsModule();
|
|
initFiltersModule();
|
|
initContentModule();
|
|
initMessageStylesModule();
|
|
|
|
await loadServers();
|
|
$("#subscriptionsTab").click();
|
|
});
|
|
|
|
$(document).on("selectedServerChange", function() {
|
|
$("#subscriptionsTab").click();
|
|
});
|
|
|
|
|
|
// region Hex Strings
|
|
|
|
function genHexString(len=6) {
|
|
let output = '';
|
|
for (let i = 0; i < len; ++i) {
|
|
output += (Math.floor(Math.random() * 16)).toString(16);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
|
|
// region DateTime
|
|
|
|
function isISODateTimeString(value) {
|
|
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:\d{2}|Z)$/;
|
|
return typeof value === 'string' && isoDatePattern.test(value);
|
|
}
|
|
|
|
// my clone of python's datetime.strftime
|
|
function formatStringDate(date, format) {
|
|
const padZero = (num, len) => String(num).padStart(len, "0");
|
|
const abbreviate = (str) => str.slice(0, 3);
|
|
const ordSuffix = day => (day % 100 >= 11 && day % 100 <= 13) ? "th" : ["th", "st", "nd", "rd"][day % 10] || "th";
|
|
|
|
const formatters = {
|
|
"%a": date => abbreviate(date.toLocaleString("en-GB", { weekday: "short" })),
|
|
"%A": date => date.toLocaleString("en-GB", { weekday: "long" }),
|
|
"%w": date => date.getDay(),
|
|
"%d": date => padZero(date.getDate(), 2),
|
|
"%-d": date => date.getDate(),
|
|
"%b": date => abbreviate(date.toLocaleString("en-GB", { month: "short" })),
|
|
"%B": date => date.toLocaleString("en-GB", { month: "long" }),
|
|
"%m": date => padZero(date.getMonth() + 1, 2),
|
|
"%-m": date => date.getMonth() + 1,
|
|
"%y": date => padZero(date.getFullYear() % 100, 2),
|
|
"%-y": date => date.getFullYear() % 100,
|
|
"%Y": date => date.getFullYear(),
|
|
"%H": date => padZero(date.getHours(), 2),
|
|
"%-H": date => date.getHours(),
|
|
"%I": date => padZero(date.getHours() % 12 || 12, 2),
|
|
"%-I": date => date.getHours() % 12 || 12,
|
|
"%p": date => date.getHours() >= 12 ? "PM" : "AM",
|
|
"%M": date => padZero(date.getMinutes(), 2),
|
|
"%-M": date => date.getMinutes(),
|
|
"%S": date => padZero(date.getSeconds(), 2),
|
|
"%-S": date => date.getSeconds(),
|
|
"%f": date => padZero(date.getMilliseconds() * 1000, 6),
|
|
"%z": date => {
|
|
const offset = date.getTimezoneOffset();
|
|
const sign = offset > 0 ? "-" : "+";
|
|
const absOffset = Math.abs(offset);
|
|
const hours = padZero(Math.floor(absOffset / 60), 2);
|
|
const minutes = padZero(absOffset % 60, 2);
|
|
return `${sign}${hours}${minutes}`;
|
|
},
|
|
"%Z": date => {
|
|
const match = date.toTimeString().match(/\((.*)\)/);
|
|
return match ? match[1] : "";
|
|
},
|
|
"%j": date => padZero(Math.ceil((date - new Date(date.getFullYear(), 0, 1)) / 86400000) + 1, 3),
|
|
"%-j": date => Math.ceil((date - new Date(date.getFullYear(), 0, 1)) / 86400000) + 1,
|
|
"%U": date => padZero(Math.floor((date - new Date(date.getFullYear(), 0, 1) + (86400000 * (date.getDay() || 7 - 1))) / (86400000 * 7)), 2),
|
|
"%W": date => padZero(Math.floor((date - new Date(date.getFullYear(), 0, 1) + (86400000 * (date.getDay() || 7))) / (86400000 * 7)), 2),
|
|
"%c": date => date.toLocaleString(),
|
|
"%x": date => date.toLocaleDateString(),
|
|
"%X": date => date.toLocaleTimeString(),
|
|
"%D": date => date.getDate() + ordSuffix(date.getDate()),
|
|
"%%": () => "%"
|
|
};
|
|
|
|
return format.replace(/%[a-zA-Z%-]/g, match => formatters[match] ? formatters[match](date) : match);
|
|
}
|
|
|
|
|
|
// region Colour Controls
|
|
$(".colour-control-picker").on("change", function() {
|
|
$(this).closest(".colour-control-group").find(".colour-control-text").val($(this).val());
|
|
});
|
|
|
|
$(".colour-control-text").on("change", function() {
|
|
$(this).closest(".colour-control-group").find(".colour-control-picker").val($(this).val());
|
|
});
|
|
|
|
function updateColourInput(id, hexString) {
|
|
hexString = normaliseHexString(hexString.toUpperCase());
|
|
$(`#${id} .colour-picker`).val(hexString);
|
|
$(`#${id} .colour-text`).val(hexString);
|
|
}
|
|
|
|
function getColourInputVal(id, includeHashtag=true) {
|
|
const hexString = $(`#${id}Text`).val();
|
|
return normaliseHexString(hexString, includeHashtag);
|
|
}
|
|
|
|
function normaliseHexString(hexString, includeHashtag=true) {
|
|
console.debug(`normalising hex string '${hexString}' include hashtag '${includeHashtag}'`);
|
|
|
|
// Remove any non-hex characters (e.g., additional hashtags)
|
|
hexString = hexString.replace(/[^A-F0-9]/gi, '');
|
|
|
|
// Ensure the hex string has a valid length of either 3, 6, or 8 characters
|
|
if (![3, 6, 8].includes(hexString.length)) {
|
|
throw new Error(`Invalid hex string length. Must be 3, 6, or 8 characters. hexString=${hexString}`);
|
|
}
|
|
|
|
return includeHashtag ? `#${hexString}` : hexString;
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
$(".colour-input").each(function() {
|
|
let id = $(this).attr("data-id")
|
|
label = $(this).attr("data-label");
|
|
helpText = $(this).attr("data-helptext");
|
|
tabIndex = parseInt($(this).attr("data-tabindex"));
|
|
dataField = $(this).attr("data-field");
|
|
defaultColour = $(this).attr("data-defaultcolour");
|
|
defaultColour = defaultColour ? defaultColour : "#3498db";
|
|
|
|
$(this).replaceWith(`
|
|
<label for="${id}Picker" class="form-label">${label}</label>
|
|
<div id="${id}" class="input-group">
|
|
<input type="color" name="${id}Picker" id="${id}Picker" class="form-control-color input-group-text colour-picker rounded-start-1" tabindex="${tabIndex}" data-default="${defaultColour}" data-field="${dataField}">
|
|
<input type="text" name="${id}Text" id="${id}Text" class="form-control colour-text" tabindex="${tabIndex + 1}">
|
|
<button type="button" class="btn btn-secondary colour-reset" data-bs-toggle="tooltip" data-bs-title="Reset Colour" data-defaultcolour="${defaultColour}" tabindex="${tabIndex + 2}">
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-secondary rounded-end-1 colour-random" data-bs-toggle="tooltip" data-bs-title="Random Colour" tabindex="${tabIndex + 3}">
|
|
<i class="bi bi-dice-5"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-text">${helpText}</div>
|
|
`);
|
|
|
|
$(`#${id} .colour-picker`).on("change", function() {
|
|
updateColourInput(id, $(this).val());
|
|
});
|
|
|
|
$(`#${id} .colour-text`).on("change", function() {
|
|
updateColourInput(id, $(this).val());
|
|
});
|
|
|
|
$(`#${id} .colour-reset`).on("click", function() {
|
|
updateColourInput(id, $(this).attr("data-defaultcolour"));
|
|
});
|
|
|
|
$(`#${id} .colour-random`).on("click", function() {
|
|
updateColourInput(id, "#" + genHexString(6));
|
|
});
|
|
|
|
updateColourInput(id, "#" + defaultColour)
|
|
$(`#${id} [data-bs-toggle="tooltip"]`).tooltip();
|
|
});
|
|
});
|
|
|
|
|
|
// region OK modal
|
|
|
|
function validateBootstrapStyle(style) {
|
|
if (!["danger", "success", "warning", "info", "primary", "secondary"].includes(style)) {
|
|
throw new Error(`${style} is not a valid style`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function okModal(title, bodyText, style, iconClass, func) {
|
|
let $modal = $("#okModal");
|
|
|
|
validateBootstrapStyle(style);
|
|
$modal.find(".modal-dismiss-btn").addClass(`btn-${style}`);
|
|
$modal.find(".modal-dismiss-btn > i").addClass(iconClass);
|
|
|
|
$modal.find(".modal-title").text(title);
|
|
$modal.find(".modal-body > p").html(bodyText);
|
|
|
|
$modal.find(".modal-dismiss-btn").off("click").on("click", async function(e) {
|
|
if (func) await func();
|
|
$modal.modal("hide");
|
|
});
|
|
|
|
$modal.modal("show");
|
|
}
|
|
|
|
|
|
// region Confirm Modal
|
|
|
|
async function confirmationModal(title, bodyText, style, iconClass, acceptFunc, declineFunc) {
|
|
let $modal = $("#confirmModal");
|
|
|
|
validateBootstrapStyle(style);
|
|
$modal.find(".modal-confirm-btn").addClass(`btn-${style}`);
|
|
$modal.find(".modal-confirm-btn > i").addClass(iconClass);
|
|
|
|
$modal.find(".modal-title").text(title);
|
|
$modal.find(".modal-body > p").html(bodyText);
|
|
|
|
$modal.find(".modal-confirm-btn").off("click").on("click", async function(e) {
|
|
await acceptFunc()
|
|
$modal.modal("hide");
|
|
});
|
|
|
|
$modal.find(".modal-dismiss-btn").off("click").on("click", async function(e) {
|
|
if (declineFunc) await declineFunc();
|
|
$modal.modal("hide");
|
|
});
|
|
|
|
$modal.modal("show");
|
|
}
|
|
|
|
function arrayToHtmlList(array, bold=false) {
|
|
$ul = $("<ul>").addClass("mb-0");
|
|
|
|
array.forEach(item => {
|
|
let $li = $("<li>");
|
|
$ul.append(bold ? $li.append($("<b>").text(item)) : $li.text(item));
|
|
});
|
|
|
|
return $ul;
|
|
}
|
|
|
|
|
|
// region Log Error
|
|
|
|
function logError(error) {
|
|
if (error instanceof Error) {
|
|
// Logs typical error properties like message and stack
|
|
console.error({
|
|
message: error.message,
|
|
stack: error.stack,
|
|
name: error.name,
|
|
});
|
|
} else if (typeof error === 'object' && error !== null) {
|
|
// Try to stringify if it's an object
|
|
try {
|
|
console.error(JSON.stringify(error, null, 2));
|
|
} catch (stringifyError) {
|
|
console.error('Could not stringify the error:', error);
|
|
}
|
|
} else {
|
|
// Fallback for any other types (string, number, etc.)
|
|
console.error('Error:', error);
|
|
}
|
|
}
|
|
|
|
|
|
// region Sidebar Visibility
|
|
|
|
var _sidebarVisible = $(".sidebar").hasClass("visible"); // Applicable for smaller screens ONLY
|
|
const getSidebarVisibility = () => _sidebarVisible;
|
|
const setSidebarVisibility = show => {
|
|
// Must always show if the sidebar is pinned
|
|
show = getSidebarPinned() ? true : show;
|
|
|
|
_sidebarVisible = show;
|
|
if (show) {
|
|
$(".sidebar").addClass("visible");
|
|
$(".sidebar-backdrop").show();
|
|
return;
|
|
}
|
|
|
|
$(".sidebar").removeClass("visible");
|
|
$(".sidebar-backdrop").hide();
|
|
}
|
|
const toggleSidebarVisibility = () => setSidebarVisibility(!getSidebarVisibility());
|
|
|
|
// Trigger an update to set the backdrop
|
|
$(document).ready(() => setSidebarVisibility(getSidebarVisibility()));
|
|
|
|
// User controls for sidebar visibility
|
|
$(".reveal-sidebar-btn").on("click", toggleSidebarVisibility);
|
|
$(".sidebar .btn-close").on("click", () => setSidebarVisibility(false));
|
|
$(".sidebar-backdrop").on("click", () => setSidebarVisibility(false));
|
|
|
|
// Prevent sidebar from opening if the screen becomes larger then smaller again, while it's visible
|
|
$(window).on('resize', () => {
|
|
// Can't pass conditional directly, causes flickering effect
|
|
if (getSidebarVisibility() && $(window).width() > 992) {
|
|
setSidebarVisibility(false);
|
|
}
|
|
});
|
|
|
|
|
|
// region Sidebar Pin
|
|
|
|
var _sidebarPinned = $(".sidebar .pin-sidebar-btn").hasClass("active");
|
|
const getSidebarPinned = () => _sidebarPinned;
|
|
const setSidebarPinned = pin => {
|
|
_sidebarPinned = pin;
|
|
|
|
// Show button as active or not
|
|
const btn = $(".sidebar .pin-sidebar-btn");
|
|
pin ? btn.addClass("active") : btn.removeClass("active");
|
|
|
|
$(".sidebar .btn-close").prop("disabled", pin);
|
|
$(".reveal-sidebar-btn").prop("disabled", pin);
|
|
}
|
|
const toggleSidebarPinned = () => setSidebarPinned(!getSidebarPinned());
|
|
|
|
// User controls for pinning the sidebar
|
|
$(".sidebar .pin-sidebar-btn").on("click", toggleSidebarPinned);
|