(function () { function byId(id) { return document.getElementById(id); } /* ======================================================================== Theme Toggle (localStorage) ======================================================================== */ function applyTheme(theme) { var isLight = theme === "light"; document.body.classList.add("theme-transitioning"); window.setTimeout(function () { document.body.classList.remove("theme-transitioning"); }, 400); if (theme === "light") { document.body.classList.add("light-mode"); } else { document.body.classList.remove("light-mode"); } document.documentElement.style.colorScheme = isLight ? "light" : "dark"; var themeColor = document.querySelector('meta[name="theme-color"]'); if (themeColor) { themeColor.setAttribute("content", isLight ? "#fff8f1" : "#000000"); } var btn = byId("theme-toggle"); if (btn) { btn.textContent = theme === "light" ? "\u263E" : "\u2600"; btn.setAttribute("title", theme === "light" ? "Switch to dark theme" : "Switch to light theme" ); btn.setAttribute("aria-label", theme === "light" ? "Switch to dark theme" : "Switch to light theme" ); } // Sync guestbook iframes ["guestbook-form-iframe", "guestbook-comments-iframe"].forEach(function (id) { var frame = byId(id); if (!frame) return; var src = frame.getAttribute("src") || ""; var base = src.split("?")[0]; var next = theme === "light" ? base + "?theme=light" : base; if (src !== next) frame.setAttribute("src", next); }); } var stored = localStorage.getItem("siteTheme"); var prefersLight = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches; var savedTheme = stored || (prefersLight ? "light" : "dark"); applyTheme(savedTheme); if (window.matchMedia) { window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", function (e) { if (!localStorage.getItem("siteTheme")) { applyTheme(e.matches ? "light" : "dark"); } }); } function syncHeaderOffset() { var header = document.querySelector("header.site-header"); if (!header) return; document.documentElement.style.setProperty( "--site-header-height", Math.ceil(header.getBoundingClientRect().height) + "px" ); } window.addEventListener("load", syncHeaderOffset); window.addEventListener("resize", syncHeaderOffset); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", syncHeaderOffset); } else { syncHeaderOffset(); } var themeBtn = byId("theme-toggle"); if (themeBtn) { themeBtn.addEventListener("click", function () { var next = document.body.classList.contains("light-mode") ? "dark" : "light"; localStorage.setItem("siteTheme", next); applyTheme(next); }); } /* ======================================================================== Comfort spacing toggle (localStorage, site-wide) ======================================================================== */ (function initComfortSpacing() { if ((location.pathname || "").indexOf("/startpage") !== -1) return; var KEY = "siteComfortSpacing_v1"; var btn = byId("comfort-toggle"); if (!btn) return; function applyComfort(on) { document.body.classList.toggle("comfort-spacing", on); btn.setAttribute("aria-pressed", on ? "true" : "false"); btn.setAttribute("title", on ? "Comfort spacing: ON" : "Comfort spacing: OFF"); btn.setAttribute("aria-label", on ? "Comfort spacing on" : "Comfort spacing off"); } applyComfort(localStorage.getItem(KEY) === "1"); btn.addEventListener("click", function () { var next = !document.body.classList.contains("comfort-spacing"); localStorage.setItem(KEY, next ? "1" : "0"); applyComfort(next); }); })(); /* ======================================================================== Menu & Language Dropdowns ======================================================================== */ const menuBtn = byId("menu-toggle"); const nav = byId("site-nav"); const langToggle = byId("lang-toggle"); const langMenu = byId("lang-options"); const currentLang = byId("current-lang"); let openDropdown = null; function closeMenu() { if (!menuBtn || !nav) return; menuBtn.classList.remove("active"); menuBtn.setAttribute("aria-expanded", "false"); nav.hidden = true; if (openDropdown === "menu") openDropdown = null; } function closeLang() { if (!langToggle || !langMenu) return; langToggle.setAttribute("aria-expanded", "false"); langMenu.hidden = true; if (openDropdown === "lang") openDropdown = null; } function toggleMenu(e) { if (!menuBtn || !nav) return; e.stopPropagation(); if (openDropdown === "lang") closeLang(); const open = menuBtn.getAttribute("aria-expanded") === "true"; if (open) { closeMenu(); return; } menuBtn.classList.add("active"); menuBtn.setAttribute("aria-expanded", "true"); nav.hidden = false; openDropdown = "menu"; } function toggleLang(e) { if (!langToggle || !langMenu) return; e.stopPropagation(); if (openDropdown === "menu") closeMenu(); const open = langToggle.getAttribute("aria-expanded") === "true"; if (open) { closeLang(); return; } langToggle.setAttribute("aria-expanded", "true"); langMenu.hidden = false; openDropdown = "lang"; } if (nav) nav.hidden = true; if (langMenu) langMenu.hidden = true; if (menuBtn && nav) { menuBtn.addEventListener("click", toggleMenu); nav.addEventListener("click", (e) => e.stopPropagation()); } if (langToggle && langMenu) { langToggle.addEventListener("click", toggleLang); langMenu.addEventListener("click", (e) => e.stopPropagation()); } if ((menuBtn && nav) || (langToggle && langMenu)) { document.addEventListener("click", function (e) { if ( menuBtn && nav && (menuBtn.contains(e.target) || nav.contains(e.target)) ) { return; } if ( langToggle && langMenu && (langToggle.contains(e.target) || langMenu.contains(e.target)) ) { return; } closeMenu(); closeLang(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") { closeMenu(); closeLang(); } }); } if (currentLang) { const docLang = (document.documentElement.lang || "en").toLowerCase(); currentLang.textContent = docLang === "zh" ? "ZH" : docLang === "ja" ? "JP" : "EN"; } if (langMenu) { langMenu.querySelectorAll("a[data-lang]").forEach((a) => { a.addEventListener("click", function () { localStorage.setItem("preferredLang", this.getAttribute("data-lang") || "en"); }); }); } // Language menu hrefs: convert current page to *_zh.php / *_jp.php. function updateLangMenuHrefs() { const links = document.querySelectorAll("a[data-lang-href][data-lang]"); if (!links.length) return; let path = window.location.pathname || "/"; if (!path.startsWith("/")) path = "/" + path; const isDir = path.endsWith("/"); const file = isDir ? "index.php" : (path.split("/").pop() || "index.php"); const base = isDir ? path : path.slice(0, -file.length); const baseName = file.replace(/_(zh|jp)\.php$/i, ".php"); const map = { en: baseName, zh: baseName.replace(/(_(zh|jp))?\.php$/i, "_zh.php"), jp: baseName.replace(/(_(zh|jp))?\.php$/i, "_jp.php"), }; links.forEach((a) => { const lang = (a.getAttribute("data-lang") || "en").toLowerCase(); const targetFile = map[lang] || map.en; a.href = base + targetFile; }); } function updateAriaCurrent() { const path = window.location.pathname || "/"; const normalized = path.startsWith("/") ? path : "/" + path; const isDir = normalized.endsWith("/"); const file = isDir ? "index.php" : (normalized.split("/").pop() || "index.php"); const base = isDir ? normalized : normalized.slice(0, -file.length); const indexPath = base + "index.php"; const dirPath = base || "/"; const candidates = new Set([normalized, indexPath, dirPath, dirPath.replace(/\/$/, "")]); document.querySelectorAll("#site-nav a[href]").forEach((a) => { const href = a.getAttribute("href") || ""; const hrefUrl = new URL(href, window.location.origin); if (hrefUrl.origin !== window.location.origin) { a.removeAttribute("aria-current"); return; } const hrefPath = hrefUrl.pathname; if (candidates.has(hrefPath)) { a.setAttribute("aria-current", "page"); } else { a.removeAttribute("aria-current"); } }); const docLang = (document.documentElement.lang || "en").toLowerCase(); // "ja" is valid BCP-47 for Japanese but we label it "jp" in data-lang const normalizedDocLang = docLang === "ja" ? "jp" : docLang; document.querySelectorAll("#lang-options a[data-lang]").forEach((a) => { const lang = (a.getAttribute("data-lang") || "en").toLowerCase(); if (lang === normalizedDocLang) { a.setAttribute("aria-current", "page"); } else { a.removeAttribute("aria-current"); } }); } // Run once now... updateLangMenuHrefs(); updateAriaCurrent(); // ...and again after layout/injection settles. setTimeout(updateLangMenuHrefs, 0); setTimeout(updateAriaCurrent, 0); /* ======================================================================== Skeleton Screens — replace static "Loading…" text ======================================================================== */ function injectSkeleton(container) { if (!container) return; container.innerHTML = '
' + '' + '' + ''; } /* ======================================================================== Home page: Blog list (optional) ======================================================================== */ const blogList = byId("blog-list"); if (blogList) { injectSkeleton(blogList); const blogHost = "https://blog.sillylaird.ca"; const ctrl = new AbortController(); const timeout = setTimeout(() => ctrl.abort(), 3500); fetch(blogHost + "/api/posts.php", { signal: ctrl.signal }) .then((r) => r.json()) .then((posts) => { blogList.textContent = ""; const top = document.createElement("div"); const strong = document.createElement("strong"); strong.textContent = "Posts"; top.appendChild(strong); top.appendChild(document.createTextNode(" (")); const allLink = document.createElement("a"); allLink.href = blogHost; allLink.textContent = "all"; top.appendChild(allLink); top.appendChild(document.createTextNode(")")); blogList.appendChild(top); const list = document.createElement("ol"); (posts || []).forEach((post) => { const li = document.createElement("li"); const a = document.createElement("a"); a.href = blogHost + "/" + String(post.path || "").replace(/^\/+/, ""); a.textContent = String(post.title || "Untitled"); const date = document.createElement("div"); date.className = "muted"; const y = String(post.year || ""); const m = String(post.month || "").padStart(2, "0"); const d = String(post.day || "").padStart(2, "0"); const iso = y && m && d ? `${y}-${m}-${d}` : ""; const time = document.createElement("time"); if (iso) time.setAttribute("datetime", iso); time.textContent = iso || ""; li.appendChild(a); li.appendChild(document.createElement("br")); date.appendChild(time); li.appendChild(date); list.appendChild(li); }); blogList.appendChild(list); if (!list.children.length) { const empty = document.createElement("div"); empty.className = "muted"; empty.textContent = "No posts yet."; blogList.appendChild(empty); } }) .catch((err) => { blogList.textContent = err && err.name === "AbortError" ? "Blog list timed out." : "Cannot load posts right now."; }) .finally(() => clearTimeout(timeout)); } /* ======================================================================== Home page: Changelog list (optional) ======================================================================== */ const changelogList = byId("changelog-list"); if (changelogList) { injectSkeleton(changelogList); const changelogPage = "https://www.sillylaird.ca/changelog/latest.php"; const ctrl = new AbortController(); const timeout = setTimeout(() => ctrl.abort(), 3500); fetch(changelogPage, { signal: ctrl.signal }) .then((r) => r.text()) .then((html) => { changelogList.textContent = ""; const doc = new DOMParser().parseFromString(html, "text/html"); const entries = Array.from(doc.querySelectorAll(".changelog-entry")); const top = document.createElement("div"); const strong = document.createElement("strong"); strong.textContent = "Entries"; top.appendChild(strong); top.appendChild(document.createTextNode(" (")); const allLink = document.createElement("a"); allLink.href = "https://www.sillylaird.ca/changelog/"; allLink.textContent = "all"; top.appendChild(allLink); top.appendChild(document.createTextNode(")")); changelogList.appendChild(top); const list = document.createElement("ol"); entries.forEach((entry) => { const li = document.createElement("li"); const titleText = (entry.querySelector("strong, b")?.textContent || "").trim(); const timeEl = entry.querySelector("time"); const dateText = (timeEl?.textContent || "").trim(); const dateIso = (timeEl?.getAttribute("datetime") || "").trim(); const bodyEl = entry.querySelector(".text"); const bodyText = (bodyEl?.textContent || "").trim(); const title = document.createElement("strong"); if (titleText) { title.textContent = titleText; li.appendChild(title); if (dateText) { const date = document.createElement("div"); date.className = "muted"; const time = document.createElement("time"); if (dateIso) time.setAttribute("datetime", dateIso); time.textContent = dateText; date.appendChild(time); li.appendChild(date); } } else if (dateText) { title.textContent = dateText; li.appendChild(title); } if (bodyText) { const body = document.createElement("div"); body.className = "changelog-body"; body.textContent = bodyText; li.appendChild(body); } list.appendChild(li); }); changelogList.appendChild(list); const filesSection = doc.querySelector(".changelog-files-section"); if (filesSection) { changelogList.appendChild(document.importNode(filesSection, true)); } if (!list.children.length) { const empty = document.createElement("div"); empty.className = "muted"; empty.textContent = "No updates yet."; changelogList.appendChild(empty); } }) .catch((err) => { changelogList.textContent = err && err.name === "AbortError" ? "Changelog list timed out." : "Cannot load changelog right now."; }) .finally(() => clearTimeout(timeout)); } /* ======================================================================== Guestbook helpers ======================================================================== */ const formRefreshBtn = byId("guestbook-form-refresh"); if (formRefreshBtn) { formRefreshBtn.addEventListener("click", () => { const frame = byId("guestbook-form-iframe"); if (!frame) return; setIframeLoading(frame); resetIframe(frame); }); } function setIframeLoading(frame) { if (!frame) return; const clip = frame.closest(".iframe-clip"); if (!clip) return; clip.classList.add("is-loading"); const onLoad = () => { clip.classList.remove("is-loading"); }; frame.addEventListener("load", onLoad, { once: true }); } function resetIframe(frame) { if (!frame) return; const src = frame.getAttribute("src") || frame.src; if (src) { frame.setAttribute("src", src); return; } if (frame.contentWindow) frame.contentWindow.location.reload(); } function loadGuestbookComments() { var box = byId("guestbook-comments-inline"); if (!box) return; box.innerHTML = 'Could not load comments. Open guestbook
'; }); } var gbReload = byId("guestbook-comments-reload"); if (gbReload) { gbReload.addEventListener("click", loadGuestbookComments); loadGuestbookComments(); } document.querySelectorAll(".iframe-clip iframe, .vibe-embed iframe").forEach(function (frame) { var clip = frame.closest(".iframe-clip") || frame.closest(".vibe-embed"); if (!clip) return; clip.classList.add("is-loading"); frame.addEventListener("load", function () { clip.classList.remove("is-loading"); }, { once: true }); }); /* ======================================================================== Home page: Last.fm live refresh (The "Pro" Way) ======================================================================== */ const lastfmList = byId("lastfm-list"); const lastfmNow = byId("lastfm-now"); if (lastfmList || lastfmNow) { let lastDataStr = ""; function renderNowPlaying(tracks) { if (!lastfmNow) return; lastfmNow.textContent = ""; var current = null; if (Array.isArray(tracks)) { for (var i = 0; i < tracks.length; i++) { if (tracks[i].nowplaying) { current = tracks[i]; break; } } if (!current && tracks[0]) current = tracks[0]; } if (!current) { var fallback = document.createElement("p"); fallback.className = "muted"; fallback.innerHTML = 'Nothing scrobbling — Last.fm profile'; lastfmNow.appendChild(fallback); return; } var card = document.createElement("a"); card.className = "lastfm-track lastfm-track--featured"; card.href = current.url || "https://www.last.fm/user/SillyLaird"; card.target = "_blank"; card.rel = "noopener noreferrer"; if (current.image) { var wrap = document.createElement("span"); wrap.className = "lastfm-image-wrap"; var img = document.createElement("img"); img.src = current.image; img.alt = (current.artist || "") + " — " + (current.name || "track"); img.loading = "lazy"; img.decoding = "async"; wrap.appendChild(img); card.appendChild(wrap); } var info = document.createElement("span"); info.className = "lastfm-info"; var status = document.createElement("span"); status.className = "lastfm-status" + (current.nowplaying ? " is-playing" : ""); status.textContent = current.nowplaying ? "Now playing" : "Last scrobbled"; var title = document.createElement("span"); title.className = "lastfm-title"; title.textContent = current.name || "Unknown track"; var artist = document.createElement("span"); artist.className = "lastfm-artist"; artist.textContent = current.artist || ""; info.appendChild(status); info.appendChild(title); info.appendChild(artist); card.appendChild(info); lastfmNow.appendChild(card); } function updateLastFM() { fetch("/api/lastfm.php") .then(res => { if (!res.ok) throw new Error("API error: " + res.status); return res.json(); }) .then(tracks => { if (tracks.error) throw new Error(tracks.message || "Unknown proxy error"); // Only update DOM if the data has changed const currentDataStr = JSON.stringify(tracks); if (currentDataStr === lastDataStr) return; lastDataStr = currentDataStr; renderNowPlaying(tracks); if (!lastfmList) return; var ul = document.createElement("ul"); ul.className = "lastfm-text-list"; if (Array.isArray(tracks) && tracks.length > 0) { tracks.forEach(function (track) { var li = document.createElement("li"); if (track.nowplaying) { var strong = document.createElement("strong"); strong.textContent = "\u25B6 Now playing: "; li.appendChild(strong); } var a = document.createElement("a"); a.href = track.url; a.target = "_blank"; a.rel = "noopener noreferrer"; a.textContent = track.name; li.appendChild(a); li.appendChild(document.createTextNode(" \u2014 " + track.artist)); ul.appendChild(li); }); } else { var empty = document.createElement("li"); empty.className = "muted"; empty.textContent = "No tracks found."; ul.appendChild(empty); } lastfmList.textContent = ""; lastfmList.appendChild(ul); lastfmList.dataset.loaded = "1"; if (lastfmNow) lastfmNow.dataset.loaded = "1"; }) .catch(err => { console.error("Last.fm Widget Error:", err); if (lastfmNow && !lastfmNow.dataset.loaded) { lastfmNow.dataset.loaded = "1"; lastfmNow.innerHTML = ''; } if (lastfmList && !lastfmList.dataset.loaded) { lastfmList.dataset.loaded = "1"; lastfmList.innerHTML = 'View recent tracks on Last.fm ↗
'; } }); } // Defer the first fetch until the widget scrolls into view (saves a // third-party request on initial paint — matters on slow/Tor connections). let refreshTimer = null; function startLastFM() { if (refreshTimer) return; updateLastFM(); refreshTimer = setInterval(updateLastFM, 30000); } if ("IntersectionObserver" in window) { const io = new IntersectionObserver(function (entries) { if (entries.some(function (e) { return e.isIntersecting; })) { io.disconnect(); startLastFM(); } }, { rootMargin: "200px" }); io.observe(lastfmList || lastfmNow); } else { startLastFM(); } // Manual refresh button const refreshBtn = byId("lastfm-refresh"); if (refreshBtn) { refreshBtn.addEventListener("click", () => { refreshBtn.textContent = "Updating..."; refreshBtn.disabled = true; updateLastFM(); setTimeout(() => { refreshBtn.textContent = "Refresh"; refreshBtn.disabled = false; }, 500); }); } } /* ======================================================================== Footer helpers ======================================================================== */ // Footer year: local time only. (function () { const el = byId("copyright-year"); if (!el) return; el.textContent = String(new Date().getFullYear()); })(); // Footer last-updated: based on document.lastModified. (function () { const el = byId("last-updated"); if (!el) return; const raw = document.lastModified; const date = raw ? new Date(raw) : null; if (!date || Number.isNaN(date.getTime())) return; const iso = date.toISOString().slice(0, 10); el.setAttribute("datetime", iso); el.textContent = iso; })(); /* ======================================================================== Force red text in same-origin iframes ======================================================================== */ var _gbFrameIds = new Set(["guestbook-form-iframe", "guestbook-comments-iframe"]); function forceIframeColors(frame) { if (_gbFrameIds.has(frame.id)) return; try { var doc = frame.contentDocument || frame.contentWindow.document; if (!doc) return; if (doc.getElementById('forced-colors-style')) return; var style = doc.createElement('style'); style.id = 'forced-colors-style'; style.textContent = ':root { --ink: #8b0000; --bg: #000000; }' + '* { color: #8b0000 !important; }' + 'a { color: #8b0000 !important; text-decoration: underline; }' + 'body { background-color: var(--bg, #000) !important; color: #8b0000 !important; }'; doc.head.appendChild(style); } catch (e) { // Cross-domain frames cannot be styled } } function attachIframeColorForcing() { document.querySelectorAll('iframe').forEach(function (frame) { if (frame._colorForced) return; frame._colorForced = true; frame.addEventListener('load', function () { forceIframeColors(frame); }); // Also try immediately in case already loaded forceIframeColors(frame); }); } window.addEventListener('load', attachIframeColorForcing); document.addEventListener('DOMContentLoaded', attachIframeColorForcing); /* ======================================================================== Global Header Search (unified search w/ engine picker) Mirrors /startpage/ behavior; uses gh-* IDs to avoid collisions. ======================================================================== */ var GH_SEARCH_ENGINES = [ { key: 'site', label: '⌂', title: 'This site', placeholder: 'Search pages on sillylaird.ca…', action: '#', param: 'q', local: true }, { key: 'google', label: 'G', title: 'Google Search', placeholder: 'Search Google...', action: 'https://www.google.ca/search', param: 'q' }, { key: 'youtube', label: 'YT', title: 'YouTube Search', placeholder: 'Search YouTube...', action: 'https://www.youtube.com/results', param: 'search_query' }, { key: 'sillylaird', label: 'SL', title: 'SillyLaird Search', placeholder: 'Search SillyLaird...', action: 'https://search.sillylaird.ca/search', param: 'q' }, { key: '4get', label: '4G', title: '4get Search', placeholder: 'Search 4get...', action: 'https://4get.sillylaird.ca/web', param: 's' }, { key: 'gemini', label: 'GM', title: 'Gemini Search', placeholder: 'Ask Gemini...', action: '/gemini-search.php', param: 'q' } ]; var GH_SEARCH_KEY = 'siteHeaderSearchEngine_v1'; function initGlobalHeaderSearch() { var form = byId('gh-search-form'); var input = byId('gh-search-input'); var toggle = byId('gh-search-engine-toggle'); var menu = byId('gh-search-engine-menu'); if (!form || !input || !toggle || !menu) return; if (toggle._ghInit) return; toggle._ghInit = true; var saved = null; try { saved = localStorage.getItem(GH_SEARCH_KEY); } catch (_) {} var idx = GH_SEARCH_ENGINES.findIndex(function (e) { return e.key === saved; }); if (idx < 0) idx = GH_SEARCH_ENGINES.findIndex(function (e) { return e.key === 'google'; }); if (idx < 0) idx = 0; GH_SEARCH_ENGINES.forEach(function (e, i) { var li = document.createElement('li'); li.setAttribute('role', 'option'); li.dataset.index = String(i); var icon = document.createElement('span'); icon.className = 'engine-icon'; icon.textContent = e.label; var name = document.createElement('span'); name.className = 'engine-name'; name.textContent = e.title; li.appendChild(icon); li.appendChild(name); li.addEventListener('click', function () { idx = i; apply(idx); closeMenu(); input.focus(); }); menu.appendChild(li); }); function apply(i) { var e = GH_SEARCH_ENGINES[i]; form.setAttribute('action', e.local ? '#' : e.action); if (e.local) { form.removeAttribute('target'); } else { form.setAttribute('target', '_blank'); form.setAttribute('rel', 'noopener noreferrer'); } input.setAttribute('name', e.local ? 'site-q' : e.param); input.setAttribute('placeholder', e.placeholder); toggle.textContent = ''; toggle.appendChild(document.createTextNode(e.label + ' ')); var caret = document.createElement('span'); caret.className = 'search-engine-caret'; caret.textContent = '▾'; toggle.appendChild(caret); toggle.setAttribute('title', 'Search engine: ' + e.title); Array.prototype.forEach.call(menu.children, function (li, li_i) { li.setAttribute('aria-selected', li_i === i ? 'true' : 'false'); }); try { localStorage.setItem(GH_SEARCH_KEY, e.key); } catch (_) {} } function openMenu() { menu.hidden = false; toggle.setAttribute('aria-expanded', 'true'); } function closeMenu() { menu.hidden = true; toggle.setAttribute('aria-expanded', 'false'); } toggle.addEventListener('click', function (ev) { ev.stopPropagation(); if (menu.hidden) openMenu(); else closeMenu(); }); document.addEventListener('click', function (ev) { if (menu.hidden) return; if (ev.target === toggle || toggle.contains(ev.target)) return; if (menu.contains(ev.target)) return; closeMenu(); }); document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && !menu.hidden) closeMenu(); }); var siteHits = byId('site-search-hits'); var siteIndex = null; function loadSiteIndex() { if (siteIndex) return Promise.resolve(siteIndex); return fetch('/assets/data/site-index.json') .then(function (r) { return r.json(); }) .then(function (j) { siteIndex = Array.isArray(j) ? j : []; return siteIndex; }) .catch(function () { siteIndex = []; return siteIndex; }); } function renderSiteHits(q) { if (!siteHits) return; var query = (q || '').trim().toLowerCase(); if (!query) { siteHits.hidden = true; siteHits.textContent = ''; return; } loadSiteIndex().then(function (pages) { var hits = pages.filter(function (p) { var hay = (p.title + ' ' + p.url + ' ' + (p.tags || '')).toLowerCase(); return hay.indexOf(query) !== -1; }).slice(0, 8); siteHits.textContent = ''; if (!hits.length) { siteHits.hidden = true; return; } hits.forEach(function (p) { var li = document.createElement('li'); var a = document.createElement('a'); a.href = p.url; if (/^https?:\/\//.test(p.url)) { a.target = '_blank'; a.rel = 'noopener noreferrer'; } a.textContent = p.title; li.appendChild(a); siteHits.appendChild(li); }); siteHits.hidden = false; }); } input.addEventListener('input', function () { if (GH_SEARCH_ENGINES[idx] && GH_SEARCH_ENGINES[idx].local) { renderSiteHits(input.value); } else if (siteHits) { siteHits.hidden = true; } }); form.addEventListener('submit', function (ev) { if (GH_SEARCH_ENGINES[idx] && GH_SEARCH_ENGINES[idx].local) { ev.preventDefault(); var first = siteHits && siteHits.querySelector('a'); if (first) first.click(); else renderSiteHits(input.value); } }); apply(idx); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initGlobalHeaderSearch); } else { initGlobalHeaderSearch(); } (function initRadioWidget() { var box = byId('radio-widget'); if (!box) return; function render(title) { box.textContent = ''; var p = document.createElement('p'); if (title) { p.className = 'radio-now-line'; var strong = document.createElement('strong'); strong.textContent = 'Radio: '; p.appendChild(strong); var span = document.createElement('span'); span.id = 'radio-nowplaying-title'; span.textContent = title; p.appendChild(span); p.appendChild(document.createTextNode(' — ')); var a = document.createElement('a'); a.href = 'https://radio.sillylaird.ca/'; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = 'Listen live'; p.appendChild(a); } else { p.className = 'muted'; p.textContent = 'Radio offline — '; var link = document.createElement('a'); link.href = 'https://radio.sillylaird.ca/'; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.textContent = 'radio.sillylaird.ca'; p.appendChild(link); } box.appendChild(p); } function poll() { fetch('/api/nowplaying.php', { cache: 'no-store' }) .then(function (r) { return r.ok ? r.json() : Promise.reject(); }) .then(function (j) { var title = j && j.title ? String(j.title).trim() : ''; render(title); }) .catch(function () { render(''); }); } poll(); setInterval(poll, 30000); })(); (function initLainEgg() { var btn = byId('lain-avatar-btn'); if (!btn) return; var secrets = [ btn.getAttribute('data-secret-1'), btn.getAttribute('data-secret-2'), btn.getAttribute('data-secret-3'), ].filter(Boolean); var i = 0; btn.addEventListener('click', function () { var msg = secrets[i % secrets.length] || "Let's all love Lain."; i += 1; btn.classList.add('is-lain-pulse'); window.setTimeout(function () { btn.classList.remove('is-lain-pulse'); }, 600); var toast = document.createElement('div'); toast.className = 'lain-toast'; toast.setAttribute('role', 'status'); toast.textContent = msg; document.body.appendChild(toast); window.setTimeout(function () { toast.classList.add('is-visible'); }, 10); window.setTimeout(function () { toast.classList.remove('is-visible'); window.setTimeout(function () { toast.remove(); }, 350); }, 2800); }); })(); // Per-page visit counter: canonical implementation is visit-counter.js // (nginx injects it network-wide). Fall back here if that script is absent. (function initVisitCounter() { if (window.__slVisitCounterInit) return; if (document.querySelector('script[src*="visit-counter.js"]')) return; var s = document.createElement('script'); // Always load from www so sibling hosts (blog/radio/…) resolve correctly // even when this file itself was loaded cross-origin via nav.js. s.src = 'https://www.sillylaird.ca/assets/js/visit-counter.js?v=2026080403'; s.defer = true; document.head.appendChild(s); })(); // Scroll-to-top: canonical implementation is scroll-to-top.js (nginx injects // it network-wide alongside the visit counter). Fall back if absent. (function initScrollToTop() { if (window.__slScrollTopInit) return; if (document.querySelector('script[src*="scroll-to-top.js"]')) return; var s = document.createElement('script'); s.src = 'https://www.sillylaird.ca/assets/js/scroll-to-top.js?v=20260821b'; s.defer = true; document.head.appendChild(s); })(); (function initSectionDraw() { var titles = document.querySelectorAll('.section-accent-title, .network-subtitle'); var sections = document.querySelectorAll('.home-content > section'); var targets = []; titles.forEach(function (el) { targets.push(el); }); sections.forEach(function (el) { targets.push(el); }); if (!targets.length) return; function markDrawn(el) { el.classList.add('is-drawn'); } if (!('IntersectionObserver' in window) || window.matchMedia('(prefers-reduced-motion: reduce)').matches) { targets.forEach(markDrawn); return; } var io = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; markDrawn(entry.target); io.unobserve(entry.target); }); }, { threshold: 0.35, rootMargin: '0px 0px -8% 0px' }); targets.forEach(function (el) { io.observe(el); }); })(); (function initNetworkAccordion() { var items = document.querySelectorAll('.network-collapsible'); if (!items.length) return; var mq = window.matchMedia('(max-width: 600px)'); function sync() { var mobile = mq.matches; items.forEach(function (el, i) { el.open = mobile ? i === 0 : true; }); } sync(); if (typeof mq.addEventListener === 'function') { mq.addEventListener('change', sync); } else if (typeof mq.addListener === 'function') { mq.addListener(sync); } })(); /* ======================================================================== Vibe facade: click-to-load embeds (no provider request until clicked) The player iframe is created synchronously inside the click handler so Chrome allows autoplay (gesture-driven). State is read straight from each provider's postMessage protocol, so when the viewer pauses we show a "paused" overlay that mirrors the load facade but with a pause glyph. One provider entry per supported site. `state(data, iframe)` maps a raw message to 'paused' | 'playing' | null. Bilibili exposes no cross-origin pause event, so it has no entry and falls back to a plain autoplay iframe. ======================================================================== */ function vibePost(iframe, msg, origin) { try { iframe.contentWindow.postMessage(typeof msg === 'string' ? msg : JSON.stringify(msg), origin || '*'); } catch (_) {} } function withParam(url, extra) { return url + (url.indexOf('?') > -1 ? '&' : '?') + extra; } // Pause overlay — same menu as the load facade. Mode 'paused' shows a pause // glyph + resume copy; mode 'ended' shows a replay glyph + replay copy. function setOverlayMode(overlay, mode) { if (mode === 'ended') { overlay.classList.add('vibe-facade-ended'); overlay.setAttribute('aria-label', 'Replay the video'); overlay.innerHTML = '' + 'Replay