perf fix + ad guard for the games grid, search section cleanup

- drop backdrop-filter from .game tiles (likely cause of a freeze-on-load
  bug on heavy uncached loads) and harden tile sizing so it can't balloon
  regardless; fix some long-dead CSS selectors along the way (mobile
  breakpoints were silently never applying)
- strip AdSense in-feed ads if they try to inject into the games grid
- top games / starred / recently played now only show a section if
  something inside it still matches the current search or tag filter,
  instead of leaving orphaned tiles with no header or stale clutter
- remove the proxy nav card from home (dead now that it's disabled)
- delete cookie.js and savedownload.js, confirmed fully unreferenced
  dead code superseded by the save/load flow already in settings.html
This commit is contained in:
first@lynx.com
2026-10-01 00:30:48 -04:00
parent 3f1bb80f8f
commit 3781b08108
5 changed files with 89 additions and 180 deletions

View File

@ -1,4 +1,33 @@
document.addEventListener("DOMContentLoaded", loadGames);
document.addEventListener("DOMContentLoaded", guardGameGridsFromAds);
// AdSense in-feed auto ads see the uniform grid of .game tiles as a "feed" and
// inject an <ins> ad as a sibling tile, which breaks the flex layout. Strip any
// ad node that lands directly inside a game grid so ads never fill into the list.
function guardGameGridsFromAds() {
const gridIds = ["games", "topGames", "starredgames", "recentgames"];
const isAdNode = (node) => {
if (!node || node.nodeType !== 1) return false;
if (node.tagName === "INS") return true;
if (node.classList && node.classList.contains("adsbygoogle")) return true;
if (/^(aswift_|google_ads_|div-gpt-)/.test(node.id || "")) return true;
if (typeof node.querySelector === "function" &&
node.querySelector("ins.adsbygoogle, iframe[id^='google_ads_iframe'], iframe[id^='aswift_']")) return true;
return false;
};
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (isAdNode(node)) node.remove();
}
}
});
gridIds.forEach((id) => {
const el = document.getElementById(id);
if (el) observer.observe(el, { childList: true });
});
}
let elements = [];
let gameElements = [];
let topElements = [];
@ -143,9 +172,6 @@ async function loadGames() {
if (starredGames.length > 0) {
document.getElementById("starredHeader").style.display = "block";
_rebuildStarred();
document.querySelectorAll("#starredgames #star").forEach((e) => {
e.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); starEvent(e); });
});
}
const recentHeader = document.getElementById("recentHeader");
@ -219,15 +245,39 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
function elementMatchesFilters(el, input, selectedTags) {
const matchesSearch = el.dataset.name.includes(input);
const elTags = el.dataset.tags ? el.dataset.tags.split(",") : [];
const matchesTags = selectedTags.size === 0 || elTags.some((t) => selectedTags.has(t));
return matchesSearch && matchesTags;
}
// Filters a section built from cloned .game tiles (starred / recently played):
// hides individual clones that don't match, and hides the whole section
// (container + header) only if nothing inside it matches anymore.
function filterClonedSection(containerId, headerId, input, selectedTags) {
const container = document.getElementById(containerId);
const header = document.getElementById(headerId);
if (!container) return;
let anyVisible = false;
container.querySelectorAll(".game").forEach((el) => {
if (elementMatchesFilters(el, input, selectedTags)) {
el.style.display = "flex";
anyVisible = true;
} else {
el.style.display = "none";
}
});
container.style.display = anyVisible ? "" : "none";
if (header) header.style.display = anyVisible ? "block" : "none";
}
function applyFilters() {
const searchInput = document.getElementById("gamesearch");
const input = (searchInput ? searchInput.value : "").toUpperCase();
let hidden = 0;
elements.forEach((el) => {
const matchesSearch = el.dataset.name.includes(input);
const elTags = el.dataset.tags ? el.dataset.tags.split(",") : [];
const matchesTags = selectedTags.size === 0 || elTags.some((t) => selectedTags.has(t));
if (matchesSearch && matchesTags) {
if (elementMatchesFilters(el, input, selectedTags)) {
el.style.display = "flex";
} else {
el.style.display = "none";
@ -235,6 +285,17 @@ function applyFilters() {
}
});
document.getElementById("noResults").style.display = hidden >= elements.length ? "flex" : "none";
// top games shares tiles with the main list (already filtered above via
// `elements`) - its header just needs to track whether any survived.
const topHeader = document.getElementById("topHeader");
if (topHeader) {
const anyTopVisible = topElements.some((el) => el.style.display !== "none");
topHeader.style.display = anyTopVisible ? "block" : "none";
}
filterClonedSection("starredgames", "starredHeader", input, selectedTags);
filterClonedSection("recentgames", "recentHeader", input, selectedTags);
}
let searchRaf = null;
@ -248,26 +309,15 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
function _rebuildStarred() {
const container = document.getElementById("starredgames");
// cloneNode(true) does not copy event listeners, so the cloned stars need their
// click handler re-bound here rather than relying on each caller to do it.
function _rebuildList(containerId, dirs) {
const container = document.getElementById(containerId);
const gamesEl = document.getElementById("games");
const topGEl = document.getElementById("topGames");
container.innerHTML = "";
const fragment = document.createDocumentFragment();
starredGames.forEach((dir) => {
const el = gamesEl.querySelector(`a.game[data-target='${dir}']`) || (topGEl && topGEl.querySelector(`a.game[data-target='${dir}']`));
if (el) fragment.appendChild(el.cloneNode(true));
});
container.appendChild(fragment);
}
function _rebuildRecent() {
const container = document.getElementById("recentgames");
const gamesEl = document.getElementById("games");
const topGEl = document.getElementById("topGames");
container.innerHTML = "";
const fragment = document.createDocumentFragment();
recentGames.slice(0, 4).forEach((dir) => {
dirs.forEach((dir) => {
const el = gamesEl.querySelector(`a.game[data-target='${dir}']`) || (topGEl && topGEl.querySelector(`a.game[data-target='${dir}']`));
if (el) fragment.appendChild(el.cloneNode(true));
});
@ -277,6 +327,14 @@ function _rebuildRecent() {
});
}
function _rebuildStarred() {
_rebuildList("starredgames", starredGames);
}
function _rebuildRecent() {
_rebuildList("recentgames", recentGames.slice(0, 4));
}
function getTotalPlayTime(directory) {
const playTime = JSON.parse(localStorage.getItem("selenite.playTime") || "{}");
return Object.values(playTime).reduce((total, day) => total + (day[directory] || 0), 0);
@ -365,8 +423,4 @@ function starEvent(e) {
localStorage.setItem("selenite.starred", JSON.stringify(starredGames));
_rebuildStarred();
document.querySelectorAll("#starredgames #star").forEach((e) => {
e.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); starEvent(e); });
});
}