Displays preview cards for previous and next posts on Pawchive post pages with instant navigation.
// ==UserScript==
// @name Pawchive Next/Previous Post Navigator
// @namespace pawchive-next-previous-card
// @version 1.0
// @description Displays preview cards for previous and next posts on Pawchive post pages with instant navigation.
// @author Mekokung
// @tag utilities
// @icon https://pawchive.pw/static/favicon.png
// @supportURL https://linktr.ee/Mekokung
// @match https://pawchive.pw/patreon/user/*
// @match https://pawchive.pw/fanbox/user/*
// @match https://pawchive.pw/discord/user/*
// @match https://pawchive.pw/discord/server/*
// @match https://pawchive.pw/*
// @run-at document-start
// @noframes
// @grant none
// @license GPL-3.0
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
debug: false,
cacheStorageKey: 'pawchive_post_navigator_cache_v1',
maxCacheEntries: 300,
requestTimeoutMs: 8000,
sidebarWidthRem: 12,
cardWidthPx: 270,
cardHeightPx: 92,
thumbWidthPx: 90
};
const PREFIX = '[PawchiveNav]';
function log(...args) {
if (CONFIG.debug) {
console.log(PREFIX, ...args);
}
}
function logError(...args) {
console.error(PREFIX, ...args);
}
class PostCache {
constructor() {
this.memoryCache = new Map();
this.loadFromStorage();
}
loadFromStorage() {
try {
const raw = sessionStorage.getItem(CONFIG.cacheStorageKey);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
for (const [key, value] of Object.entries(parsed)) {
this.memoryCache.set(key, value);
}
}
}
} catch (err) {
log('Failed to load cache from sessionStorage:', err);
}
}
saveToStorage() {
try {
if (this.memoryCache.size > CONFIG.maxCacheEntries) {
const keys = Array.from(this.memoryCache.keys());
const toDelete = keys.slice(0, keys.length - CONFIG.maxCacheEntries);
for (const k of toDelete) {
this.memoryCache.delete(k);
}
}
const obj = Object.fromEntries(this.memoryCache.entries());
sessionStorage.setItem(CONFIG.cacheStorageKey, JSON.stringify(obj));
} catch (err) {
log('Failed to save cache to sessionStorage:', err);
}
}
normalizeKey(urlOrId) {
if (!urlOrId) return '';
try {
const parsed = new URL(urlOrId, window.location.origin);
return parsed.pathname.replace(/\/+$/, '');
} catch {
return String(urlOrId).trim();
}
}
get(urlOrId) {
const key = this.normalizeKey(urlOrId);
return this.memoryCache.get(key) || null;
}
set(urlOrId, postData) {
if (!urlOrId || !postData) return;
const key = this.normalizeKey(urlOrId);
this.memoryCache.set(key, postData);
this.saveToStorage();
}
}
const postCache = new PostCache();
const SVG_ICONS = {
arrowLeft: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
arrowRight: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
clock: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
noPreview: `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`
};
function isPostPage() {
const path = window.location.pathname;
const matchesPostPath = /\/post\/[^/?#]+/.test(path);
const hasPostElement = Boolean(document.querySelector('.site-section--post, #page[data-service]'));
return matchesPostPath || hasPostElement;
}
function isPostListPage() {
const path = window.location.pathname;
return /\/[^/]+\/user\/[^/]+(?:\/)?$/.test(path) && !/\/post\//.test(path);
}
function formatPublishedDate(rawDate) {
if (!rawDate) return '';
const trimmed = rawDate.replace(/Published:\s*/i, '').trim();
const isoMatch = trimmed.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})(?::\d{2})?/);
if (isoMatch) {
return `${isoMatch[1]} ${isoMatch[2]}`;
}
return trimmed;
}
function sanitizeText(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function isSidebarOpen() {
const sidebar = document.querySelector('.global-sidebar');
const header = document.querySelector('.header');
const wrapper = document.querySelector('.content-wrapper');
const isRetracted = sidebar?.classList.contains('retracted') ||
header?.classList.contains('sidebar-retracted');
if (isRetracted) return false;
const isShifted = wrapper?.classList.contains('shifted');
if (isShifted) return true;
if (sidebar) {
const rect = sidebar.getBoundingClientRect();
if (rect.right > 50 && rect.left >= -10) return true;
}
return false;
}
function updateSidebarPosition() {
const prevCard = document.getElementById('pawchive-card-prev');
if (!prevCard) return;
const open = isSidebarOpen();
const targetLeft = open ? `calc(${CONFIG.sidebarWidthRem}rem + 12px)` : '12px';
prevCard.style.left = targetLeft;
log('Sidebar position updated. Open:', open, 'Left:', targetLeft);
}
function indexPostListIfPresent() {
const postCards = document.querySelectorAll('article.post-card');
if (!postCards.length) return;
log(`Indexing ${postCards.length} posts from current list page...`);
postCards.forEach(card => {
const id = card.getAttribute('data-id');
const link = card.querySelector('a.image-link, a[href*="/post/"]');
if (!link) return;
const url = link.href;
const titleEl = card.querySelector('.post-card__header');
const imgEl = card.querySelector('.post-card__image');
const timeEl = card.querySelector('time.timestamp');
const title = titleEl ? titleEl.textContent.trim() : '';
const thumbnail = imgEl ? (imgEl.getAttribute('data-src') || imgEl.src || '') : '';
const published = timeEl ? (timeEl.getAttribute('datetime') || timeEl.textContent.trim()) : '';
postCache.set(url, {
id: id || '',
title: title || 'Untitled Post',
thumbnail,
published: formatPublishedDate(published),
url
});
});
}
function indexCurrentPost() {
if (!isPostPage()) return;
const postSection = document.querySelector('.site-section--post, #page');
const id = postSection?.getAttribute('data-id') || window.location.pathname.split('/').filter(Boolean).pop();
const titleEl = document.querySelector('.post__title span') || document.querySelector('.post__title');
const pubMeta = document.querySelector('meta[name="published"]');
const pubEl = document.querySelector('.post__published');
const imgEl = document.querySelector('.post__thumbnail img, .post__files img, .post__content img');
let title = titleEl ? titleEl.textContent.trim() : '';
if (!title) {
title = document.title.replace(/\s*\|\s*Pawchive$/i, '').trim();
}
const published = pubMeta?.content || (pubEl ? pubEl.textContent : '');
const thumbnail = imgEl ? (imgEl.getAttribute('data-src') || imgEl.src || '') : '';
postCache.set(window.location.href, {
id,
title: title || 'Untitled Post',
thumbnail,
published: formatPublishedDate(published),
url: window.location.href
});
}
function getAdjacentNavLinks() {
const prevLink = document.querySelector('a.post__nav-link.prev');
const nextLink = document.querySelector('a.post__nav-link.next');
return {
prevUrl: prevLink && prevLink.href ? prevLink.href : null,
nextUrl: nextLink && nextLink.href ? nextLink.href : null
};
}
let activeAbortController = null;
async function fetchPostMetadata(url, signal) {
if (!url) return null;
const cached = postCache.get(url);
if (cached) {
log('Cache hit for:', url);
return cached;
}
log('Fetching metadata for:', url);
try {
const timeoutSignal = AbortSignal.timeout(CONFIG.requestTimeoutMs);
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml'
},
credentials: 'same-origin',
signal: combinedSignal
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const id = doc.querySelector('meta[name="id"]')?.content ||
doc.querySelector('.site-section--post')?.getAttribute('data-id') ||
url.split('/').filter(Boolean).pop();
let title = doc.querySelector('.post__title span')?.textContent?.trim() ||
doc.querySelector('.post__title')?.textContent?.trim();
if (!title) {
const ogTitle = doc.querySelector('meta[property="og:title"]')?.content;
if (ogTitle) {
const m = ogTitle.match(/^"([^"]+)"/);
title = m ? m[1] : ogTitle;
} else {
title = doc.title.replace(/\s*\|\s*Pawchive$/i, '').trim();
}
}
const pubMeta = doc.querySelector('meta[name="published"]')?.content;
const pubDiv = doc.querySelector('.post__published')?.textContent;
const timeTag = doc.querySelector('time.timestamp')?.getAttribute('datetime') ||
doc.querySelector('time.timestamp')?.textContent;
const published = pubMeta || pubDiv || timeTag || '';
let thumbnail = '';
const thumbImg = doc.querySelector('.post__thumbnail img, .post__files img, .post__attachment img, .fileThumb img, .post__content img');
if (thumbImg) {
thumbnail = thumbImg.getAttribute('data-src') || thumbImg.getAttribute('src') || '';
}
if (thumbnail) {
try {
thumbnail = new URL(thumbnail, window.location.origin).href;
} catch {
}
}
const postData = {
id,
title: title || 'Untitled Post',
thumbnail,
published: formatPublishedDate(published),
url
};
postCache.set(url, postData);
return postData;
} catch (err) {
if (err.name === 'AbortError') {
log('Fetch aborted for', url);
return null;
}
logError('Failed to fetch post metadata for', url, err);
return {
id: url.split('/').filter(Boolean).pop(),
title: 'View Post',
thumbnail: '',
published: '',
url
};
}
}
function injectStyles() {
if (document.getElementById('pawchive-navigator-styles')) return;
const style = document.createElement('style');
style.id = 'pawchive-navigator-styles';
style.textContent = `
.pawchive-nav-card {
position: fixed;
bottom: 110px;
width: ${CONFIG.cardWidthPx}px;
height: ${CONFIG.cardHeightPx}px;
background: rgba(28, 30, 32, 0.94);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
text-decoration: none;
color: #e0e0e0;
z-index: 900;
user-select: none;
cursor: pointer;
box-sizing: border-box;
transition: left 0.25s ease-in-out, transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
border-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
}
.pawchive-nav-card:hover {
transform: translateY(-2px);
border-color: #ffd05b;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.75), 0 0 10px rgba(255, 208, 91, 0.25);
}
.pawchive-nav-card.pawchive-prev {
left: 12px;
}
body:has(.content-wrapper.shifted):not(:has(.header.sidebar-retracted)):not(:has(.global-sidebar.retracted)) .pawchive-nav-card.pawchive-prev {
left: calc(${CONFIG.sidebarWidthRem}rem + 12px);
}
body:has(.header.sidebar-retracted) .pawchive-nav-card.pawchive-prev,
body:has(.global-sidebar.retracted) .pawchive-nav-card.pawchive-prev,
body:not(:has(.content-wrapper.shifted)) .pawchive-nav-card.pawchive-prev {
left: 12px;
}
.pawchive-nav-card.pawchive-next {
right: 12px;
}
.pawchive-nav-tooltip {
position: absolute;
top: -30px;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
background: #181a1b;
color: #ffd05b;
border: 1px solid rgba(255, 208, 91, 0.38);
border-radius: 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.2px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.2s ease, transform 0.2s ease;
white-space: nowrap;
z-index: 910;
}
.pawchive-prev .pawchive-nav-tooltip {
left: 0;
}
.pawchive-next .pawchive-nav-tooltip {
right: 0;
}
.pawchive-nav-card:hover .pawchive-nav-tooltip {
opacity: 1;
transform: translateY(0);
}
.pawchive-nav-body {
display: flex;
flex-direction: row;
align-items: stretch;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 7px;
}
.pawchive-nav-text-col {
flex: 1;
min-width: 0;
padding: 8px 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
background: transparent;
}
.pawchive-nav-title {
font-size: 12px;
font-weight: 600;
line-height: 1.35;
color: #f0f2f5;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.pawchive-nav-card:hover .pawchive-nav-title {
color: #ffffff;
}
.pawchive-nav-date {
font-size: 10.5px;
color: #8f959c;
margin-top: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 4px;
}
.pawchive-nav-date svg {
opacity: 0.7;
flex-shrink: 0;
}
.pawchive-nav-thumb-col {
width: ${CONFIG.thumbWidthPx}px;
height: 100%;
flex-shrink: 0;
overflow: hidden;
position: relative;
background: #141617;
display: flex;
align-items: center;
justify-content: center;
}
.pawchive-nav-thumb {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.pawchive-nav-card:hover .pawchive-nav-thumb {
transform: scale(1.05);
}
.pawchive-no-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
background: #151718;
color: #71767c;
font-size: 9.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
user-select: none;
padding: 4px;
box-sizing: border-box;
text-align: center;
}
.pawchive-no-preview svg {
opacity: 0.55;
}
.pawchive-nav-skeleton .pawchive-nav-title {
background: linear-gradient(90deg, #25282a 25%, #34383c 50%, #25282a 75%);
background-size: 200% 100%;
animation: pawNavShimmer 1.5s infinite;
height: 24px;
border-radius: 4px;
color: transparent;
}
.pawchive-nav-skeleton .pawchive-nav-date {
background: linear-gradient(90deg, #25282a 25%, #34383c 50%, #25282a 75%);
background-size: 200% 100%;
animation: pawNavShimmer 1.5s infinite;
height: 12px;
width: 65%;
border-radius: 3px;
color: transparent;
}
@keyframes pawNavShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@media (max-width: 900px) {
.pawchive-nav-card {
width: 220px;
height: 80px;
bottom: 80px;
}
.pawchive-nav-thumb-col {
width: 72px;
}
.pawchive-nav-title {
font-size: 11px;
}
.pawchive-nav-date {
font-size: 9.5px;
}
}
@media (max-width: 520px) {
.pawchive-nav-card {
display: none !important;
}
}
`;
document.head.appendChild(style);
}
function createThumbnailElement(thumbnailUrl, altText) {
const thumbCol = document.createElement('div');
thumbCol.className = 'pawchive-nav-thumb-col';
if (thumbnailUrl) {
const img = document.createElement('img');
img.className = 'pawchive-nav-thumb';
img.src = thumbnailUrl;
img.alt = altText || 'Post thumbnail';
img.loading = 'lazy';
img.onerror = () => {
thumbCol.innerHTML = `
<div class="pawchive-no-preview">
${SVG_ICONS.noPreview}
<span>No Preview</span>
</div>
`;
};
thumbCol.appendChild(img);
} else {
thumbCol.innerHTML = `
<div class="pawchive-no-preview">
${SVG_ICONS.noPreview}
<span>No Preview</span>
</div>
`;
}
return thumbCol;
}
function createTextElement(title, published) {
const textCol = document.createElement('div');
textCol.className = 'pawchive-nav-text-col';
const titleDiv = document.createElement('div');
titleDiv.className = 'pawchive-nav-title';
titleDiv.title = title || '';
titleDiv.textContent = title || 'Loading...';
const dateDiv = document.createElement('div');
dateDiv.className = 'pawchive-nav-date';
if (published) {
dateDiv.innerHTML = `${SVG_ICONS.clock} <span>${sanitizeText(published)}</span>`;
} else {
dateDiv.innerHTML = `${SVG_ICONS.clock} <span>Date unavailable</span>`;
}
textCol.appendChild(titleDiv);
textCol.appendChild(dateDiv);
return textCol;
}
function createCardElement(type, url, postData) {
const isPrev = type === 'prev';
const card = document.createElement('a');
card.id = isPrev ? 'pawchive-card-prev' : 'pawchive-card-next';
card.className = `pawchive-nav-card ${isPrev ? 'pawchive-prev' : 'pawchive-next'}`;
card.href = url;
card.title = isPrev ? 'Go to Previous Post' : 'Go to Next post';
const tooltip = document.createElement('div');
tooltip.className = 'pawchive-nav-tooltip';
tooltip.innerHTML = isPrev
? `${SVG_ICONS.arrowLeft} <span>Go to Previous Post</span>`
: `<span>Go to Next post</span> ${SVG_ICONS.arrowRight}`;
card.appendChild(tooltip);
const body = document.createElement('div');
body.className = 'pawchive-nav-body';
if (!postData) {
card.classList.add('pawchive-nav-skeleton');
const textCol = createTextElement('Loading post...', '');
const thumbCol = createThumbnailElement('', '');
if (isPrev) {
body.appendChild(textCol);
body.appendChild(thumbCol);
} else {
body.appendChild(thumbCol);
body.appendChild(textCol);
}
} else {
const textCol = createTextElement(postData.title, postData.published);
const thumbCol = createThumbnailElement(postData.thumbnail, postData.title);
if (isPrev) {
body.appendChild(textCol);
body.appendChild(thumbCol);
} else {
body.appendChild(thumbCol);
body.appendChild(textCol);
}
}
card.appendChild(body);
return card;
}
function updateCardContent(card, type, postData) {
if (!card || !postData) return;
const isPrev = type === 'prev';
card.classList.remove('pawchive-nav-skeleton');
card.href = postData.url;
const body = card.querySelector('.pawchive-nav-body');
if (!body) return;
body.innerHTML = '';
const textCol = createTextElement(postData.title, postData.published);
const thumbCol = createThumbnailElement(postData.thumbnail, postData.title);
if (isPrev) {
body.appendChild(textCol);
body.appendChild(thumbCol);
} else {
body.appendChild(thumbCol);
body.appendChild(textCol);
}
}
function removeCards() {
document.getElementById('pawchive-card-prev')?.remove();
document.getElementById('pawchive-card-next')?.remove();
}
async function updateNavigator() {
if (!isPostPage()) {
removeCards();
if (isPostListPage()) {
indexPostListIfPresent();
}
return;
}
injectStyles();
indexCurrentPost();
const { prevUrl, nextUrl } = getAdjacentNavLinks();
log('Adjacent links detected:', { prevUrl, nextUrl });
if (!prevUrl && !nextUrl) {
removeCards();
return;
}
if (activeAbortController) {
activeAbortController.abort();
}
activeAbortController = new AbortController();
const { signal } = activeAbortController;
const container = document.body;
let prevCard = document.getElementById('pawchive-card-prev');
if (prevUrl) {
const cachedPrev = postCache.get(prevUrl);
if (!prevCard) {
prevCard = createCardElement('prev', prevUrl, cachedPrev);
container.appendChild(prevCard);
} else {
prevCard.href = prevUrl;
if (cachedPrev) updateCardContent(prevCard, 'prev', cachedPrev);
}
updateSidebarPosition();
if (!cachedPrev) {
fetchPostMetadata(prevUrl, signal).then(data => {
if (data && prevCard && prevCard.href === data.url) {
updateCardContent(prevCard, 'prev', data);
updateSidebarPosition();
}
});
}
} else if (prevCard) {
prevCard.remove();
}
let nextCard = document.getElementById('pawchive-card-next');
if (nextUrl) {
const cachedNext = postCache.get(nextUrl);
if (!nextCard) {
nextCard = createCardElement('next', nextUrl, cachedNext);
container.appendChild(nextCard);
} else {
nextCard.href = nextUrl;
if (cachedNext) updateCardContent(nextCard, 'next', cachedNext);
}
if (!cachedNext) {
fetchPostMetadata(nextUrl, signal).then(data => {
if (data && nextCard && nextCard.href === data.url) {
updateCardContent(nextCard, 'next', data);
}
});
}
} else if (nextCard) {
nextCard.remove();
}
}
let debounceTimer = null;
function scheduleUpdate() {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
updateNavigator();
}, 50);
}
function initObservers() {
document.body.addEventListener('htmx:afterSettle', () => {
scheduleUpdate();
updateSidebarPosition();
});
document.body.addEventListener('htmx:afterSwap', () => {
scheduleUpdate();
updateSidebarPosition();
});
document.body.addEventListener('htmx:historyRestore', () => {
scheduleUpdate();
updateSidebarPosition();
});
window.addEventListener('popstate', scheduleUpdate);
const originalPushState = history.pushState;
if (originalPushState) {
history.pushState = function (...args) {
const result = originalPushState.apply(this, args);
scheduleUpdate();
return result;
};
}
const originalReplaceState = history.replaceState;
if (originalReplaceState) {
history.replaceState = function (...args) {
const result = originalReplaceState.apply(this, args);
scheduleUpdate();
return result;
};
}
document.addEventListener('click', (e) => {
const target = e.target;
if (target && target.closest && target.closest('#burgor, .close-sidebar, .backdrop, .global-sidebar-entry-item')) {
requestAnimationFrame(updateSidebarPosition);
setTimeout(updateSidebarPosition, 60);
setTimeout(updateSidebarPosition, 270);
}
}, { passive: true });
window.addEventListener('resize', updateSidebarPosition, { passive: true });
const observer = new MutationObserver(mutations => {
let shouldUpdateCards = false;
let shouldUpdateSidebar = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.id === 'main' || node.classList?.contains('site-section--post') || node.querySelector?.('.site-section--post')) {
shouldUpdateCards = true;
break;
}
}
}
} else if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target;
if (target.classList?.contains('global-sidebar') ||
target.classList?.contains('content-wrapper') ||
target.classList?.contains('header')) {
shouldUpdateSidebar = true;
}
}
}
if (shouldUpdateCards) scheduleUpdate();
if (shouldUpdateSidebar) updateSidebarPosition();
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
}
function init() {
log('Initializing Pawchive Post Navigator...');
initObservers();
scheduleUpdate();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();