@ywkrlerche/richardwho 1.0.22 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +270 -82
- package/games.json +1 -1
- package/index.html +100 -48
- package/logos/1-on-1-soccer.webp +0 -0
- package/logos/1-on-1-tennis.webp +0 -0
- package/logos/10-minutes-till-dawn.webp +0 -0
- package/logos/1v1.lol.webp +0 -0
- package/logos/2048-cupcakes.webp +0 -0
- package/logos/2048.webp +0 -0
- package/logos/3D-flight-simulator.webp +0 -0
- package/logos/3d-free-kick.webp +0 -0
- package/logos/8-ball-classic.webp +0 -0
- package/logos/9007199254740992.webp +0 -0
- package/logos/Alpha_1.2.6.webp +0 -0
- package/logos/Indev.webp +0 -0
- package/logos/agar-io-lite.webp +0 -0
- package/logos/age-of-war-2.webp +0 -0
- package/logos/age-of-war.webp +0 -0
- package/logos/ages-of-conflict.webp +0 -0
- package/logos/among-us.webp +0 -0
- package/logos/angry-birds-space.webp +0 -0
- package/logos/angry-birds.webp +0 -0
- package/logos/awesome-tanks-2.webp +0 -0
- package/logos/awesome-tanks.webp +0 -0
- package/logos/bacon-may-die.webp +0 -0
- package/logos/bad-ice-cream-2.webp +0 -0
- package/logos/bad-ice-cream-3.webp +0 -0
- package/logos/bad-ice-cream.webp +0 -0
- package/logos/bad-parenting.webp +0 -0
- package/logos/bad-piggies.webp +0 -0
- package/logos/bank-robbery-2.webp +0 -0
- package/logos/bank-robbery-3.webp +0 -0
- package/logos/baseball-bros.webp +0 -0
- package/logos/basket-bros.webp +0 -0
- package/logos/basket-random.webp +0 -0
- package/logos/basketball-legends.webp +0 -0
- package/logos/basketball-stars.webp +0 -0
- package/logos/big-ice-tower-tiny-square.webp +0 -0
- package/logos/big-neon-tower-tiny-square.webp +0 -0
- package/logos/big-tower-tiny-square-2.webp +0 -0
- package/logos/big-tower-tiny-square.webp +0 -0
- package/logos/blocky-snakes.webp +0 -0
- package/logos/bloons-TD-2.webp +0 -0
- package/logos/bloons-TD.webp +0 -0
- package/logos/bloxorz.webp +0 -0
- package/logos/blumgi-racers.webp +0 -0
- package/logos/blumgi-rocket.webp +0 -0
- package/logos/bob-the-robber-2.webp +0 -0
- package/logos/bob-the-robber.webp +0 -0
- package/logos/bouncy-motors.webp +0 -0
- package/logos/boxing-random.webp +0 -0
- package/logos/breaking-the-bank.webp +0 -0
- package/logos/bubble-shooter.webp +0 -0
- package/logos/candy-crush.webp +0 -0
- package/logos/capybara-clicker.webp +0 -0
- package/logos/car-drawing.webp +0 -0
- package/logos/chaos-faction-2.webp +0 -0
- package/logos/chess.webp +0 -0
- package/logos/choppy-orc.webp +0 -0
- package/logos/circloO-2.webp +0 -0
- package/logos/circloO.webp +0 -0
- package/logos/clash-of-vikings.webp +0 -0
- package/logos/cleanup-io.webp +0 -0
- package/logos/cluster-rush.webp +0 -0
- package/logos/cookie-clicker.webp +0 -0
- package/logos/coreball.webp +0 -0
- package/logos/crazy-cars.webp +0 -0
- package/logos/crazy-cattle-3D.webp +0 -0
- package/logos/crazy-crash-landing.webp +0 -0
- package/logos/crazy-motorcycle.webp +0 -0
- package/logos/crossy-road.webp +0 -0
- package/logos/cut-the-rope-2.webp +0 -0
- package/logos/cut-the-rope-holiday-gift.webp +0 -0
- package/logos/cut-the-rope-time-travel.webp +0 -0
- package/logos/cut-the-rope.webp +0 -0
- package/logos/dadish-2.webp +0 -0
- package/logos/dadish-3.webp +0 -0
- package/logos/dadish.webp +0 -0
- package/logos/death-chase.webp +0 -0
- package/logos/death-run-3D.webp +0 -0
- package/logos/demolition-derby-crash-racing.webp +0 -0
- package/logos/doodle-jump.webp +0 -0
- package/logos/draw-climber.webp +0 -0
- package/logos/dreadhead-parkour.webp +0 -0
- package/logos/drift-boss.webp +0 -0
- package/logos/drift-hunters-pro.webp +0 -0
- package/logos/drive-mad.webp +0 -0
- package/logos/duck-life.webp +0 -0
- package/logos/ducklings-io.webp +0 -0
- package/logos/eagle-ride.webp +0 -0
- package/logos/earn-to-die-2.webp +0 -0
- package/logos/earn-to-die.webp +0 -0
- package/logos/eggy-car.webp +0 -0
- package/logos/elastic-face.webp +0 -0
- package/logos/escape-road-2.webp +0 -0
- package/logos/escape-road-3.webp +0 -0
- package/logos/escape-road.webp +0 -0
- package/logos/escaping-the-prison.webp +0 -0
- package/logos/eugenes-life.webp +0 -0
- package/logos/evil-glitch.webp +0 -0
- package/logos/fancy-pants-adventure-2.webp +0 -0
- package/logos/fancy-pants-adventure.webp +0 -0
- package/logos/fireblob.webp +0 -0
- package/logos/fireboy-and-watergirl.webp +0 -0
- package/logos/flappy-bird.webp +0 -0
- package/logos/fleeing-the-complex.webp +0 -0
- package/logos/flood-runner-2.webp +0 -0
- package/logos/flood-runner-3.webp +0 -0
- package/logos/flood-runner-4.webp +0 -0
- package/logos/fnaf-2.webp +0 -0
- package/logos/fnaf-3.webp +0 -0
- package/logos/fnaf.webp +0 -0
- package/logos/football-bros.webp +0 -0
- package/logos/football-legends.webp +0 -0
- package/logos/friday-night-funkin.webp +0 -0
- package/logos/fruit-ninja.webp +0 -0
- package/logos/funny-battle.webp +0 -0
- package/logos/funny-mad-racing.webp +0 -0
- package/logos/funny-shooter-2.webp +0 -0
- package/logos/geometry-dash-lite.webp +0 -0
- package/logos/geometry-dash.webp +0 -0
- package/logos/geometry-vibes.webp +0 -0
- package/logos/get-on-top.webp +0 -0
- package/logos/getaway-shootout.webp +0 -0
- package/logos/gladi-hoppers.webp +0 -0
- package/logos/google-baseball.webp +0 -0
- package/logos/google-dino.webp +0 -0
- package/logos/granny.webp +0 -0
- package/logos/guess-their-answer.webp +0 -0
- package/logos/happy-wheels.webp +0 -0
- package/logos/helix-jump.webp +0 -0
- package/logos/highway-traffic.webp +0 -0
- package/logos/hill-climb-racing-lite.webp +0 -0
- package/logos/hole-io.webp +0 -0
- package/logos/house-of-hazards.webp +0 -0
- package/logos/hover-racer-drive.webp +0 -0
- package/logos/ice-dodo.webp +0 -0
- package/logos/idle-breakout.webp +0 -0
- package/logos/idle-dice.webp +0 -0
- package/logos/infiltrating-the-airship.webp +0 -0
- package/logos/iron-snout.webp +0 -0
- package/logos/jacksmith.webp +0 -0
- package/logos/jetpack-joyride.webp +0 -0
- package/logos/johnny-trigger.webp +0 -0
- package/logos/jumping-shell.webp +0 -0
- package/logos/karate-bros.webp +0 -0
- package/logos/kart-bros.webp +0 -0
- package/logos/kittytoy.webp +0 -0
- package/logos/learn-to-fly-2.webp +0 -0
- package/logos/learn-to-fly-3.webp +0 -0
- package/logos/learn-to-fly.webp +0 -0
- package/logos/level-devil.webp +0 -0
- package/logos/little-alchemy-2.webp +0 -0
- package/logos/madalin-stunt-cars-2.webp +0 -0
- package/logos/madness-project-nexus.webp +0 -0
- package/logos/melon-playground.webp +0 -0
- package/logos/merge-round-racers.webp +0 -0
- package/logos/minesweeper.webp +0 -0
- package/logos/monkey-mart.webp +0 -0
- package/logos/monster-tracks.webp +0 -0
- package/logos/motherload.webp +0 -0
- package/logos/moto-x3m-2.webp +0 -0
- package/logos/moto-x3m-3.webp +0 -0
- package/logos/moto-x3m-pool-party.webp +0 -0
- package/logos/moto-x3m-spooky-land.webp +0 -0
- package/logos/moto-x3m-winter.webp +0 -0
- package/logos/moto-x3m.webp +0 -0
- package/logos/murder.webp +0 -0
- package/logos/noob-miner.webp +0 -0
- package/logos/openTTD.webp +0 -0
- package/logos/opposite-day.webp +0 -0
- package/logos/ovo.webp +0 -0
- package/logos/pac-man.webp +0 -0
- package/logos/papas-bakeria.webp +0 -0
- package/logos/papas-burgeria.webp +0 -0
- package/logos/papas-cheeseria.webp +0 -0
- package/logos/papas-cupcakeria.webp +0 -0
- package/logos/papas-donuteria.webp +0 -0
- package/logos/papas-freezeria.webp +0 -0
- package/logos/papas-pancakeria.webp +0 -0
- package/logos/papas-pastaria.webp +0 -0
- package/logos/papas-pizzeria.webp +0 -0
- package/logos/papas-scooperia.webp +0 -0
- package/logos/papas-sushiria.webp +0 -0
- package/logos/papas-taco-mia.webp +0 -0
- package/logos/papas-wingeria.webp +0 -0
- package/logos/paper-io-2.webp +0 -0
- package/logos/parking-fury-2.webp +0 -0
- package/logos/picos-school.webp +0 -0
- package/logos/ping-pong-chaos.webp +0 -0
- package/logos/pixel-speedrun.webp +0 -0
- package/logos/plants-vs-zombies.webp +0 -0
- package/logos/plonky.webp +0 -0
- package/logos/polytrack.webp +0 -0
- package/logos/poor-bunny.webp +0 -0
- package/logos/pou.webp +0 -0
- package/logos/ragdoll-archers.webp +0 -0
- package/logos/ragdoll-hit.webp +0 -0
- package/logos/ragdoll-soccer.webp +0 -0
- package/logos/red-ball-4-vol-1.webp +0 -0
- package/logos/red-ball-4-vol-2.webp +0 -0
- package/logos/red-ball-4-vol-3.webp +0 -0
- package/logos/retro-bowl-college.webp +0 -0
- package/logos/retro-bowl.webp +0 -0
- package/logos/retro-highway.webp +0 -0
- package/logos/retro-ping-pong.webp +0 -0
- package/logos/riddle-school-2.webp +0 -0
- package/logos/riddle-school-3.webp +0 -0
- package/logos/riddle-school-4.webp +0 -0
- package/logos/riddle-school-5.webp +0 -0
- package/logos/riddle-school.webp +0 -0
- package/logos/riddle-transfer.webp +0 -0
- package/logos/rocket-soccer-derby.webp +0 -0
- package/logos/rooftop-run.webp +0 -0
- package/logos/rooftop-snipers-2.webp +0 -0
- package/logos/rooftop-snipers.webp +0 -0
- package/logos/run-2.webp +0 -0
- package/logos/run-3.webp +0 -0
- package/logos/run.webp +0 -0
- package/logos/sandbox-city.webp +0 -0
- package/logos/short-life.webp +0 -0
- package/logos/slope-2-player.webp +0 -0
- package/logos/slope-3.webp +0 -0
- package/logos/slope.webp +0 -0
- package/logos/snail-bob-2.webp +0 -0
- package/logos/snail-bob-3.webp +0 -0
- package/logos/snail-bob-4.webp +0 -0
- package/logos/snail-bob-5.webp +0 -0
- package/logos/snail-bob-6.webp +0 -0
- package/logos/snail-bob-7.webp +0 -0
- package/logos/snail-bob-8.webp +0 -0
- package/logos/snail-bob.webp +0 -0
- package/logos/snow-rider.webp +0 -0
- package/logos/snow-road.webp +0 -0
- package/logos/snowball-io.webp +0 -0
- package/logos/soccer-bros.webp +0 -0
- package/logos/soccer-random.webp +0 -0
- package/logos/space-is-key-2.webp +0 -0
- package/logos/space-is-key.webp +0 -0
- package/logos/space-waves.webp +0 -0
- package/logos/spacebar-clicker.webp +0 -0
- package/logos/speed-stars.webp +0 -0
- package/logos/sprunki.webp +0 -0
- package/logos/stack.webp +0 -0
- package/logos/stacktris.webp +0 -0
- package/logos/state-io.webp +0 -0
- package/logos/stealing-the-diamond.webp +0 -0
- package/logos/stick-archers-battle.webp +0 -0
- package/logos/stick-fighter.webp +0 -0
- package/logos/stick-merge.webp +0 -0
- package/logos/stickman-hook.webp +0 -0
- package/logos/subway-surfers-beijing.webp +0 -0
- package/logos/subway-surfers-havana.webp +0 -0
- package/logos/subway-surfers-san-francisco.webp +0 -0
- package/logos/super-bike-the-champion.webp +0 -0
- package/logos/super-liquid-soccer.webp +0 -0
- package/logos/super-star-car.webp +0 -0
- package/logos/table-tennis-world-tour.webp +0 -0
- package/logos/tag.webp +0 -0
- package/logos/tanuki-sunset.webp +0 -0
- package/logos/tap-road.webp +0 -0
- package/logos/temple-of-boom.webp +0 -0
- package/logos/temple-run-2.webp +0 -0
- package/logos/territorial-io.webp +0 -0
- package/logos/tetrominoes.webp +0 -0
- package/logos/the-impossible-quiz-2.webp +0 -0
- package/logos/the-impossible-quiz.webp +0 -0
- package/logos/they-are-coming.webp +0 -0
- package/logos/this-is-the-only-level-too.webp +0 -0
- package/logos/this-is-the-only-level.webp +0 -0
- package/logos/time-shooter-2.webp +0 -0
- package/logos/time-shooter-3.webp +0 -0
- package/logos/tiny-fishing.webp +0 -0
- package/logos/trap-the-cat.webp +0 -0
- package/logos/trivia-crack.webp +0 -0
- package/logos/tube-jumpers.webp +0 -0
- package/logos/tunnel-rush.webp +0 -0
- package/logos/unicycle-hero.webp +0 -0
- package/logos/vex-4.webp +0 -0
- package/logos/vex-5.webp +0 -0
- package/logos/vex-x3m-2.webp +0 -0
- package/logos/vex-x3m.webp +0 -0
- package/logos/volley-random.webp +0 -0
- package/logos/war-the-knights.webp +0 -0
- package/logos/we-become-what-we-behold.webp +0 -0
- package/logos/wheelie-bike.webp +0 -0
- package/logos/wheely-2.webp +0 -0
- package/logos/wheely-3.webp +0 -0
- package/logos/wheely-4.webp +0 -0
- package/logos/wheely-5.webp +0 -0
- package/logos/wheely-6.webp +0 -0
- package/logos/wheely-7.webp +0 -0
- package/logos/wheely.webp +0 -0
- package/logos/wordle-unlimited.webp +0 -0
- package/logos/worlds-hardest-game-2.webp +0 -0
- package/logos/worlds-hardest-game.webp +0 -0
- package/logos/wrestle-bros.webp +0 -0
- package/logos/yohoho-io.webp +0 -0
- package/logos/zombie-rush.webp +0 -0
- package/package.json +4 -3
- package/style.css +194 -160
package/app.js
CHANGED
|
@@ -2,23 +2,33 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const CFG = window.RUG_CONFIG || {};
|
|
5
|
+
const SITE_NAME = "Richards Unblocked Games";
|
|
5
6
|
const RECENT_KEY = "rug-recent";
|
|
6
7
|
const CLOCK_KEY = "rug-clock24";
|
|
7
8
|
const THEME_KEY = "rug-theme";
|
|
9
|
+
const PER_ROW_KEY = "rug-per-row";
|
|
8
10
|
const MAX_RECENT = 6;
|
|
11
|
+
const MARQUEE_SPEED = 38; // pixels per second
|
|
9
12
|
|
|
10
13
|
const TAG_EMOJI = {
|
|
11
14
|
Minecraft: "⛏️", Racing: "🏎️", Sports: "⚽", Shooter: "🎯", Horror: "👻", Clicker: "👆",
|
|
12
15
|
Cooking: "🍔", Puzzle: "🧩", Strategy: "🏰", IO: "🌐", Platformer: "🏃", Arcade: "👾",
|
|
16
|
+
Rhythm: "🎵", Adventure: "🗺️", Tools: "🧰",
|
|
13
17
|
};
|
|
18
|
+
// Shown in the "Popular games" strip; any id missing from the catalog is skipped
|
|
19
|
+
const POPULAR = ["1v1.lol", "slope", "retro-bowl", "subway-surfers-san-francisco", "fnaf", "moto-x3m",
|
|
20
|
+
"papas-freezeria", "basket-random", "drift-boss", "monkey-mart", "temple-run-2", "happy-wheels",
|
|
21
|
+
"jetpack-joyride", "cut-the-rope", "friday-night-funkin", "plants-vs-zombies", "crossy-road", "run-3"];
|
|
14
22
|
|
|
15
23
|
const $ = (id) => document.getElementById(id);
|
|
16
24
|
const els = {
|
|
17
|
-
|
|
18
|
-
search: $("search"), chips: $("chips"), count: $("count"), empty: $("empty"),
|
|
25
|
+
homeView: $("view-home"), gamesView: $("view-games"), homeInput: $("home-input"), homeSub: $("home-sub"),
|
|
26
|
+
grid: $("grid"), search: $("search"), sort: $("sort"), chips: $("chips"), count: $("count"), empty: $("empty"),
|
|
19
27
|
allTitle: $("all-title"), footerCount: $("footer-count"),
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
recentBlock: $("recent-block"), recentRow: $("recent-row"),
|
|
29
|
+
popularBlock: $("popular-block"), marquee: $("marquee"), marqueeTrack: $("marquee-track"),
|
|
30
|
+
player: $("player"), frame: $("game-frame"), playerTitle: $("player-title"),
|
|
31
|
+
loader: $("loader"), loaderName: $("loader-name"), loaderDesc: $("loader-desc"), loaderArt: $("loader-art"),
|
|
22
32
|
barFill: $("bar-fill"), loaderStatus: $("loader-status"), retryBtn: $("retry-btn"),
|
|
23
33
|
};
|
|
24
34
|
|
|
@@ -39,21 +49,28 @@
|
|
|
39
49
|
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
40
50
|
return h >>> 0;
|
|
41
51
|
}
|
|
42
|
-
function
|
|
52
|
+
function gradient(game) {
|
|
43
53
|
const h = hash(game.id);
|
|
44
54
|
const a = h % 360, b = (a + 40 + (h >> 9) % 80) % 360;
|
|
45
|
-
return `
|
|
55
|
+
return `linear-gradient(135deg, hsl(${a} 65% 42%), hsl(${b} 70% 24%))`;
|
|
46
56
|
}
|
|
47
57
|
function initials(name) {
|
|
48
58
|
const words = name.replace(/\(.*?\)/g, "").split(/\s+/).filter(Boolean);
|
|
49
59
|
if (words.length === 1) return words[0].slice(0, 3).toUpperCase();
|
|
50
60
|
return words.slice(0, 3).map((w) => w[0]).join("").toUpperCase();
|
|
51
61
|
}
|
|
62
|
+
// Our logos ship inside this package under logos/<id>.webp (relative, so it works on unpkg and locally);
|
|
63
|
+
// games from the extra collection carry their own pinned cover URL
|
|
64
|
+
function logoUrl(game) {
|
|
65
|
+
if (game.cover) return game.cover;
|
|
66
|
+
return game.logo ? `logos/${encodeURIComponent(game.id)}.webp` : null;
|
|
67
|
+
}
|
|
52
68
|
function fmtMB(bytes) {
|
|
53
69
|
const mb = bytes / 1048576;
|
|
54
70
|
return mb < 1 ? `${Math.max(1, Math.round(bytes / 1024))} KB` : `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
|
|
55
71
|
}
|
|
56
72
|
function gameUrls(game) {
|
|
73
|
+
if (game.url) return [game.url]; // extra collection: one launch file at a pinned URL
|
|
57
74
|
const base = CFG.localBase
|
|
58
75
|
? `${CFG.localBase}/${game.repo}`
|
|
59
76
|
: `https://cdn.jsdelivr.net/gh/${CFG.githubUser}/${game.repo}@${CFG.branch || "main"}`;
|
|
@@ -62,54 +79,106 @@
|
|
|
62
79
|
return Array.from({ length: game.parts }, (_, i) => `${url}.part${i}`);
|
|
63
80
|
}
|
|
64
81
|
function escapeHtml(s) {
|
|
65
|
-
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
82
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
// ---------- Rendering ----------
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</
|
|
86
|
+
function tileHtml(game, { popular = false, eager = false } = {}) {
|
|
87
|
+
const url = logoUrl(game);
|
|
88
|
+
return `<button class="tile" type="button" data-id="${escapeHtml(game.id)}" title="${escapeHtml(game.desc || game.name)}">
|
|
89
|
+
<span class="tile-art" style="background:${gradient(game)}">${escapeHtml(initials(game.name))}</span>
|
|
90
|
+
${url ? `<span class="tile-art"><img src="${url}" alt="" loading="${eager ? "eager" : "lazy"}" decoding="async" onerror="this.parentNode.remove()"></span>` : ""}
|
|
91
|
+
${popular ? `<span class="badge">Popular</span>` : ""}
|
|
92
|
+
<span class="tile-label">${escapeHtml(game.name)}</span>
|
|
76
93
|
</button>`;
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
function
|
|
80
|
-
const counts = {
|
|
96
|
+
function tagCounts() {
|
|
97
|
+
const counts = {};
|
|
81
98
|
games.forEach((g) => { counts[g.tag] = (counts[g.tag] || 0) + 1; });
|
|
82
|
-
|
|
99
|
+
return counts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderChips() {
|
|
103
|
+
const counts = tagCounts();
|
|
104
|
+
const tags = ["All", ...Object.keys(counts).sort((a, b) => counts[b] - counts[a])];
|
|
83
105
|
els.chips.innerHTML = tags.map((t) =>
|
|
84
|
-
`<button class="
|
|
106
|
+
`<button class="tab" role="tab" type="button" data-tag="${escapeHtml(t)}" aria-selected="${t === activeTag}">${t === "All" ? "All games" : (TAG_EMOJI[t] || "") + " " + escapeHtml(t)}<small>${t === "All" ? games.length : counts[t]}</small></button>`
|
|
85
107
|
).join("");
|
|
86
108
|
}
|
|
87
109
|
|
|
110
|
+
function sorted(list) {
|
|
111
|
+
const by = els.sort.value;
|
|
112
|
+
const out = list.slice();
|
|
113
|
+
if (by === "za") out.reverse();
|
|
114
|
+
else if (by === "small") out.sort((a, b) => a.size - b.size);
|
|
115
|
+
else if (by === "big") out.sort((a, b) => b.size - a.size);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
function renderGrid() {
|
|
89
|
-
const
|
|
90
|
-
const terms =
|
|
91
|
-
const list = games.filter((g) => {
|
|
120
|
+
const raw = els.search.value.trim();
|
|
121
|
+
const terms = raw.toLowerCase().split(/\s+/).filter(Boolean);
|
|
122
|
+
const list = sorted(games.filter((g) => {
|
|
92
123
|
if (activeTag !== "All" && g.tag !== activeTag) return false;
|
|
93
|
-
const hay =
|
|
124
|
+
const hay = `${g.name} ${g.id} ${g.tag} ${g.desc || ""}`.toLowerCase();
|
|
94
125
|
return terms.every((t) => hay.includes(t));
|
|
95
|
-
});
|
|
96
|
-
els.grid.innerHTML = list.map(
|
|
126
|
+
}));
|
|
127
|
+
els.grid.innerHTML = list.map((g) => tileHtml(g)).join("");
|
|
97
128
|
els.empty.hidden = list.length > 0;
|
|
98
129
|
els.count.textContent = `${list.length} game${list.length === 1 ? "" : "s"}`;
|
|
99
|
-
els.allTitle.textContent =
|
|
130
|
+
els.allTitle.textContent = raw ? `Results for “${raw}”` : activeTag === "All" ? "All games" : `${activeTag} games`;
|
|
131
|
+
// Strips are for browsing; hide them while searching or filtering
|
|
132
|
+
const browsing = !raw && activeTag === "All";
|
|
133
|
+
els.popularBlock.hidden = !browsing;
|
|
134
|
+
els.recentBlock.hidden = !browsing || !els.recentRow.children.length;
|
|
100
135
|
}
|
|
101
136
|
|
|
102
137
|
function renderRecent() {
|
|
103
138
|
const recent = store.get(RECENT_KEY, []).map((id) => byId.get(id)).filter(Boolean);
|
|
104
|
-
els.
|
|
105
|
-
els.
|
|
139
|
+
els.recentRow.innerHTML = recent.map((g) => tileHtml(g, { eager: true })).join("");
|
|
140
|
+
els.recentBlock.hidden = recent.length === 0 || !!els.search.value.trim() || activeTag !== "All";
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
function addRecent(id) {
|
|
109
144
|
const list = store.get(RECENT_KEY, []).filter((x) => x !== id);
|
|
110
145
|
list.unshift(id);
|
|
111
146
|
store.set(RECENT_KEY, list.slice(0, MAX_RECENT));
|
|
112
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setTag(tag) {
|
|
150
|
+
activeTag = tag;
|
|
151
|
+
els.chips.querySelectorAll(".tab").forEach((c) => c.setAttribute("aria-selected", c.dataset.tag === tag));
|
|
152
|
+
renderGrid();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function setPerRow(n) {
|
|
156
|
+
document.documentElement.style.setProperty("--per-row", n);
|
|
157
|
+
document.querySelectorAll(".per-row button").forEach((b) => b.classList.toggle("on", Number(b.dataset.per) === n));
|
|
158
|
+
store.set(PER_ROW_KEY, n);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------- Popular games: a smooth, endless sideways glide ----------
|
|
162
|
+
function startMarquee() {
|
|
163
|
+
const list = POPULAR.map((id) => byId.get(id)).filter(Boolean);
|
|
164
|
+
if (!list.length) { els.popularBlock.hidden = true; return; }
|
|
165
|
+
const set = list.map((g) => tileHtml(g, { popular: true, eager: true })).join("");
|
|
166
|
+
els.marqueeTrack.innerHTML = set + set; // two copies so the loop never shows a gap
|
|
167
|
+
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
168
|
+
let offset = 0, last = 0;
|
|
169
|
+
function step(t) {
|
|
170
|
+
const dt = last ? Math.min(64, t - last) : 0;
|
|
171
|
+
last = t;
|
|
172
|
+
// Pause while pointed at or keyboard-focused (checked live, so it can never get stuck paused)
|
|
173
|
+
const paused = els.marquee.matches(":hover") || els.marquee.contains(document.activeElement);
|
|
174
|
+
const half = els.marqueeTrack.scrollWidth / 2; // width of one copy (gap included); 0 while hidden
|
|
175
|
+
if (!paused && !reduce && half > 0 && els.player.hidden) {
|
|
176
|
+
offset = (offset + (MARQUEE_SPEED * dt) / 1000) % half;
|
|
177
|
+
els.marqueeTrack.style.transform = `translate3d(${-offset}px,0,0)`;
|
|
178
|
+
}
|
|
179
|
+
requestAnimationFrame(step);
|
|
180
|
+
}
|
|
181
|
+
requestAnimationFrame(step);
|
|
113
182
|
}
|
|
114
183
|
|
|
115
184
|
// ---------- Game loading ----------
|
|
@@ -124,21 +193,29 @@
|
|
|
124
193
|
|
|
125
194
|
els.playerTitle.textContent = game.name;
|
|
126
195
|
els.loaderName.textContent = game.name;
|
|
127
|
-
els.
|
|
128
|
-
|
|
196
|
+
els.loaderDesc.textContent = game.desc || "";
|
|
197
|
+
const url = logoUrl(game);
|
|
198
|
+
els.loaderArt.style.backgroundImage = url ? `url("${url}"), ${gradient(game)}` : gradient(game);
|
|
199
|
+
els.loaderArt.textContent = url ? "" : initials(game.name);
|
|
129
200
|
els.loader.hidden = false;
|
|
130
201
|
els.retryBtn.hidden = true;
|
|
131
202
|
els.barFill.style.width = "0%";
|
|
132
203
|
els.loaderStatus.textContent = "Connecting…";
|
|
133
|
-
document.title = `${game.name} ·
|
|
204
|
+
document.title = `${game.name} · ${SITE_NAME}`;
|
|
134
205
|
|
|
135
206
|
const urls = gameUrls(game);
|
|
136
207
|
const loaded = new Array(urls.length).fill(0);
|
|
137
208
|
const total = game.size || 1;
|
|
138
209
|
const update = () => {
|
|
139
210
|
const got = loaded.reduce((a, b) => a + b, 0);
|
|
140
|
-
|
|
141
|
-
|
|
211
|
+
if (game.size) {
|
|
212
|
+
els.barFill.style.width = `${Math.min(100, (got / total) * 100).toFixed(1)}%`;
|
|
213
|
+
els.loaderStatus.textContent = `${fmtMB(got)} / ${fmtMB(total)}${urls.length > 1 ? ` · ${urls.length} parts` : ""}`;
|
|
214
|
+
} else {
|
|
215
|
+
// Size unknown (extra collection): show what has arrived so far
|
|
216
|
+
els.barFill.style.width = "35%";
|
|
217
|
+
els.loaderStatus.textContent = `${fmtMB(got)} loaded`;
|
|
218
|
+
}
|
|
142
219
|
};
|
|
143
220
|
|
|
144
221
|
try {
|
|
@@ -159,8 +236,10 @@
|
|
|
159
236
|
}));
|
|
160
237
|
if (currentLoad?.id !== game.id) return;
|
|
161
238
|
els.loaderStatus.textContent = "Starting…";
|
|
162
|
-
|
|
239
|
+
let html = await new Blob(pieces.flat()).text();
|
|
163
240
|
if (currentLoad?.id !== game.id) return;
|
|
241
|
+
if (game.src === "gn") html = stripTracking(html);
|
|
242
|
+
html = guardHistory(html);
|
|
164
243
|
const doc = freshFrame().contentDocument;
|
|
165
244
|
doc.open();
|
|
166
245
|
doc.write(html);
|
|
@@ -176,6 +255,24 @@
|
|
|
176
255
|
}
|
|
177
256
|
}
|
|
178
257
|
|
|
258
|
+
// The extra collection's launch files embed Google Analytics; remove it so players aren't tracked
|
|
259
|
+
function stripTracking(html) {
|
|
260
|
+
return html
|
|
261
|
+
.replace(/<script\b[^>]*googletagmanager\.com[^>]*>\s*<\/script>/gi, "")
|
|
262
|
+
.replace(/<script\b[^>]*>(?:(?!<\/script>)[\s\S])*?\bgtag\s*\((?:(?!<\/script>)[\s\S])*?<\/script>/gi, "");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// A written-in game shares this page's address and session history, so a game calling history.back()
|
|
266
|
+
// or pushState() would move the whole tab (and kick the player out). Give games no-op versions first.
|
|
267
|
+
const HISTORY_GUARD = "<script>(function(){try{var h=window.history,n=function(){};" +
|
|
268
|
+
"h.back=h.forward=h.go=h.pushState=h.replaceState=n;}catch(e){}})();<\/script>";
|
|
269
|
+
function guardHistory(html) {
|
|
270
|
+
if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, (m) => m + HISTORY_GUARD);
|
|
271
|
+
if (/<html[^>]*>/i.test(html)) return html.replace(/<html[^>]*>/i, (m) => m + HISTORY_GUARD);
|
|
272
|
+
if (/^\s*<!doctype[^>]*>/i.test(html)) return html.replace(/^\s*<!doctype[^>]*>/i, (m) => m + HISTORY_GUARD);
|
|
273
|
+
return HISTORY_GUARD + html;
|
|
274
|
+
}
|
|
275
|
+
|
|
179
276
|
function stopGame() {
|
|
180
277
|
if (!currentLoad) return;
|
|
181
278
|
currentLoad.controller.abort();
|
|
@@ -191,15 +288,12 @@
|
|
|
191
288
|
return fresh;
|
|
192
289
|
}
|
|
193
290
|
|
|
194
|
-
// The
|
|
291
|
+
// The game frame is same-origin, so we can listen for the panic keys inside the game too
|
|
195
292
|
function hookFrameKeys() {
|
|
196
|
-
try {
|
|
197
|
-
const win = els.frame.contentWindow;
|
|
198
|
-
win.addEventListener("keydown", onKey, true);
|
|
199
|
-
} catch {}
|
|
293
|
+
try { els.frame.contentWindow.addEventListener("keydown", onKey, true); } catch {}
|
|
200
294
|
}
|
|
201
295
|
|
|
202
|
-
// ---------- Routing
|
|
296
|
+
// ---------- Routing: "" home, #games, #recent, #play/<id> ----------
|
|
203
297
|
function route() {
|
|
204
298
|
const m = location.hash.match(/^#play\/(.+)$/);
|
|
205
299
|
const game = m && byId.get(decodeURIComponent(m[1]));
|
|
@@ -208,36 +302,41 @@
|
|
|
208
302
|
document.body.classList.add("playing");
|
|
209
303
|
addRecent(game.id);
|
|
210
304
|
if (currentLoad?.id !== game.id) loadGame(game);
|
|
211
|
-
|
|
212
|
-
stopGame();
|
|
213
|
-
els.player.hidden = true;
|
|
214
|
-
document.body.classList.remove("playing");
|
|
215
|
-
document.title = "Richards Unblocked Games";
|
|
305
|
+
return;
|
|
216
306
|
}
|
|
307
|
+
stopGame();
|
|
308
|
+
els.player.hidden = true;
|
|
309
|
+
document.body.classList.remove("playing");
|
|
310
|
+
document.title = SITE_NAME;
|
|
311
|
+
const view = location.hash === "#games" || location.hash === "#recent" ? "games" : "home";
|
|
312
|
+
els.homeView.hidden = view !== "home";
|
|
313
|
+
els.gamesView.hidden = view !== "games";
|
|
314
|
+
document.querySelectorAll(".rail-btn[data-nav]").forEach((b) =>
|
|
315
|
+
b.classList.toggle("active", b.dataset.nav === (location.hash === "#recent" ? "recent" : view)));
|
|
316
|
+
if (view === "games") renderRecent();
|
|
317
|
+
if (location.hash === "#recent") requestAnimationFrame(() => els.recentBlock.scrollIntoView({ block: "start" }));
|
|
217
318
|
}
|
|
218
319
|
function play(id) { location.hash = `#play/${encodeURIComponent(id)}`; }
|
|
219
|
-
function
|
|
220
|
-
|
|
320
|
+
function go(view) {
|
|
321
|
+
const target = view === "home" ? "" : `#${view}`;
|
|
322
|
+
if (location.hash !== target) history.pushState(null, "", target || location.pathname + location.search);
|
|
323
|
+
route();
|
|
324
|
+
if (view !== "recent") window.scrollTo(0, 0);
|
|
325
|
+
}
|
|
326
|
+
function goBack() {
|
|
327
|
+
if (location.hash.startsWith("#play/")) history.length > 1 ? history.back() : (location.hash = "#games");
|
|
328
|
+
else if (location.hash) go("home");
|
|
221
329
|
}
|
|
222
330
|
|
|
223
331
|
// ---------- Utilities ----------
|
|
224
|
-
//
|
|
332
|
+
// If this page is ever shown inside a frame, redirect the whole tab
|
|
225
333
|
function panic() {
|
|
226
334
|
const url = CFG.panicUrl || "https://classroom.google.com/";
|
|
227
335
|
try { window.top.location.replace(url); } catch { location.replace(url); }
|
|
228
336
|
}
|
|
229
337
|
|
|
230
|
-
function onKey(e) {
|
|
231
|
-
const keys = CFG.panicKeys || ["`", "]"];
|
|
232
|
-
if (keys.includes(e.key)) { e.preventDefault(); panic(); return; }
|
|
233
|
-
if (e.key === "/" && document.activeElement !== els.search && els.player.hidden) {
|
|
234
|
-
e.preventDefault(); els.search.focus();
|
|
235
|
-
}
|
|
236
|
-
if (e.key === "Escape" && document.activeElement === els.search) { els.search.value = ""; renderGrid(); els.search.blur(); }
|
|
237
|
-
}
|
|
238
|
-
|
|
239
338
|
function randomGame() {
|
|
240
|
-
const pool = activeTag
|
|
339
|
+
const pool = !els.gamesView.hidden && activeTag !== "All" ? games.filter((g) => g.tag === activeTag) : games;
|
|
241
340
|
if (pool.length) play(pool[Math.floor(Math.random() * pool.length)].id);
|
|
242
341
|
}
|
|
243
342
|
|
|
@@ -248,9 +347,8 @@
|
|
|
248
347
|
const d = new Date();
|
|
249
348
|
let h = d.getHours();
|
|
250
349
|
const m = String(d.getMinutes()).padStart(2, "0");
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
else { const ap = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; timeEl.textContent = `${h}:${m}:${s}`; metaEl.textContent = ap; }
|
|
350
|
+
if (is24) { timeEl.textContent = `${String(h).padStart(2, "0")}:${m}`; metaEl.textContent = "24H"; }
|
|
351
|
+
else { const ap = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; timeEl.textContent = `${h}:${m}`; metaEl.textContent = ap; }
|
|
254
352
|
};
|
|
255
353
|
$("clock").addEventListener("click", () => { is24 = !is24; store.set(CLOCK_KEY, is24); tick(); });
|
|
256
354
|
tick();
|
|
@@ -261,7 +359,6 @@
|
|
|
261
359
|
const btn = $("theme-btn");
|
|
262
360
|
const apply = (t) => {
|
|
263
361
|
document.documentElement.dataset.theme = t;
|
|
264
|
-
btn.textContent = t === "dark" ? "🌙" : "☀️";
|
|
265
362
|
btn.title = t === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
|
266
363
|
};
|
|
267
364
|
apply(document.documentElement.dataset.theme === "light" ? "light" : "dark");
|
|
@@ -272,32 +369,118 @@
|
|
|
272
369
|
});
|
|
273
370
|
}
|
|
274
371
|
|
|
372
|
+
// ---------- Arrow keys and controller move focus between tiles and buttons ----------
|
|
373
|
+
function focusables() {
|
|
374
|
+
const scope = els.player.hidden ? document : els.player;
|
|
375
|
+
return [...scope.querySelectorAll(".tile, .rail-btn, .tab, .per-row button, #search, #sort, #home-input, .text-btn, .pill-btn")]
|
|
376
|
+
.filter((el) => el.offsetParent !== null && !el.closest("[hidden]"));
|
|
377
|
+
}
|
|
378
|
+
function moveFocus(dir) {
|
|
379
|
+
const cur = document.activeElement;
|
|
380
|
+
const vw = innerWidth;
|
|
381
|
+
const list = focusables().filter((el) => { const r = el.getBoundingClientRect(); return r.right > 0 && r.left < vw; });
|
|
382
|
+
if (!list.includes(cur)) { (list.find((el) => el.classList.contains("tile")) || list[0])?.focus(); return; }
|
|
383
|
+
const a = cur.getBoundingClientRect();
|
|
384
|
+
const ax = a.left + a.width / 2, ay = a.top + a.height / 2;
|
|
385
|
+
let best = null, bestScore = Infinity;
|
|
386
|
+
for (const el of list) {
|
|
387
|
+
if (el === cur) continue;
|
|
388
|
+
const b = el.getBoundingClientRect();
|
|
389
|
+
const dx = b.left + b.width / 2 - ax, dy = b.top + b.height / 2 - ay;
|
|
390
|
+
const main = { left: -dx, right: dx, up: -dy, down: dy }[dir];
|
|
391
|
+
if (main <= 4) continue;
|
|
392
|
+
const cross = dir === "left" || dir === "right" ? Math.abs(dy) : Math.abs(dx);
|
|
393
|
+
const score = main + cross * 2.5;
|
|
394
|
+
if (score < bestScore) { bestScore = score; best = el; }
|
|
395
|
+
}
|
|
396
|
+
if (best) { best.focus({ preventScroll: true }); best.scrollIntoView({ block: "nearest", behavior: "smooth" }); }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function onKey(e) {
|
|
400
|
+
const keys = CFG.panicKeys || ["`", "]"];
|
|
401
|
+
if (keys.includes(e.key)) { e.preventDefault(); panic(); return; }
|
|
402
|
+
if (!els.player.hidden) {
|
|
403
|
+
if (e.key === "Escape" && document.activeElement !== els.frame) goBack();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const typing = document.activeElement === els.search || document.activeElement === els.homeInput;
|
|
407
|
+
if (e.key === "/" && !typing) {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
(els.homeView.hidden ? els.search : els.homeInput).focus();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (e.key === "Escape") {
|
|
413
|
+
if (typing) { document.activeElement.value = ""; renderGrid(); document.activeElement.blur(); } else goBack();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const dir = { ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", ArrowDown: "down" }[e.key];
|
|
417
|
+
if (dir && !(typing && (dir === "left" || dir === "right"))) { e.preventDefault(); moveFocus(dir); }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Gamepad: d-pad/left stick move, A select, B back, Y random. In a game only View (⧉) leaves,
|
|
421
|
+
// since games use A/B themselves.
|
|
422
|
+
function startGamepad() {
|
|
423
|
+
const prev = {};
|
|
424
|
+
let lastMove = 0;
|
|
425
|
+
const press = (i) => {
|
|
426
|
+
if (!els.player.hidden) { if (i === 8) goBack(); return; }
|
|
427
|
+
if (i === 0) document.activeElement?.click?.();
|
|
428
|
+
else if (i === 1) goBack();
|
|
429
|
+
else if (i === 3) randomGame();
|
|
430
|
+
else if (i === 12) moveFocus("up");
|
|
431
|
+
else if (i === 13) moveFocus("down");
|
|
432
|
+
else if (i === 14) moveFocus("left");
|
|
433
|
+
else if (i === 15) moveFocus("right");
|
|
434
|
+
};
|
|
435
|
+
function poll(t) {
|
|
436
|
+
const pads = navigator.getGamepads ? [...navigator.getGamepads()].filter(Boolean) : [];
|
|
437
|
+
for (const pad of pads) {
|
|
438
|
+
pad.buttons.forEach((b, i) => {
|
|
439
|
+
const key = `${pad.index}:${i}`;
|
|
440
|
+
if (b.pressed && !prev[key]) press(i);
|
|
441
|
+
prev[key] = b.pressed;
|
|
442
|
+
});
|
|
443
|
+
const [x, y] = pad.axes;
|
|
444
|
+
if (els.player.hidden && t - lastMove > 180 && (Math.abs(x) > 0.6 || Math.abs(y) > 0.6)) {
|
|
445
|
+
moveFocus(Math.abs(x) > Math.abs(y) ? (x > 0 ? "right" : "left") : (y > 0 ? "down" : "up"));
|
|
446
|
+
lastMove = t;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (pads.length) requestAnimationFrame(poll);
|
|
450
|
+
}
|
|
451
|
+
window.addEventListener("gamepadconnected", () => requestAnimationFrame(poll));
|
|
452
|
+
}
|
|
453
|
+
|
|
275
454
|
// ---------- Wire up ----------
|
|
276
455
|
function bind() {
|
|
277
456
|
document.addEventListener("keydown", onKey, true);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const chip = e.target.closest(".chip"); if (!chip) return;
|
|
284
|
-
activeTag = chip.dataset.tag;
|
|
285
|
-
els.chips.querySelectorAll(".chip").forEach((c) => c.setAttribute("aria-selected", c === chip));
|
|
286
|
-
renderGrid();
|
|
457
|
+
document.addEventListener("click", (e) => {
|
|
458
|
+
const tile = e.target.closest(".tile");
|
|
459
|
+
if (tile) { play(tile.dataset.id); return; }
|
|
460
|
+
const nav = e.target.closest("[data-nav]");
|
|
461
|
+
if (nav) { e.preventDefault(); go(nav.dataset.nav); }
|
|
287
462
|
});
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
els.
|
|
463
|
+
// Typing on the home search jumps straight into the games list
|
|
464
|
+
const fromHome = () => {
|
|
465
|
+
els.search.value = els.homeInput.value;
|
|
466
|
+
els.homeInput.value = "";
|
|
467
|
+
setTag("All");
|
|
468
|
+
go("games");
|
|
469
|
+
els.search.focus();
|
|
292
470
|
renderGrid();
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
});
|
|
471
|
+
};
|
|
472
|
+
els.homeInput.addEventListener("input", () => { if (els.homeInput.value.trim()) fromHome(); });
|
|
473
|
+
$("home-search").addEventListener("submit", (e) => { e.preventDefault(); fromHome(); });
|
|
474
|
+
|
|
475
|
+
els.search.addEventListener("input", renderGrid);
|
|
476
|
+
els.sort.addEventListener("change", renderGrid);
|
|
477
|
+
els.chips.addEventListener("click", (e) => { const tab = e.target.closest(".tab"); if (tab) setTag(tab.dataset.tag); });
|
|
478
|
+
document.querySelectorAll(".per-row button").forEach((b) => b.addEventListener("click", () => setPerRow(Number(b.dataset.per))));
|
|
296
479
|
$("clear-recent").addEventListener("click", () => { store.set(RECENT_KEY, []); renderRecent(); });
|
|
297
480
|
$("random-btn").addEventListener("click", randomGame);
|
|
298
481
|
$("panic-btn").addEventListener("click", panic);
|
|
299
482
|
$("panic-btn-2").addEventListener("click", panic);
|
|
300
|
-
$("back-btn").addEventListener("click",
|
|
483
|
+
$("back-btn").addEventListener("click", goBack);
|
|
301
484
|
$("reload-btn").addEventListener("click", () => {
|
|
302
485
|
const g = currentLoad && byId.get(currentLoad.id);
|
|
303
486
|
if (g) loadGame(g);
|
|
@@ -308,11 +491,14 @@
|
|
|
308
491
|
(el.requestFullscreen || el.webkitRequestFullscreen)?.call(el);
|
|
309
492
|
});
|
|
310
493
|
window.addEventListener("hashchange", route);
|
|
494
|
+
window.addEventListener("popstate", route);
|
|
311
495
|
}
|
|
312
496
|
|
|
313
497
|
async function init() {
|
|
314
498
|
initTheme();
|
|
315
499
|
startClock();
|
|
500
|
+
startGamepad();
|
|
501
|
+
setPerRow(store.get(PER_ROW_KEY, 6));
|
|
316
502
|
bind();
|
|
317
503
|
try {
|
|
318
504
|
const res = await fetch("games.json", { cache: "no-cache" });
|
|
@@ -321,14 +507,16 @@
|
|
|
321
507
|
byId = new Map(games.map((g) => [g.id, g]));
|
|
322
508
|
} catch (err) {
|
|
323
509
|
console.error(err);
|
|
324
|
-
els.
|
|
325
|
-
els.empty.textContent = "Couldn't load the game list (games.json).";
|
|
510
|
+
els.homeSub.textContent = "Couldn't load the game list. Try refreshing.";
|
|
326
511
|
return;
|
|
327
512
|
}
|
|
513
|
+
els.homeInput.placeholder = `Search ${games.length} games…`;
|
|
514
|
+
els.homeSub.innerHTML = `${games.length} games · press <kbd>/</kbd> to search`;
|
|
328
515
|
els.footerCount.textContent = `${games.length} games`;
|
|
329
516
|
renderChips();
|
|
330
|
-
renderGrid();
|
|
331
517
|
renderRecent();
|
|
518
|
+
renderGrid();
|
|
519
|
+
startMarquee();
|
|
332
520
|
route();
|
|
333
521
|
}
|
|
334
522
|
|