Two file renames (docker/mclogs.ini -> docker/iblogs.ini, web/public/css/mclogs.css -> web/public/css/iblogs.css) plus the internal references that pointed at them (Dockerfile COPY directive, the linux user name in the container). Also catches the runtime identifier renames: env-var prefix MCLOGS_* -> IBLOGS_* (compose files), browser cookie name MCLOGS_SETTINGS -> IBLOGS_SETTINGS (web/public/js/log.js), production image tag ghcr.io/aternosorg/mclogs:2 -> ghcr.io/indifferentketchup/iblogs:2, and the README walk-through with the new branding. example.config.json branding strings (legal contact, mclo.gs frontend name) and visible UI text (taglines, meta descriptions) are deferred to a separate branding commit.
310 lines
9.6 KiB
JavaScript
310 lines
9.6 KiB
JavaScript
/* line numbers */
|
|
updateLineNumber(location.hash);
|
|
|
|
for (let line of document.querySelectorAll('.line-number')) {
|
|
line.addEventListener("click", () =>
|
|
updateLineNumber(line.attributes.getNamedItem("id").value));
|
|
}
|
|
|
|
function updateLineNumber(id) {
|
|
if (id && id.startsWith('#')) {
|
|
id = id.substring(1);
|
|
}
|
|
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
let element = document.getElementById(id);
|
|
if (element.classList.contains("line-number")) {
|
|
for (const line of document.querySelectorAll(".line-active")) {
|
|
line.classList.remove("line-active");
|
|
}
|
|
element.closest('.entry').classList.add('line-active');
|
|
}
|
|
}
|
|
|
|
/* Scroll to top/bottom buttons */
|
|
const downButton = document.getElementById("down-button");
|
|
if (downButton) {
|
|
downButton.addEventListener("click", () => scrollToHeight(document.body.scrollHeight));
|
|
}
|
|
|
|
const upButton = document.getElementById("up-button");
|
|
if (upButton) {
|
|
upButton.addEventListener("click", () => scrollToHeight(0));
|
|
}
|
|
|
|
/**
|
|
* Scroll to a specific height
|
|
* Disable smooth scrolling for large pages
|
|
* @param {number} top height to scroll to
|
|
* @param {number} [smoothScrollLimit] only use smooth scrolling if the distance is less than this value
|
|
*/
|
|
function scrollToHeight(top, smoothScrollLimit = 10000) {
|
|
const distance = Math.abs(document.documentElement.scrollTop - top);
|
|
const behavior = (distance < smoothScrollLimit) ? "smooth" : "instant";
|
|
window.scrollTo({left: 0, top, behavior});
|
|
}
|
|
|
|
/* error collapse toggle */
|
|
const toggleErrorsButton = document.getElementById("error-toggle");
|
|
if (toggleErrorsButton) {
|
|
toggleErrorsButton.addEventListener("click", toggleErrors);
|
|
}
|
|
|
|
function toggleErrors() {
|
|
if (toggleErrorsButton.classList.contains("toggled")) {
|
|
toggleErrorsButton.classList.remove("toggled");
|
|
uncollapseAllErrors();
|
|
} else {
|
|
toggleErrorsButton.classList.add("toggled");
|
|
collapseAllErrors();
|
|
}
|
|
}
|
|
|
|
function collapseAllErrors() {
|
|
let firstNoErrorLine = false;
|
|
let lines = document.querySelectorAll('.log-inner > .entry');
|
|
let totalLines = lines.length;
|
|
for (const [i, line] of lines.entries()) {
|
|
let lineNumber = line.querySelector(".line-number").innerHTML;
|
|
if (line.classList.contains("entry-no-error")) {
|
|
line.style.display = "none";
|
|
|
|
if (firstNoErrorLine === false) {
|
|
firstNoErrorLine = lineNumber;
|
|
}
|
|
|
|
if (i + 1 === totalLines && firstNoErrorLine) {
|
|
line.insertAdjacentElement("afterend", generateCollapsedLines(firstNoErrorLine, lineNumber));
|
|
}
|
|
} else {
|
|
if (firstNoErrorLine) {
|
|
line.insertAdjacentElement("beforebegin", generateCollapsedLines(firstNoErrorLine, lineNumber - 1));
|
|
firstNoErrorLine = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function uncollapseAllErrors() {
|
|
document.querySelectorAll('.entry-no-error').forEach(line => line.style.removeProperty("display"));
|
|
document.querySelectorAll('.collapsed-lines').forEach(collapsed => collapsed.remove());
|
|
}
|
|
|
|
function handleCollapsedClick(e) {
|
|
let collapsed = e.currentTarget;
|
|
let positionElement = document.getElementById(`L${parseInt(collapsed.dataset.end) + 1}`);
|
|
let position;
|
|
if (positionElement) {
|
|
position = positionElement.getBoundingClientRect().top - window.scrollY;
|
|
}
|
|
for (let i = parseInt(collapsed.dataset.start); i <= parseInt(collapsed.dataset.end); i++) {
|
|
document.getElementById(`L${i}`).parentElement.parentElement.style.removeProperty("display");
|
|
}
|
|
if (positionElement) {
|
|
window.scrollTo({
|
|
left: 0,
|
|
top: positionElement.getBoundingClientRect().top - position - collapsed.offsetHeight,
|
|
behavior: "instant"
|
|
});
|
|
}
|
|
collapsed.remove();
|
|
}
|
|
|
|
function generateCollapsedLines(start, end) {
|
|
let count = end - start + 1;
|
|
let string = count === 1 ? "line" : "lines";
|
|
|
|
let collapsedRow = document.createElement("div");
|
|
collapsedRow.classList.add("collapsed-lines");
|
|
collapsedRow.dataset.start = start;
|
|
collapsedRow.dataset.end = end;
|
|
collapsedRow.appendChild(document.createElement("div"));
|
|
collapsedRow.addEventListener("click", handleCollapsedClick);
|
|
|
|
let collapsedLinesCount = document.createElement("div");
|
|
collapsedLinesCount.classList.add("collapsed-lines-count");
|
|
let icon = document.createElement("i");
|
|
icon.classList.add("fa-solid", "fa-angle-up");
|
|
collapsedLinesCount.appendChild(icon);
|
|
collapsedLinesCount.append(` ${count} ${string} `);
|
|
collapsedLinesCount.append(icon.cloneNode());
|
|
collapsedRow.appendChild(collapsedLinesCount);
|
|
|
|
return collapsedRow;
|
|
}
|
|
|
|
/* convert timestamps */
|
|
let timeElements = document.querySelectorAll('[data-time]');
|
|
for (const element of timeElements) {
|
|
const timestamp = parseInt(element.dataset.time);
|
|
if (isNaN(timestamp)) {
|
|
continue;
|
|
}
|
|
const date = new Date(timestamp * 1000);
|
|
element.innerHTML = date.toLocaleString();
|
|
}
|
|
|
|
/* settings */
|
|
const settingCheckboxes = document.querySelectorAll(".setting-checkbox");
|
|
settingCheckboxes.forEach(checkbox => checkbox.addEventListener("change", handleSettingChange));
|
|
|
|
let settingsChannel = null;
|
|
if (typeof BroadcastChannel !== "undefined") {
|
|
settingsChannel = new BroadcastChannel("mc-logs-settings");
|
|
settingsChannel.onmessage = (e) => {
|
|
if (e.data.type === "settings-updated") {
|
|
for (const checkbox of settingCheckboxes) {
|
|
checkbox.checked = !!e.data.settings[checkbox.dataset.key];
|
|
applySetting(checkbox);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function handleSettingChange(e) {
|
|
let checkbox = e.target;
|
|
applySetting(checkbox);
|
|
saveSettings();
|
|
if (settingsChannel) {
|
|
settingsChannel.postMessage({
|
|
type: "settings-updated",
|
|
settings: getCurrentSettings()
|
|
});
|
|
}
|
|
}
|
|
|
|
function applySetting(checkbox) {
|
|
let bodyClass = checkbox.dataset.bodyClass;
|
|
if (checkbox.checked) {
|
|
document.body.classList.add(bodyClass);
|
|
} else {
|
|
document.body.classList.remove(bodyClass);
|
|
}
|
|
switch (checkbox.dataset.key) {
|
|
case "floatingScrollbar":
|
|
initFloatingScrollbar();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function getCurrentSettings() {
|
|
const data = {};
|
|
for (const checkbox of settingCheckboxes) {
|
|
data[checkbox.dataset.key] = checkbox.checked;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function saveSettings() {
|
|
const data = {};
|
|
for (const checkbox of settingCheckboxes) {
|
|
data[checkbox.dataset.key] = checkbox.checked;
|
|
}
|
|
document.cookie = "IBLOGS_SETTINGS=" + encodeURIComponent(JSON.stringify(data)) + ";path=/;expires=" + new Date(new Date().getTime() + 100 * 365 * 24 * 60 * 60 * 1000).toUTCString();
|
|
}
|
|
|
|
/* copy to clipboard */
|
|
const copyButtons = document.querySelectorAll("[data-clipboard]");
|
|
copyButtons.forEach(button => button.addEventListener("click", handleCopyButtonClick));
|
|
const doneClassName = "fa-solid fa-check";
|
|
|
|
async function handleCopyButtonClick(e) {
|
|
const button = e.currentTarget;
|
|
const data = button.dataset.clipboard;
|
|
await navigator.clipboard.writeText(data);
|
|
|
|
const iconElement = button.querySelector("i");
|
|
if (!iconElement) {
|
|
return;
|
|
}
|
|
const originalClassName = iconElement.className;
|
|
if (originalClassName === doneClassName) {
|
|
return;
|
|
}
|
|
iconElement.className = doneClassName;
|
|
setTimeout(() => {
|
|
iconElement.className = originalClassName;
|
|
}, 2000);
|
|
}
|
|
|
|
/* delete button */
|
|
const deleteButton = document.querySelector(".delete-log-button");
|
|
const deleteErrorElement = document.querySelector(".delete-overlay .popover-error");
|
|
if (deleteButton) {
|
|
deleteButton.addEventListener("click", handleDeleteButtonClick);
|
|
}
|
|
|
|
async function handleDeleteButtonClick() {
|
|
deleteErrorElement.style.display = "none";
|
|
const response = await fetch(window.location.href, {
|
|
method: "DELETE",
|
|
credentials: "include"
|
|
});
|
|
if (!response.ok) {
|
|
deleteErrorElement.style.display = "block";
|
|
deleteErrorElement.textContent = `${response.status} (${response.statusText})`;
|
|
return;
|
|
}
|
|
window.location.href = "/";
|
|
}
|
|
|
|
/* floating scroll bar */
|
|
const browser = getComputedStyle(document.body)
|
|
.getPropertyValue("--browser")
|
|
.replaceAll(/['"]/g, '')
|
|
.trim()
|
|
.toLowerCase();
|
|
const floatingScrollbar = document.querySelector(".floating-scrollbar");
|
|
let logContainer = null;
|
|
if (browser === "firefox") {
|
|
logContainer = document.querySelector(".log");
|
|
} else {
|
|
logContainer = document.querySelector(".log-inner");
|
|
}
|
|
|
|
if (floatingScrollbar && logContainer) {
|
|
updateFloatingScrollbarWidths();
|
|
|
|
floatingScrollbar.addEventListener("scroll", () => {
|
|
syncScroll(floatingScrollbar, logContainer);
|
|
});
|
|
|
|
logContainer.addEventListener("scroll", () => {
|
|
syncScroll(logContainer, floatingScrollbar);
|
|
});
|
|
|
|
const observer = new ResizeObserver(() => {
|
|
updateFloatingScrollbarWidths();
|
|
});
|
|
observer.observe(logContainer);
|
|
}
|
|
|
|
function syncScroll(source, target) {
|
|
if (Math.abs(source.scrollLeft - target.scrollLeft) > 1) {
|
|
target.scrollLeft = source.scrollLeft;
|
|
}
|
|
}
|
|
|
|
function initFloatingScrollbar() {
|
|
if (!floatingScrollbar || !logContainer) {
|
|
return;
|
|
}
|
|
updateFloatingScrollbarWidths();
|
|
syncScroll(logContainer, floatingScrollbar);
|
|
}
|
|
|
|
function updateFloatingScrollbarWidths() {
|
|
floatingScrollbar.style.setProperty(
|
|
"--floating-scrollbar-width",
|
|
`${logContainer.clientWidth}px`
|
|
);
|
|
|
|
floatingScrollbar.style.setProperty(
|
|
"--floating-scrollbar-content-width",
|
|
`${logContainer.scrollWidth}px`
|
|
);
|
|
}
|