435 lines
10 KiB
JavaScript
Executable File
435 lines
10 KiB
JavaScript
Executable File
/* ============================================================================
|
|
KalliLab — CUSTOM JAVASCRIPT
|
|
Home / Media Tabs
|
|
Reload-Button entfernen
|
|
Aktiven Tab merken
|
|
============================================================================ */
|
|
|
|
const CONFIG = {
|
|
STORAGE: {
|
|
KEY: "kallilab-last-focused-tab-id",
|
|
},
|
|
TIMING: {
|
|
RETRY_DELAY: 500,
|
|
STANDARD_REFRESH: 1800000,
|
|
QUICK_REFRESH: 60000,
|
|
RETRY_ON_ERROR: 30000,
|
|
BATCH_DELAY: 100,
|
|
},
|
|
SERVICES: {
|
|
QUICK_REFRESH: [],
|
|
},
|
|
};
|
|
|
|
const RELOAD_BUTTON_SELECTORS = [
|
|
"#revalidate",
|
|
'[data-testid="revalidate"]',
|
|
".reload-button",
|
|
'button[aria-label="Reload"]',
|
|
'[role="button"][aria-label="Reload"]',
|
|
];
|
|
|
|
const TAB_HASH_MAP = {
|
|
"#home": "Home-tab",
|
|
"#media": "Media-tab",
|
|
"": "Home-tab",
|
|
};
|
|
|
|
const state = {
|
|
lastUpdate: new WeakMap(),
|
|
currentFocusedTab: null,
|
|
observers: {
|
|
reloadButton: null,
|
|
},
|
|
};
|
|
|
|
const domCache = {
|
|
myTab: null,
|
|
tabContents: null,
|
|
activeTabContent: null,
|
|
|
|
initCache() {
|
|
this.myTab = document.getElementById("myTab");
|
|
this.tabContents = document.querySelectorAll(".tabcontent");
|
|
this.updateActiveTab();
|
|
},
|
|
|
|
updateActiveTab() {
|
|
this.activeTabContent = document.querySelector(".tabcontent.active");
|
|
},
|
|
|
|
clear() {
|
|
this.myTab = null;
|
|
this.tabContents = null;
|
|
this.activeTabContent = null;
|
|
},
|
|
};
|
|
|
|
const storage = {
|
|
save(tabId) {
|
|
try {
|
|
localStorage.setItem(CONFIG.STORAGE.KEY, tabId);
|
|
} catch (error) {
|
|
console.warn("Storage save failed:", error);
|
|
}
|
|
},
|
|
|
|
get() {
|
|
try {
|
|
return localStorage.getItem(CONFIG.STORAGE.KEY);
|
|
} catch (error) {
|
|
console.warn("Storage retrieval failed:", error);
|
|
return null;
|
|
}
|
|
},
|
|
};
|
|
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func(...args), wait);
|
|
};
|
|
}
|
|
|
|
function throttle(func, limit) {
|
|
let inThrottle;
|
|
return function executedFunction(...args) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => {
|
|
inThrottle = false;
|
|
}, limit);
|
|
}
|
|
};
|
|
}
|
|
|
|
function removeReloadButton() {
|
|
RELOAD_BUTTON_SELECTORS.forEach((selector) => {
|
|
document.querySelectorAll(selector).forEach((element) => element.remove());
|
|
});
|
|
}
|
|
|
|
function setupReloadButtonObserver() {
|
|
if (state.observers.reloadButton) {
|
|
return state.observers.reloadButton;
|
|
}
|
|
|
|
const observer = new MutationObserver(
|
|
throttle((mutations) => {
|
|
const hasAddedNodes = mutations.some((m) => m.addedNodes.length > 0);
|
|
if (hasAddedNodes) {
|
|
removeReloadButton();
|
|
}
|
|
}, 100),
|
|
);
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ["class", "style"],
|
|
});
|
|
|
|
state.observers.reloadButton = observer;
|
|
return observer;
|
|
}
|
|
|
|
function setTabFocus(tab) {
|
|
requestAnimationFrame(() => {
|
|
if (state.currentFocusedTab) {
|
|
state.currentFocusedTab.classList.remove("tab-focused");
|
|
}
|
|
|
|
state.currentFocusedTab = tab;
|
|
|
|
if (state.currentFocusedTab) {
|
|
state.currentFocusedTab.classList.add("tab-focused");
|
|
}
|
|
});
|
|
}
|
|
|
|
function showTabContent(contentElement) {
|
|
if (!contentElement || !domCache.tabContents) return;
|
|
|
|
domCache.tabContents.forEach((content) => {
|
|
content.classList.remove("active");
|
|
content.style.display = "none";
|
|
});
|
|
|
|
contentElement.classList.add("active");
|
|
contentElement.style.display = "block";
|
|
domCache.updateActiveTab();
|
|
}
|
|
|
|
function resolveTabContentElement(tab) {
|
|
if (!tab) return null;
|
|
|
|
const ariaControls = tab.getAttribute("aria-controls");
|
|
if (ariaControls) {
|
|
return document.getElementById(ariaControls);
|
|
}
|
|
|
|
const tabText = (tab.textContent || "").trim().toLowerCase();
|
|
const directMatch = Array.from(document.querySelectorAll(".tabcontent")).find((content) => {
|
|
return content.id.toLowerCase() === tabText;
|
|
});
|
|
|
|
return directMatch || document.querySelector(".tabcontent.active");
|
|
}
|
|
|
|
function activateTab(tab, updateHash = true) {
|
|
if (!tab) return;
|
|
|
|
setTabFocus(tab);
|
|
storage.save(tab.id);
|
|
|
|
const contentToShow = resolveTabContentElement(tab);
|
|
showTabContent(contentToShow);
|
|
|
|
if (updateHash) {
|
|
const normalized = (tab.textContent || "").trim().toLowerCase();
|
|
if (normalized === "home") {
|
|
history.replaceState(null, "", "#home");
|
|
} else if (normalized === "media") {
|
|
history.replaceState(null, "", "#media");
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleTabFocusFromURL() {
|
|
const hash = window.location.hash.toLowerCase();
|
|
const tabId = TAB_HASH_MAP[hash] || TAB_HASH_MAP[""];
|
|
const tabToFocus = document.getElementById(tabId);
|
|
|
|
if (tabToFocus) {
|
|
activateTab(tabToFocus, false);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function updateServiceCard(card, data) {
|
|
requestAnimationFrame(() => {
|
|
const titleElement = card.querySelector(".card-title");
|
|
const statusElement = card.querySelector(".card-status");
|
|
|
|
if (titleElement && data.title) {
|
|
titleElement.textContent = data.title;
|
|
}
|
|
|
|
if (statusElement) {
|
|
statusElement.textContent = Array.isArray(data)
|
|
? `${data.length} items`
|
|
: (data.status ?? (typeof data === "object" ? "Data received" : ""));
|
|
statusElement.style.color = "";
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateServiceCardError(card, error) {
|
|
requestAnimationFrame(() => {
|
|
const statusElement = card.querySelector(".card-status");
|
|
if (statusElement) {
|
|
statusElement.textContent = error.message.includes("404")
|
|
? "Service unavailable"
|
|
: "Error loading data";
|
|
statusElement.style.color = "#ff7b7b";
|
|
}
|
|
});
|
|
}
|
|
|
|
async function refreshService(card) {
|
|
const apiEndpoint = card.dataset.apiEndpoint;
|
|
if (!apiEndpoint) return;
|
|
|
|
const serviceId = card.id || apiEndpoint;
|
|
const now = Date.now();
|
|
const lastUpdateTime = state.lastUpdate.get(card) || 0;
|
|
const isQuickRefreshService = CONFIG.SERVICES.QUICK_REFRESH.includes(serviceId);
|
|
const minInterval = isQuickRefreshService
|
|
? CONFIG.TIMING.QUICK_REFRESH
|
|
: CONFIG.TIMING.STANDARD_REFRESH;
|
|
|
|
if (now - lastUpdateTime < minInterval) return;
|
|
|
|
try {
|
|
card.classList.add("updating");
|
|
|
|
const response = await fetch(apiEndpoint, {
|
|
signal: AbortSignal.timeout(10000),
|
|
}).then((r) => {
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
return r.json();
|
|
});
|
|
|
|
updateServiceCard(card, response);
|
|
state.lastUpdate.set(card, now);
|
|
} catch (error) {
|
|
console.error(`${serviceId} refresh failed:`, error);
|
|
updateServiceCardError(card, error);
|
|
state.lastUpdate.set(card, now - (minInterval - CONFIG.TIMING.RETRY_ON_ERROR));
|
|
} finally {
|
|
card.classList.remove("updating");
|
|
}
|
|
}
|
|
|
|
async function batchUpdateServiceCards(cards) {
|
|
const batchSize = 3;
|
|
const batches = [];
|
|
|
|
for (let i = 0; i < cards.length; i += batchSize) {
|
|
batches.push(cards.slice(i, i + batchSize));
|
|
}
|
|
|
|
for (const batch of batches) {
|
|
await Promise.all(batch.map(refreshService));
|
|
if (batches.length > 1) {
|
|
await new Promise((resolve) => setTimeout(resolve, CONFIG.TIMING.BATCH_DELAY));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function preloadAllTabs() {
|
|
const tabContents = document.querySelectorAll(".tabcontent, .tab-pane");
|
|
const serviceCards = new Set();
|
|
|
|
tabContents.forEach((tab) => {
|
|
tab.querySelectorAll(".service-card[data-api-endpoint]").forEach((card) => {
|
|
serviceCards.add(card);
|
|
});
|
|
});
|
|
|
|
await batchUpdateServiceCards(Array.from(serviceCards));
|
|
}
|
|
|
|
function setupPeriodicRefresh() {
|
|
const debouncedRefresh = debounce(async () => {
|
|
if (domCache.activeTabContent) {
|
|
const cards = domCache.activeTabContent.querySelectorAll(
|
|
".service-card[data-api-endpoint]",
|
|
);
|
|
await batchUpdateServiceCards(Array.from(cards));
|
|
}
|
|
}, 250);
|
|
|
|
setInterval(
|
|
debouncedRefresh,
|
|
Math.min(CONFIG.TIMING.QUICK_REFRESH, CONFIG.TIMING.STANDARD_REFRESH),
|
|
);
|
|
|
|
document.addEventListener("visibilitychange", () => {
|
|
if (document.visibilityState === "visible") {
|
|
debouncedRefresh();
|
|
}
|
|
});
|
|
|
|
domCache.myTab?.addEventListener("click", (event) => {
|
|
const tab = event.target.closest('[id$="-tab"]');
|
|
if (tab) {
|
|
activateTab(tab);
|
|
debouncedRefresh();
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeTabFocus() {
|
|
const tabs = document.querySelectorAll('#myTab [id$="-tab"]');
|
|
if (!tabs.length) return;
|
|
|
|
const handledByHash = handleTabFocusFromURL();
|
|
|
|
if (!handledByHash) {
|
|
const savedTabId = storage.get();
|
|
const savedTab = savedTabId && document.getElementById(savedTabId);
|
|
|
|
if (savedTab) {
|
|
activateTab(savedTab, false);
|
|
} else {
|
|
const activeTab = document.querySelector('#myTab [aria-selected="true"]');
|
|
if (activeTab) {
|
|
activateTab(activeTab, false);
|
|
} else {
|
|
activateTab(tabs[0], false);
|
|
}
|
|
}
|
|
}
|
|
|
|
tabs.forEach((tab) => {
|
|
tab.addEventListener("click", () => {
|
|
activateTab(tab);
|
|
});
|
|
|
|
tab.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
activateTab(tab);
|
|
}
|
|
});
|
|
});
|
|
|
|
window.addEventListener("beforeunload", () => {
|
|
if (state.currentFocusedTab) {
|
|
storage.save(state.currentFocusedTab.id);
|
|
}
|
|
});
|
|
|
|
window.addEventListener("hashchange", () => {
|
|
handleTabFocusFromURL();
|
|
});
|
|
}
|
|
|
|
function initializeEverything() {
|
|
removeReloadButton();
|
|
setupReloadButtonObserver();
|
|
|
|
[100, 500, 1000].forEach((delay) => {
|
|
setTimeout(removeReloadButton, delay);
|
|
});
|
|
|
|
const hasRequiredElements =
|
|
document.querySelector("#myTab") && document.querySelector(".service-card");
|
|
|
|
if (hasRequiredElements) {
|
|
domCache.initCache();
|
|
initializeTabFocus();
|
|
preloadAllTabs();
|
|
setupPeriodicRefresh();
|
|
} else {
|
|
setTimeout(initializeEverything, CONFIG.TIMING.RETRY_DELAY);
|
|
}
|
|
}
|
|
|
|
function cleanup() {
|
|
if (state.observers.reloadButton) {
|
|
state.observers.reloadButton.disconnect();
|
|
state.observers.reloadButton = null;
|
|
}
|
|
|
|
domCache.clear();
|
|
state.currentFocusedTab = null;
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", removeReloadButton);
|
|
window.addEventListener("load", initializeEverything);
|
|
window.addEventListener("unload", cleanup);
|
|
|
|
if (typeof window.htmlLoaded === "function") {
|
|
const originalHtmlLoaded = window.htmlLoaded;
|
|
window.htmlLoaded = () => {
|
|
originalHtmlLoaded();
|
|
initializeEverything();
|
|
};
|
|
}
|
|
|
|
if ("ontouchstart" in window) {
|
|
window.addEventListener(
|
|
"touchend",
|
|
() => {
|
|
setTimeout(removeReloadButton, 100);
|
|
},
|
|
{ passive: true },
|
|
);
|
|
} |