Files
rustpad/static/js/boot-loading.js
T
2026-10-16 22:49:22 +02:00

130 lines
5.5 KiB
JavaScript

/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
(() => {
const order = ["assets", "language", "libraries", "access", "files"];
const resourceParts = new Set();
const locale = String(document.documentElement.lang || "en").toLowerCase().startsWith("pl") ? "pl" : "en";
const strings = {
en: {
assets: "Preparing the interface", language: "Setting up language",
libraries: "Starting the editor", access: "Checking access", files: "Opening the note",
complete: "Ready", error: "The note could not be opened", retry: "Reload page",
errorDetails: "Check your connection and try again.", progress: "{done} of {total} stages completed",
timeout: "Still waiting for the note. Check the connection or reload."
},
pl: {
assets: "Przygotowanie interfejsu", language: "Ustawianie j\u0119zyka",
libraries: "Uruchamianie edytora", access: "Sprawdzanie dost\u0119pu", files: "Otwieranie notatki",
complete: "Gotowe", error: "Nie uda\u0142o si\u0119 otworzy\u0107 notatki", retry: "Za\u0142aduj ponownie",
errorDetails: "Sprawd\u017a po\u0142\u0105czenie i spr\u00f3buj ponownie.", progress: "Uko\u0144czono {done} z {total} etap\u00f3w",
timeout: "Nadal oczekujemy na notatk\u0119. Sprawd\u017a po\u0142\u0105czenie lub od\u015bwie\u017c stron\u0119."
}
}[locale];
const completed = new Set();
const loadedStyleElements = new WeakSet();
let loadedStyleCount = 0;
let readyRequested = false;
let dismissed = false;
let failure = "";
const element = id => document.getElementById(id);
function update() {
const overlay = element("app-boot-overlay");
if (!overlay || dismissed) return;
const count = completed.size;
const progress = Math.round((count / order.length) * 100);
const next = order.find(stage => !completed.has(stage));
const heading = element("app-boot-heading");
if (heading) heading.textContent = locale === "pl" ? "Ładowanie…" : "Loading…";
const stageText = element("app-boot-stage");
const stageCount = element("app-boot-count");
const bar = element("app-boot-progress");
const fill = element("app-boot-progress-fill");
const retry = element("app-boot-retry");
if (stageText) stageText.textContent = failure ? strings.error : (next ? strings[next] : strings.complete);
if (stageCount) stageCount.textContent = failure || strings.progress.replace("{done}", count).replace("{total}", order.length);
if (bar) bar.setAttribute("aria-valuenow", String(progress));
if (fill) fill.style.width = `${progress}%`;
if (retry) { retry.hidden = !failure; retry.textContent = strings.retry; }
overlay.classList.toggle("has-error", Boolean(failure));
for (const stage of order) {
const node = element(`app-boot-step-${stage}`);
if (!node) continue;
const isDone = completed.has(stage);
node.dataset.status = isDone ? "done" : (stage === next && !failure ? "active" : "waiting");
node.querySelector("span")?.replaceChildren(document.createTextNode(strings[stage]));
}
if (readyRequested && count === order.length && !failure) dismiss();
}
function dismiss() {
if (dismissed) return;
dismissed = true;
const overlay = element("app-boot-overlay");
if (!overlay) return;
overlay.classList.add("is-ready");
const remove = () => overlay.remove();
overlay.addEventListener("transitionend", remove, { once: true });
setTimeout(remove, 300);
}
function complete(stage) {
if (dismissed || failure) return;
if (stage === "css" || stage === "js") {
resourceParts.add(stage);
if (resourceParts.has("css") && resourceParts.has("js")) completed.add("assets");
} else if (order.includes(stage)) {
completed.add(stage);
} else {
return;
}
update();
}
function fail(reason) {
if (dismissed || failure) return;
const safeMessage = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "";
failure = safeMessage && safeMessage.length < 180 ? `${strings.error}: ${safeMessage}` : strings.errorDetails;
update();
}
document.addEventListener("load", event => {
if (!(event.target instanceof HTMLLinkElement) || !event.target.matches("link[data-rustpad-shell-css]")) return;
if (loadedStyleElements.has(event.target)) return;
loadedStyleElements.add(event.target);
loadedStyleCount += 1;
const expected = document.querySelectorAll("link[data-rustpad-shell-css]").length;
if (expected > 0 && loadedStyleCount >= expected) complete("css");
}, true);
document.addEventListener("error", event => {
if (event.target instanceof HTMLLinkElement && event.target.matches("link[data-rustpad-shell-css]")) {
fail(strings.errorDetails);
}
}, true);
document.addEventListener("DOMContentLoaded", () => {
element("app-boot-retry")?.addEventListener("click", () => location.reload());
update();
}, { once: true });
const timeout = setTimeout(() => {
if (!dismissed) fail(strings.timeout);
}, 45000);
window.__RUSTPAD_LOADING__ = Object.freeze({
complete,
skip: complete,
ready() { readyRequested = true; update(); if (dismissed) clearTimeout(timeout); },
fail,
stageDone(stage) {
return stage === "css" || stage === "js" ? resourceParts.has(stage) : completed.has(stage);
}
});
})();