diff --git a/public/ai.html b/public/ai.html
index ad9849c..7c20def 100644
--- a/public/ai.html
+++ b/public/ai.html
@@ -43,7 +43,8 @@
ShadowAssistant
-
+
+
diff --git a/public/books/learningcourses.json b/public/books/learningcourses.json
index 38aaf97..a63ac91 100644
--- a/public/books/learningcourses.json
+++ b/public/books/learningcourses.json
@@ -9,14 +9,6 @@
"Shooting"
]
},
- {
- "label": "Retro Bowl",
- "imageUrl": "images/main4/3_33.png",
- "url": "gn/33.html",
- "categories": [
- "Sports"
- ]
- },
{
"label": "Sky Riders",
"url": "arsenic/sky-riders/index.html",
diff --git a/public/books/script.js b/public/books/script.js
index c1c85eb..70f86bb 100644
--- a/public/books/script.js
+++ b/public/books/script.js
@@ -1,6 +1,5 @@
import { SettingsManager } from "../assets/js/settings_manager.js";
-const BATCH = 60;
const BASE_PATH = '/books/files/k12learning/';
const FAV_KEY = 'arcade_favorites';
@@ -8,11 +7,10 @@ const settings = new SettingsManager();
let allGames = [];
let visible = [];
-let rendered = 0;
+let previousVisible = new Set();
let activeFilter = 'all';
let searchQuery = '';
let favorites = new Set();
-let imgObserver, scrollObserver;
async function loadFavorites() {
const stored = await settings.get(FAV_KEY);
@@ -29,7 +27,8 @@ function isHot(game) {
function openGameTab(game) {
const gameUrl = BASE_PATH + game.url;
- const embedUrl = `embed.html?url=${encodeURIComponent(gameUrl)}`;
+ const iconUrl = game.imageUrl ? BASE_PATH + game.imageUrl : '';
+ const embedUrl = `embed.html?url=${encodeURIComponent(gameUrl)}&title=${encodeURIComponent(game.label || '')}&icon=${encodeURIComponent(iconUrl)}&src=books`;
try {
const tabsApi = window.parent?.tabs;
@@ -43,30 +42,11 @@ function openGameTab(game) {
const HEART_FILLED = ``;
const HEART_EMPTY = ``;
-function setupImgObserver() {
- imgObserver = new IntersectionObserver(entries => {
- entries.forEach(e => {
- if (!e.isIntersecting) return;
- const img = e.target;
- img.src = img.dataset.src;
- img.onload = () => { img.classList.add('loaded'); img.previousElementSibling?.classList.add('hidden'); };
- img.onerror = () => img.remove();
- imgObserver.unobserve(img);
- });
- }, { rootMargin: '300px' });
-}
-
-function setupScrollObserver() {
- scrollObserver = new IntersectionObserver(entries => {
- if (entries[0].isIntersecting && rendered < visible.length) renderBatch();
- }, { rootMargin: '400px' });
- scrollObserver.observe(document.getElementById('sentinel'));
-}
-
function buildCard(game) {
const a = document.createElement('a');
a.className = 'card';
a.href = '#';
+ a.dataset.gameLabel = game.label;
a.addEventListener('click', e => {
e.preventDefault();
openGameTab(game);
@@ -79,11 +59,20 @@ function buildCard(game) {
if (game.imageUrl) {
const img = document.createElement('img');
- img.dataset.src = BASE_PATH + game.imageUrl;
+ img.src = BASE_PATH + game.imageUrl;
img.alt = game.label;
+ img.loading = 'lazy';
img.decoding = 'async';
+ img.onload = () => {
+ if (!img.isConnected) return;
+ img.classList.add('loaded');
+ img.previousElementSibling?.classList.add('hidden');
+ };
+ img.onerror = () => {
+ if (!img.isConnected) return;
+ img.remove();
+ };
a.appendChild(img);
- imgObserver.observe(img);
}
if (isHot(game)) {
@@ -133,27 +122,71 @@ function buildCard(game) {
return a;
}
-function renderBatch() {
+function renderVisible() {
const grid = document.getElementById('grid');
- const end = Math.min(rendered + BATCH, visible.length);
const frag = document.createDocumentFragment();
- for (let i = rendered; i < end; i++) frag.appendChild(buildCard(visible[i]));
+ for (let i = 0; i < visible.length; i++) frag.appendChild(buildCard(visible[i]));
grid.appendChild(frag);
- rendered = end;
+}
+
+function updateDisplay() {
+ const grid = document.getElementById('grid');
+ const visibleLabels = new Set(visible.map(g => g.label));
+ const cardsByLabel = new Map();
+
+ grid.querySelectorAll('.card').forEach(card => {
+ cardsByLabel.set(card.dataset.gameLabel, card);
+ });
+
+ const orderedCards = [];
+ for (let i = 0; i < visible.length; i++) {
+ const card = cardsByLabel.get(visible[i].label);
+ if (card) orderedCards.push(card);
+ }
+
+ for (let i = 0; i < orderedCards.length; i++) {
+ grid.appendChild(orderedCards[i]);
+ }
+
+ grid.querySelectorAll('.card').forEach(card => {
+ const label = card.dataset.gameLabel;
+ const shouldBeVisible = visibleLabels.has(label);
+ const wasVisible = previousVisible.has(label);
+
+ // Only update if visibility changed
+ if (shouldBeVisible !== wasVisible) {
+ card.style.display = shouldBeVisible ? '' : 'none';
+ }
+ });
+
+ previousVisible = visibleLabels;
+
+ let emptyMsg = grid.querySelector('.empty');
+ if (visible.length === 0) {
+ if (!emptyMsg) {
+ emptyMsg = document.createElement('div');
+ emptyMsg.className = 'empty';
+ emptyMsg.innerHTML = 'NO RESULTSTry a different search or filter';
+ grid.appendChild(emptyMsg);
+ }
+ emptyMsg.style.display = '';
+ } else if (emptyMsg) {
+ emptyMsg.style.display = 'none';
+ }
}
function applyFilters() {
const q = searchQuery.toLowerCase();
- let filtered = allGames.filter(g => {
- if (activeFilter === 'favorites') return favorites.has(g.label);
+ const filtered = allGames.filter(g => {
+ const searchOk = !q || g.label.toLowerCase().includes(q);
+ if (activeFilter === 'favorites') return favorites.has(g.label) && searchOk;
const catOk = activeFilter === 'all' ||
(g.categories || []).some(c => c.toLowerCase() === activeFilter);
- const searchOk = !q || g.label.toLowerCase().includes(q);
return catOk && searchOk;
});
- if (activeFilter !== 'favorites' && !q) {
+ if (activeFilter !== 'favorites') {
const hot = filtered.filter(g => isHot(g));
const rest = filtered.filter(g => !isHot(g));
visible = [...hot, ...rest];
@@ -161,19 +194,10 @@ function applyFilters() {
visible = filtered;
}
- rendered = 0;
- const grid = document.getElementById('grid');
- grid.innerHTML = '';
-
document.getElementById('countDisplay').textContent =
`${visible.length} game${visible.length !== 1 ? 's' : ''}`;
- if (visible.length === 0) {
- grid.innerHTML = 'NO RESULTSTry a different search or filter
';
- return;
- }
-
- renderBatch();
+ updateDisplay();
}
function buildFilters(games) {
@@ -208,8 +232,6 @@ function debounce(fn, ms) {
}
async function init() {
- setupImgObserver();
- setupScrollObserver();
await loadFavorites();
try {
const data = await fetch('learningcourses.json').then(r => r.json());
@@ -217,6 +239,9 @@ async function init() {
document.getElementById('loading').style.display = 'none';
document.getElementById('grid').style.display = 'grid';
buildFilters(allGames);
+ visible = allGames;
+ previousVisible = new Set(allGames.map(g => g.label));
+ renderVisible();
applyFilters();
document.getElementById('searchInput').addEventListener('input',
debounce(e => { searchQuery = e.target.value; applyFilters(); }, 150));
diff --git a/public/css/ai.css b/public/css/ai.css
index d557e98..8cea65d 100644
--- a/public/css/ai.css
+++ b/public/css/ai.css
@@ -175,6 +175,15 @@ body {
padding-right: 36px;
}
+#model-selector option {
+ background: var(--primary);
+ color: var(--accent-light);
+ outline:none;
+ border:none;
+ padding: 10px 12px;
+}
+
+
#model-selector:focus {
outline: 2px solid var(--accent);
}
diff --git a/public/pages/embed/index.html b/public/pages/embed/index.html
index 2376f3e..9f6bccc 100644
--- a/public/pages/embed/index.html
+++ b/public/pages/embed/index.html
@@ -16,67 +16,163 @@