Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion public/ai.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ <h1>ShadowAssistant</h1>
<option value="shuttleai/auto" selected>
Shuttle Auto (Default)
</option>
<option value="openai/gpt-5.2">GPT-5.2</option>
<option value="openai/gpt-5.4">GPT-5.4</option>
<option value="anthropic/claude-sonnet-4-6">Claude Sonnet 4.6</option>
<option value="google/gemini-3-flash-preview">
Gemini 3 Flash Preview
</option>
Expand Down
8 changes: 0 additions & 8 deletions public/books/learningcourses.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
117 changes: 71 additions & 46 deletions public/books/script.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { SettingsManager } from "../assets/js/settings_manager.js";

const BATCH = 60;
const BASE_PATH = '/books/files/k12learning/';
const FAV_KEY = 'arcade_favorites';

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);
Expand All @@ -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;
Expand All @@ -43,30 +42,11 @@ function openGameTab(game) {
const HEART_FILLED = `<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px" fill="currentColor"><path d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Z"/></svg>`;
const HEART_EMPTY = `<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 -960 960 960" width="16px" fill="currentColor"><path d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Zm0-108q96-86 158-147.5t98-107q36-45.5 50-81t14-70.5q0-60-40-100t-100-40q-47 0-87 26.5T518-680h-76q-15-41-55-67.5T300-774q-60 0-100 40t-40 100q0 35 14 70.5t50 81q36 45.5 98 107T480-228Zm0-273Z"/></svg>`;

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);
Expand All @@ -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)) {
Expand Down Expand Up @@ -133,47 +122,82 @@ 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 = '<strong>NO RESULTS</strong>Try 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];
} else {
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 = '<div class="empty"><strong>NO RESULTS</strong>Try a different search or filter</div>';
return;
}

renderBatch();
updateDisplay();
}

function buildFilters(games) {
Expand Down Expand Up @@ -208,15 +232,16 @@ function debounce(fn, ms) {
}

async function init() {
setupImgObserver();
setupScrollObserver();
await loadFavorites();
try {
const data = await fetch('learningcourses.json').then(r => r.json());
allGames = data.games.filter(g => g.label !== 'Request Games');
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));
Expand Down
9 changes: 9 additions & 0 deletions public/css/ai.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading
Loading