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 @@