4sp-dv 1.0.39 → 1.0.40
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/4simpleproblems_v5.html +60 -4
- package/logged-in/games.html +135 -573
- package/package.json +1 -1
package/4simpleproblems_v5.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>4SP - VERSION 5 CLIENT</title>
|
|
7
7
|
<link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo.png">
|
|
8
8
|
|
|
9
|
-
<base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.
|
|
9
|
+
<base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.39/logged-in/">
|
|
10
10
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
|
12
12
|
|
|
@@ -468,9 +468,33 @@
|
|
|
468
468
|
#universal-loader.active {
|
|
469
469
|
pointer-events: auto;
|
|
470
470
|
}
|
|
471
|
+
|
|
472
|
+
/* Offline Indicator */
|
|
473
|
+
#offline-indicator {
|
|
474
|
+
position: fixed;
|
|
475
|
+
top: 1rem;
|
|
476
|
+
left: 50%;
|
|
477
|
+
transform: translateX(-50%);
|
|
478
|
+
background: rgba(239, 68, 68, 0.2);
|
|
479
|
+
border: 1px solid rgba(239, 68, 68, 0.5);
|
|
480
|
+
color: #f87171;
|
|
481
|
+
padding: 4px 12px;
|
|
482
|
+
border-radius: 20px;
|
|
483
|
+
font-size: 0.75rem;
|
|
484
|
+
z-index: 9999;
|
|
485
|
+
display: none;
|
|
486
|
+
align-items: center;
|
|
487
|
+
gap: 6px;
|
|
488
|
+
backdrop-filter: blur(4px);
|
|
489
|
+
}
|
|
490
|
+
#offline-indicator.show { display: flex; }
|
|
471
491
|
</style>
|
|
472
492
|
</head>
|
|
473
493
|
<body>
|
|
494
|
+
<div id="offline-indicator">
|
|
495
|
+
<i class="fa-solid fa-plane"></i>
|
|
496
|
+
<span>Offline Mode</span>
|
|
497
|
+
</div>
|
|
474
498
|
<div id="lock-screen" class="w-full h-full flex flex-col items-center justify-center p-4">
|
|
475
499
|
<div class="max-w-5xl w-full bg-[#040404] flex flex-col md:flex-row border border-[#252525] rounded-3xl overflow-hidden shadow-2xl">
|
|
476
500
|
<div class="flex-1 p-8 flex flex-col justify-center items-center border-b md:border-b-0 md:border-r border-[#252525]">
|
|
@@ -778,7 +802,7 @@
|
|
|
778
802
|
const displayUsername = document.getElementById('display-username');
|
|
779
803
|
const displayEmail = document.getElementById('display-email');
|
|
780
804
|
|
|
781
|
-
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.
|
|
805
|
+
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.39/logged-in/';
|
|
782
806
|
|
|
783
807
|
// Preload Logos
|
|
784
808
|
const preloadImgs = [
|
|
@@ -1189,7 +1213,8 @@
|
|
|
1189
1213
|
pfpLetterBg: data.pfpLetterBg,
|
|
1190
1214
|
letterAvatarText: data.letterAvatarText,
|
|
1191
1215
|
letterAvatarTextColor: data.letterAvatarTextColor, // New Field
|
|
1192
|
-
navbarTheme: data.navbarTheme // Sync theme if updated remotely
|
|
1216
|
+
navbarTheme: data.navbarTheme, // Sync theme if updated remotely
|
|
1217
|
+
verified_online: true // Mark as verified for offline use
|
|
1193
1218
|
};
|
|
1194
1219
|
|
|
1195
1220
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(newUserData));
|
|
@@ -1280,6 +1305,26 @@
|
|
|
1280
1305
|
unlockBtn.classList.add('hidden');
|
|
1281
1306
|
codeInput.classList.add('hidden');
|
|
1282
1307
|
|
|
1308
|
+
// --- OFFLINE CHECK ---
|
|
1309
|
+
if (!navigator.onLine) {
|
|
1310
|
+
console.log("Client is offline. Checking for bypass...");
|
|
1311
|
+
let cachedUser = null;
|
|
1312
|
+
try { cachedUser = JSON.parse(localStorage.getItem(USER_DATA_KEY)); } catch(e){}
|
|
1313
|
+
|
|
1314
|
+
if (cachedUser && cachedUser.verified_online) {
|
|
1315
|
+
console.log("Offline bypass granted for verified user.");
|
|
1316
|
+
currentUser = cachedUser;
|
|
1317
|
+
updateUIWithUser(cachedUser);
|
|
1318
|
+
const indicator = document.getElementById('offline-indicator');
|
|
1319
|
+
if (indicator) indicator.classList.add('show');
|
|
1320
|
+
launchApp();
|
|
1321
|
+
return;
|
|
1322
|
+
} else {
|
|
1323
|
+
resetLockScreen("You are offline. Please connect to the internet to verify your code.");
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1283
1328
|
try {
|
|
1284
1329
|
const docRef = doc(db, "access_codes", savedCode);
|
|
1285
1330
|
const docSnap = await getDoc(docRef);
|
|
@@ -1311,7 +1356,18 @@
|
|
|
1311
1356
|
}
|
|
1312
1357
|
} catch(e) {
|
|
1313
1358
|
console.error("Auto-login error:", e);
|
|
1314
|
-
|
|
1359
|
+
// Check if we can fallback to offline even on error (e.g. timeout)
|
|
1360
|
+
let cachedUser = null;
|
|
1361
|
+
try { cachedUser = JSON.parse(localStorage.getItem(USER_DATA_KEY)); } catch(err){}
|
|
1362
|
+
if (cachedUser && cachedUser.verified_online) {
|
|
1363
|
+
currentUser = cachedUser;
|
|
1364
|
+
updateUIWithUser(cachedUser);
|
|
1365
|
+
const indicator = document.getElementById('offline-indicator');
|
|
1366
|
+
if (indicator) indicator.classList.add('show');
|
|
1367
|
+
launchApp();
|
|
1368
|
+
} else {
|
|
1369
|
+
resetLockScreen("Connection failed. Retrying...");
|
|
1370
|
+
}
|
|
1315
1371
|
}
|
|
1316
1372
|
}
|
|
1317
1373
|
}
|
package/logged-in/games.html
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
window.dataLayer = window.dataLayer || [];
|
|
21
21
|
function gtag(){dataLayer.push(arguments);}
|
|
22
22
|
gtag('js', new Date());
|
|
23
|
-
|
|
24
23
|
gtag('config', 'G-1D4F692C1Q');
|
|
25
24
|
</script>
|
|
26
25
|
<script>
|
|
@@ -83,7 +82,7 @@
|
|
|
83
82
|
.btn-toolbar-style {
|
|
84
83
|
background: var(--menu-bg);
|
|
85
84
|
border: 1px solid var(--menu-border);
|
|
86
|
-
border-radius: 14px;
|
|
85
|
+
border-radius: 14px;
|
|
87
86
|
color: var(--menu-text);
|
|
88
87
|
padding: 0.5rem 1.25rem;
|
|
89
88
|
font-weight: 500;
|
|
@@ -110,7 +109,7 @@
|
|
|
110
109
|
justify-content: center;
|
|
111
110
|
width: 2.25rem;
|
|
112
111
|
height: 2.25rem;
|
|
113
|
-
border-radius: 14px;
|
|
112
|
+
border-radius: 14px;
|
|
114
113
|
background-color: transparent;
|
|
115
114
|
border: 1px solid transparent;
|
|
116
115
|
color: #9ca3af;
|
|
@@ -174,11 +173,6 @@
|
|
|
174
173
|
.animate-fade-in { animation: fadeIn 0.3s ease-out forwards; }
|
|
175
174
|
.animate-fade-out { animation: fadeOut 0.3s ease-out forwards; }
|
|
176
175
|
|
|
177
|
-
.fade-out-card {
|
|
178
|
-
animation: fadeOut 0.3s ease-out forwards;
|
|
179
|
-
pointer-events: none;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
176
|
/* Search Bar & Results */
|
|
183
177
|
#bottom-fixed-bar {
|
|
184
178
|
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease;
|
|
@@ -267,7 +261,7 @@
|
|
|
267
261
|
background-color: #1a1a1a;
|
|
268
262
|
color: #d1d5db;
|
|
269
263
|
border: 1px solid #333;
|
|
270
|
-
border-radius: 14px;
|
|
264
|
+
border-radius: 14px;
|
|
271
265
|
cursor: pointer;
|
|
272
266
|
font-size: 1rem;
|
|
273
267
|
display: flex;
|
|
@@ -286,11 +280,11 @@
|
|
|
286
280
|
#zoneViewer iframe { flex-grow: 1; border: none; background-color: #000; }
|
|
287
281
|
.hidden { display: none !important; }
|
|
288
282
|
|
|
289
|
-
/* --- Dropdown
|
|
283
|
+
/* --- Dropdown --- */
|
|
290
284
|
.eagler-dropdown {
|
|
291
285
|
display: none;
|
|
292
286
|
position: absolute;
|
|
293
|
-
bottom: 120%;
|
|
287
|
+
bottom: 120%;
|
|
294
288
|
right: 0;
|
|
295
289
|
margin-bottom: 5px;
|
|
296
290
|
background-color: rgba(20, 20, 20, 0.98);
|
|
@@ -387,22 +381,16 @@
|
|
|
387
381
|
<div id="bottom-fixed-bar" class="fixed bottom-4 right-4 bg-black/80 backdrop-blur-xl rounded-3xl border border-brand-border shadow-2xl shadow-black/50 w-full max-w-md z-50 transition-all duration-300 flex flex-col">
|
|
388
382
|
<div id="searchResults" class="search-results-container w-full" style="display: none;">
|
|
389
383
|
</div>
|
|
390
|
-
|
|
391
384
|
<div id="searchDivider" class="w-full h-[1px] bg-white/10" style="display: none;"></div>
|
|
392
|
-
|
|
393
385
|
<div class="flex items-center w-full p-1">
|
|
394
|
-
<div class="pl-4 pr-2 text-gray-400">
|
|
395
|
-
<i class="fa-solid fa-magnifying-glass"></i>
|
|
396
|
-
</div>
|
|
386
|
+
<div class="pl-4 pr-2 text-gray-400"><i class="fa-solid fa-magnifying-glass"></i></div>
|
|
397
387
|
<input type="text" id="searchInput" placeholder="Search for games…" autocomplete="off" class="w-full py-3 pr-4 bg-transparent text-white placeholder-gray-500 focus:outline-none focus:ring-0 text-base" />
|
|
398
388
|
</div>
|
|
399
389
|
</div>
|
|
400
390
|
|
|
401
391
|
<div id="zoneViewer" aria-modal="true" role="dialog">
|
|
402
392
|
<div class="zone-header">
|
|
403
|
-
<div class="zone-title">
|
|
404
|
-
<h2 id="zoneNameEl">Game Title</h2>
|
|
405
|
-
</div>
|
|
393
|
+
<div class="zone-title"><h2 id="zoneNameEl">Game Title</h2></div>
|
|
406
394
|
<div class="zone-controls">
|
|
407
395
|
<button id="fullscreenBtnZone" title="Fullscreen"><i class="fas fa-expand"></i></button>
|
|
408
396
|
<a id="downloadBtnZone" title="Download" class="hidden" href="#"><i class="fas fa-download"></i></a>
|
|
@@ -418,8 +406,7 @@
|
|
|
418
406
|
<h3 class="text-xl font-bold">Project Credits</h3>
|
|
419
407
|
<button id="closeCreditsBtn" class="text-white hover:text-gray-400 p-2"><i class="fas fa-times"></i></button>
|
|
420
408
|
</div>
|
|
421
|
-
<div id="creditsContent" class="p-6 space-y-6">
|
|
422
|
-
</div>
|
|
409
|
+
<div id="creditsContent" class="p-6 space-y-6"></div>
|
|
423
410
|
</div>
|
|
424
411
|
</div>
|
|
425
412
|
|
|
@@ -428,31 +415,11 @@
|
|
|
428
415
|
<div class="p-6 sm:p-8 overflow-y-auto custom-scrollbar">
|
|
429
416
|
<h3 class="text-2xl font-bold mb-4 text-center">Welcome to 4SP Games</h3>
|
|
430
417
|
<p class="text-sm text-gray-400 mb-6 text-center">Please read the following information before playing.</p>
|
|
431
|
-
|
|
432
418
|
<ul class="space-y-4 text-gray-300 text-sm sm:text-base leading-relaxed">
|
|
433
|
-
<li class="flex items-start">
|
|
434
|
-
|
|
435
|
-
<span><strong>Navigation:</strong> Use the arrows next to the category title (currently "<strong>StrongdogXP</strong>") to switch between different game collections like <strong>GN-Math</strong> and <strong>Others</strong>.</span>
|
|
436
|
-
</li>
|
|
437
|
-
<li class="flex items-start">
|
|
438
|
-
<i class="fas fa-file-signature mt-1 mr-3 text-accent-indigo"></i>
|
|
439
|
-
<span><strong>Rebranding:</strong> "4SP Game Hub" is now shortened to "<strong>4SP Games</strong>".</span>
|
|
440
|
-
</li>
|
|
441
|
-
<li class="flex items-start">
|
|
442
|
-
<i class="fas fa-bug mt-1 mr-3 text-strongdog-orange"></i>
|
|
443
|
-
<span><strong>Stability Disclaimer:</strong> Some games may be unstable, glitchy, or fail to load. This is often due to browser restrictions or the age of the game files.</span>
|
|
444
|
-
</li>
|
|
445
|
-
<li class="flex items-start">
|
|
446
|
-
<i class="fas fa-user-gear mt-1 mr-3 text-gn-math"></i>
|
|
447
|
-
<span><strong>Management:</strong> I do not personally manage or host the files for these game collections. I cannot directly fix broken game files or save data issues.</span>
|
|
448
|
-
</li>
|
|
449
|
-
<li class="flex items-start">
|
|
450
|
-
<i class="fas fa-server mt-1 mr-3 text-gn-math"></i>
|
|
451
|
-
<span><strong>GN-Math & CDN:</strong> The <strong>GN-Math</strong> category is particularly hard to modify because it uses a <em>CDN (Content Delivery Network)</em>. This means the game files are hosted on remote servers around the world to load faster, rather than being stored here. I cannot edit those files.</span>
|
|
452
|
-
</li>
|
|
419
|
+
<li class="flex items-start"><i class="fas fa-compass mt-1 mr-3 text-accent-indigo"></i><span><strong>Navigation:</strong> Use the arrows next to the category title to switch between collections.</span></li>
|
|
420
|
+
<li class="flex items-start"><i class="fas fa-bug mt-1 mr-3 text-strongdog-orange"></i><span><strong>Stability:</strong> Some games may be unstable due to age or browser restrictions.</span></li>
|
|
453
421
|
</ul>
|
|
454
422
|
</div>
|
|
455
|
-
|
|
456
423
|
<div class="p-6 border-t border-gray-800 bg-black/20 rounded-b-3xl flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
457
424
|
<span class="text-xs text-gray-500">Type "I understand" to continue.</span>
|
|
458
425
|
<input type="text" id="instruction-input" placeholder="Type 'I understand'" class="w-full sm:w-auto px-6 py-3 border border-transparent text-base font-normal rounded-xl text-white bg-purple-500/10 ring-1 ring-purple-500/50 focus:ring-purple-500/80 focus:bg-purple-500/20 focus:outline-none placeholder-gray-400 transition-all duration-200">
|
|
@@ -463,7 +430,7 @@
|
|
|
463
430
|
<div id="notification-container"></div>
|
|
464
431
|
|
|
465
432
|
<script>
|
|
466
|
-
// ---
|
|
433
|
+
// --- Globals ---
|
|
467
434
|
const GN_ZONES_URL = "https://cdn.jsdelivr.net/gh/gn-math/assets@main/zones.json";
|
|
468
435
|
const GN_COVER_URL_BASE = "https://cdn.jsdelivr.net/gh/gn-math/covers@main";
|
|
469
436
|
const GN_HTML_URL_BASE = "https://raw.githack.com/gn-math/html/main";
|
|
@@ -471,190 +438,42 @@
|
|
|
471
438
|
const GAMES_BASE_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/";
|
|
472
439
|
const CARDS_DATA_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/cards-data.js";
|
|
473
440
|
const FAVORITES_KEY = 'gameHubFavorites_v3';
|
|
441
|
+
const OFFLINE_STORE_NAME = 'games';
|
|
442
|
+
const DB_NAME = '4SP_OfflineGames';
|
|
474
443
|
|
|
475
|
-
let allGames = [];
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
let currentCategoryIndex = 0;
|
|
479
|
-
const categoryColors = {
|
|
480
|
-
'StrongdogXP': 'text-strongdog-orange',
|
|
481
|
-
'GN-Math': 'text-gn-math',
|
|
482
|
-
'Gameboy Games': 'text-gameboy',
|
|
483
|
-
'Others': 'text-other-grey',
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
const CREDITS_DATA = {
|
|
487
|
-
'StrongdogXP': { credit: "Core game collection provided by Josh P.", githubUrl: "https://github.com/jman1593/" },
|
|
488
|
-
'GN-Math': { credit: "Primary game source and infrastructure provided by the GN-Math Community.", githubUrl: "https://github.com/gn-math/" },
|
|
489
|
-
'Gameboy Games': { credit: "Emulator and ROM files from various public sources." },
|
|
490
|
-
'Others': { credit: "Various community developers and open-source projects (e.g., DOOM, Eaglercraft, Carnage3D, JS-DOS)." }
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
const favoritesHeader = document.getElementById("favoritesHeader");
|
|
494
|
-
const favoritesGameList = document.getElementById("favoritesGameList");
|
|
495
|
-
const searchInput = document.getElementById("searchInput");
|
|
496
|
-
const searchResults = document.getElementById("searchResults");
|
|
497
|
-
const searchDivider = document.getElementById("searchDivider");
|
|
498
|
-
const categorySlider = document.getElementById('category-slider');
|
|
499
|
-
const prevCategoryBtn = document.getElementById('prev-category');
|
|
500
|
-
const nextCategoryBtn = document.getElementById('next-category');
|
|
501
|
-
const gamesGridContainer = document.getElementById('games-grid-container');
|
|
502
|
-
const creditsModal = document.getElementById('creditsModal');
|
|
503
|
-
const creditsContent = document.getElementById('creditsContent');
|
|
504
|
-
const creditsBtn = document.getElementById('creditsBtn');
|
|
505
|
-
const closeCreditsBtn = document.getElementById('closeCreditsBtn');
|
|
506
|
-
const INSTRUCTION_KEY = '4sp-games-instruction-extended-seen';
|
|
507
|
-
const instructionOverlay = document.getElementById('instruction-overlay');
|
|
508
|
-
const instructionInput = document.getElementById('instruction-input');
|
|
509
|
-
|
|
510
|
-
// --- Intersection Observer ---
|
|
511
|
-
const imageObserver = new IntersectionObserver((entries, observer) => {
|
|
512
|
-
entries.forEach(entry => {
|
|
513
|
-
const img = entry.target;
|
|
514
|
-
if (entry.isIntersecting) {
|
|
515
|
-
if (img.dataset.src) img.src = img.dataset.src;
|
|
516
|
-
} else {
|
|
517
|
-
if(img.src && !img.src.includes('placehold.co') && !img.dataset.src) img.dataset.src = img.src;
|
|
518
|
-
if(img.dataset.src) img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
}, { rootMargin: "300px 0px", threshold: 0.01 });
|
|
522
|
-
|
|
523
|
-
function observeImages(container) {
|
|
524
|
-
const images = container.querySelectorAll('img[data-src]');
|
|
525
|
-
images.forEach(img => imageObserver.observe(img));
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// --- URL Management ---
|
|
529
|
-
function updateURL(category, gameId = null) {
|
|
530
|
-
let hash = `#${encodeURIComponent(category.replace(/\s+/g, '-'))}`;
|
|
531
|
-
if (gameId) hash += `?id=${gameId}`;
|
|
532
|
-
try { history.replaceState(null, null, hash); } catch(e) { console.warn('History API not supported'); }
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function parseURL() {
|
|
536
|
-
const hash = window.location.hash.substring(1);
|
|
537
|
-
if (!hash) return { category: null, gameId: null };
|
|
538
|
-
const parts = hash.split('?');
|
|
539
|
-
let rawCategory = parts[0];
|
|
540
|
-
let gameId = null;
|
|
541
|
-
if (parts[1]) {
|
|
542
|
-
const params = new URLSearchParams(parts[1]);
|
|
543
|
-
gameId = params.get('id');
|
|
544
|
-
}
|
|
545
|
-
return { category: rawCategory, gameId };
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function setupInstructionOverlay() {
|
|
549
|
-
if (localStorage.getItem(INSTRUCTION_KEY) !== 'seen') {
|
|
550
|
-
instructionOverlay.style.display = 'flex';
|
|
551
|
-
document.body.style.overflow = 'hidden';
|
|
552
|
-
setTimeout(() => {
|
|
553
|
-
instructionOverlay.classList.remove('opacity-0');
|
|
554
|
-
instructionOverlay.classList.add('opacity-100');
|
|
555
|
-
}, 10);
|
|
556
|
-
instructionInput.addEventListener('input', (e) => {
|
|
557
|
-
if (e.target.value.trim().toLowerCase() === 'i understand') {
|
|
558
|
-
instructionOverlay.classList.remove('opacity-100');
|
|
559
|
-
instructionOverlay.classList.add('opacity-0');
|
|
560
|
-
setTimeout(() => {
|
|
561
|
-
instructionOverlay.style.display = 'none';
|
|
562
|
-
document.body.style.overflow = '';
|
|
563
|
-
localStorage.setItem(INSTRUCTION_KEY, 'seen');
|
|
564
|
-
}, 300);
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
}
|
|
444
|
+
let allGames = [], searchableGames = [], categories = [], currentCategoryIndex = 0;
|
|
445
|
+
const categoryColors = { 'StrongdogXP': 'text-strongdog-orange', 'GN-Math': 'text-gn-math', 'Gameboy Games': 'text-gameboy', 'Others': 'text-other-grey' };
|
|
446
|
+
const CREDITS_DATA = { 'StrongdogXP': { credit: "Core collection by Josh P." }, 'GN-Math': { credit: "GN-Math Community." }, 'Others': { credit: "Various community projects." } };
|
|
569
447
|
|
|
570
|
-
// ---
|
|
448
|
+
// --- Core Functions ---
|
|
571
449
|
const getFavorites = () => JSON.parse(localStorage.getItem(FAVORITES_KEY)) || [];
|
|
572
450
|
const saveFavorites = (favs) => localStorage.setItem(FAVORITES_KEY, JSON.stringify(favs));
|
|
573
451
|
|
|
574
|
-
function
|
|
575
|
-
const strGameId = String(gameId);
|
|
576
|
-
let favorites = getFavorites().map(String);
|
|
577
|
-
const isFavorited = favorites.includes(strGameId);
|
|
578
|
-
|
|
579
|
-
if (isFavorited) {
|
|
580
|
-
favorites = favorites.filter(id => id !== strGameId);
|
|
581
|
-
saveFavorites(favorites);
|
|
582
|
-
updateAllFavoriteButtons();
|
|
583
|
-
const favCard = favoritesGameList.querySelector(`[data-game-id='${strGameId}']`);
|
|
584
|
-
if (favCard) {
|
|
585
|
-
favCard.classList.add('animate-fade-out');
|
|
586
|
-
setTimeout(() => {
|
|
587
|
-
favCard.remove();
|
|
588
|
-
if (favoritesGameList.children.length === 0) {
|
|
589
|
-
favoritesHeader.style.display = 'none';
|
|
590
|
-
favoritesGameList.style.display = 'none';
|
|
591
|
-
}
|
|
592
|
-
}, 300);
|
|
593
|
-
}
|
|
594
|
-
} else {
|
|
595
|
-
favorites.push(strGameId);
|
|
596
|
-
saveFavorites(favorites);
|
|
597
|
-
updateAllFavoriteButtons();
|
|
598
|
-
let gameData = searchableGames.find(g => String(g.id) === strGameId);
|
|
599
|
-
if (gameData) {
|
|
600
|
-
favoritesHeader.style.display = "block";
|
|
601
|
-
favoritesGameList.style.display = "grid";
|
|
602
|
-
favoritesGameList.appendChild(createGameCard(gameData, true));
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function updateAllFavoriteButtons() {
|
|
608
|
-
const favorites = getFavorites().map(String);
|
|
609
|
-
document.querySelectorAll('.btn-card-action.fav-action').forEach(btn => {
|
|
610
|
-
const card = btn.closest('[data-game-id]');
|
|
611
|
-
if (!card) return;
|
|
612
|
-
const cardGameId = String(card.dataset.gameId);
|
|
613
|
-
const isFavorited = favorites.includes(cardGameId);
|
|
614
|
-
btn.classList.toggle('favorited', isFavorited);
|
|
615
|
-
btn.title = isFavorited ? 'Remove from Favorites' : 'Add to Favorites';
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// --- Offline Mode (IndexedDB) ---
|
|
620
|
-
const DB_NAME = '4SP_OfflineGames';
|
|
621
|
-
const DB_VERSION = 1;
|
|
622
|
-
const STORE_NAME = 'games';
|
|
623
|
-
|
|
624
|
-
function openDB() {
|
|
452
|
+
async function openDB() {
|
|
625
453
|
return new Promise((resolve, reject) => {
|
|
626
|
-
const request = indexedDB.open(DB_NAME,
|
|
627
|
-
request.onupgradeneeded = (e) => {
|
|
628
|
-
const db = e.target.result;
|
|
629
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
|
630
|
-
};
|
|
454
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
455
|
+
request.onupgradeneeded = (e) => { if (!e.target.result.objectStoreNames.contains(OFFLINE_STORE_NAME)) e.target.result.createObjectStore(OFFLINE_STORE_NAME, { keyPath: 'id' }); };
|
|
631
456
|
request.onsuccess = (e) => resolve(e.target.result);
|
|
632
457
|
request.onerror = (e) => reject(e.target.error);
|
|
633
458
|
});
|
|
634
459
|
}
|
|
635
460
|
|
|
636
461
|
async function saveGameOffline(gameId, name, url) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
tx.oncomplete = () => { db.close(); resolve(); };
|
|
647
|
-
tx.onerror = () => reject(tx.error);
|
|
648
|
-
});
|
|
649
|
-
} catch (e) { console.error("Offline Save Error:", e); throw e; }
|
|
462
|
+
const res = await fetch(url);
|
|
463
|
+
const html = await res.text();
|
|
464
|
+
const db = await openDB();
|
|
465
|
+
return new Promise((resolve, reject) => {
|
|
466
|
+
const tx = db.transaction(OFFLINE_STORE_NAME, 'readwrite');
|
|
467
|
+
tx.objectStore(OFFLINE_STORE_NAME).put({ id: String(gameId), name, html, timestamp: Date.now() });
|
|
468
|
+
tx.oncomplete = () => { db.close(); resolve(); };
|
|
469
|
+
tx.onerror = () => reject(tx.error);
|
|
470
|
+
});
|
|
650
471
|
}
|
|
651
472
|
|
|
652
473
|
async function getOfflineGame(gameId) {
|
|
653
474
|
const db = await openDB();
|
|
654
475
|
return new Promise((resolve, reject) => {
|
|
655
|
-
const
|
|
656
|
-
const store = tx.objectStore(STORE_NAME);
|
|
657
|
-
const request = store.get(String(gameId));
|
|
476
|
+
const request = db.transaction(OFFLINE_STORE_NAME, 'readonly').objectStore(OFFLINE_STORE_NAME).get(String(gameId));
|
|
658
477
|
request.onsuccess = () => { db.close(); resolve(request.result); };
|
|
659
478
|
request.onerror = () => reject(request.error);
|
|
660
479
|
});
|
|
@@ -663,249 +482,43 @@
|
|
|
663
482
|
async function removeOfflineGame(gameId) {
|
|
664
483
|
const db = await openDB();
|
|
665
484
|
return new Promise((resolve, reject) => {
|
|
666
|
-
const tx = db.transaction(
|
|
667
|
-
|
|
668
|
-
store.delete(String(gameId));
|
|
485
|
+
const tx = db.transaction(OFFLINE_STORE_NAME, 'readwrite');
|
|
486
|
+
tx.objectStore(OFFLINE_STORE_NAME).delete(String(gameId));
|
|
669
487
|
tx.oncomplete = () => { db.close(); resolve(); };
|
|
670
|
-
tx.onerror = () => reject(tx.error);
|
|
671
488
|
});
|
|
672
489
|
}
|
|
673
490
|
|
|
674
|
-
async function isGameOffline(gameId) {
|
|
675
|
-
const game = await getOfflineGame(gameId);
|
|
676
|
-
return !!game;
|
|
677
|
-
}
|
|
491
|
+
async function isGameOffline(gameId) { return !!(await getOfflineGame(gameId)); }
|
|
678
492
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
async function sd_getEmbedPath(adjustedHref, originalHref, page) {
|
|
691
|
-
let cleanHref = adjustedHref.replace(/index\.html$/, "").replace(/base\.html$/, "").replace(/\.html$/, "");
|
|
692
|
-
if (!cleanHref.endsWith("/")) cleanHref += "/";
|
|
693
|
-
const pathsToTry = [cleanHref + "game/index.html", cleanHref + "game/base.html", cleanHref + "gamereal/index.html", cleanHref + "gamereal/base.html", cleanHref + "index.html", cleanHref + "base.html", ];
|
|
694
|
-
try {
|
|
695
|
-
const response = await fetch(adjustedHref);
|
|
696
|
-
if (response.ok) {
|
|
697
|
-
const text = await response.text();
|
|
698
|
-
const match = text.match(/embedGame\((['"])(.*?)\1,\s*(['"])(.*?)\3\)/);
|
|
699
|
-
if (match) {
|
|
700
|
-
const resolvedPath = new URL(match[2], adjustedHref).href;
|
|
701
|
-
if (await sd_fileExists(resolvedPath)) return resolvedPath;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
} catch (error) {}
|
|
705
|
-
for (const path of pathsToTry) { if (await sd_fileExists(path)) return path; }
|
|
706
|
-
return adjustedHref;
|
|
707
|
-
}
|
|
708
|
-
async function sd_fileExists(url) { try { const response = await fetch(url, { method: "HEAD" }); return response.ok; } catch { return false; } }
|
|
709
|
-
|
|
710
|
-
function resolveGameUrl(relativePath) {
|
|
711
|
-
if (relativePath.startsWith('../')) return `${WORKER_ROOT}${relativePath.substring(3)}`;
|
|
712
|
-
return `${GAMES_BASE_URL}${relativePath.replace(/^\.\//, '')}`;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const othersGames = [
|
|
716
|
-
{ id: "other-eaglercraft", name: "Eaglercraft", imgSrc: "./images/eaglercraft.png", description: "The classic survival and building game with authentic block-based physics and multiplayer support, accessible directly in the browser.", category: "Others", versions: [ { name: "Release 1.12.2", url: "../EAGLERCRAFT/eaglercraft-1.12.2.html", favoriteId: "other-eaglercraft_1.12.2" }, { name: "Release 1.12.2 WASM", url: "../EAGLERCRAFT/eaglercraft-1.12.2-wasm.html", favoriteId: "other-eaglercraft_wasm", wasm: true } ] },
|
|
717
|
-
{ id: "other-gta", name: "Grand Theft Auto", imgSrc: "./images/gta.png", description: "The original top-down classic. Cause chaos, complete missions, or just go for a drive in three iconic cities.", category: "Others", versions: [ { name: "Carnage3D", url: "../CARNAGE3D/web/carnage3D.html", favoriteId: "other-gta_carnage3d" }, { name: "Grand Theft Auto (JS-DOS)", url: "../GTA-JSDOS/index.html", favoriteId: "other-gta_jsdos" } ] },
|
|
718
|
-
{ id: "other-simcity", name: "Sim City", imgSrc: "./images/simcity2000.png", description: "The original city-building classics. Design, build, and manage the city of your dreams in either the original Sim City or the detailed Sim City 2000.", category: "Others", versions: [ { name: "Sim City", url: "../SIM-CITY/index.html", favoriteId: "other-simcity_1" }, { name: "Sim City 2000", url: "../SIM-CITY-2/index.html", favoriteId: "other-simcity_2000" } ] },
|
|
719
|
-
{ id: "other-doom", name: "DOOM", imgSrc: "./images/doom.png", description: "The legendary first-person shooter that pioneered the genre, focusing on blasting through hordes of demons in a timeless, adrenaline-fueled classic.", category: "Others", url: "../DOOM/index.html" }
|
|
720
|
-
];
|
|
721
|
-
|
|
722
|
-
// --- Card Creation ---
|
|
723
|
-
function createOthersGameCard(game) {
|
|
724
|
-
const favorites = getFavorites().map(String);
|
|
725
|
-
const isAnyFavorite = game.versions ? favorites.some(favId => game.versions.some(v => String(v.favoriteId) === favId)) : favorites.includes(String(game.id));
|
|
726
|
-
const card = document.createElement("div");
|
|
727
|
-
card.className = 'other-zone-item bg-card-dark rounded-2xl border border-brand-border col-span-full shadow-lg';
|
|
728
|
-
card.dataset.gameId = game.id;
|
|
729
|
-
const resolvedImgSrc = resolveGameUrl(game.imgSrc);
|
|
730
|
-
let gameButtonsHtml = '', favoriteButtonHtml = '', offlineButtonHtml = '';
|
|
731
|
-
const isEaglercraft = game.id === 'other-eaglercraft';
|
|
732
|
-
|
|
733
|
-
if (game.versions && game.versions.length > 0) {
|
|
734
|
-
const availableVersions = game.versions;
|
|
735
|
-
if (availableVersions.length === 1) {
|
|
736
|
-
const vUrl = resolveGameUrl(availableVersions[0].url);
|
|
737
|
-
gameButtonsHtml = `<button class="btn-card-action play-action" data-url="${vUrl}" data-version-name="${availableVersions[0].name}" title="Play Game"><i class="fa-solid fa-play"></i></button>`;
|
|
738
|
-
} else {
|
|
739
|
-
const dropdownLinks = availableVersions.map(v => `<a href="#" class="eagler-dropdown-link" data-url="${resolveGameUrl(v.url)}" data-version-name="${v.name}">${v.name}</a>`).join('');
|
|
740
|
-
gameButtonsHtml = `<div class="relative"><button class="btn-card-action version-btn" title="Select Version"><i class="fa-solid fa-chevron-up"></i></button><div class="eagler-dropdown">${dropdownLinks}</div></div>`;
|
|
741
|
-
}
|
|
742
|
-
if (isEaglercraft) {
|
|
743
|
-
offlineButtonHtml = `<div class="relative"><button class="btn-card-action offline-action version-offline-btn" title="Offline Mode"><i class="fa-solid fa-cloud"></i></button><div class="eagler-dropdown">${game.versions.map(v => `<a href="#" class="eagler-dropdown-link offline-link" data-game-id="${v.favoriteId}" data-url="${resolveGameUrl(v.url)}" data-name="${game.name} - ${v.name}">${v.name}</a>`).join('')}</div></div>`;
|
|
744
|
-
}
|
|
745
|
-
favoriteButtonHtml = `<div class="relative"><button class="btn-card-action fav-action version-favorite-btn ${isAnyFavorite ? 'favorited' : ''}" title="Favorite a version"><i class="fa-solid fa-star fa-solid-star"></i><i class="fa-regular fa-star fa-regular-star"></i></button><div class="eagler-dropdown">${game.versions.map(v => `<a href="#" class="eagler-dropdown-link favorite-link" data-favorite-id="${v.favoriteId}">${v.name}</a>`).join('')}</div></div>`;
|
|
746
|
-
} else {
|
|
747
|
-
const gUrl = resolveGameUrl(game.url);
|
|
748
|
-
gameButtonsHtml = `<button class="btn-card-action play-action" data-url="${gUrl}" title="Play Game"><i class="fa-solid fa-play"></i></button>`;
|
|
749
|
-
if (isEaglercraft) offlineButtonHtml = `<button class="btn-card-action offline-action" data-game-id="${game.id}" data-url="${gUrl}" data-name="${game.name}" title="Enable Offline Mode"><i class="fa-solid fa-cloud"></i></button>`;
|
|
750
|
-
favoriteButtonHtml = `<button class="btn-card-action fav-action ${isAnyFavorite ? 'favorited' : ''}" title="${isAnyFavorite ? 'Remove from Favorites' : 'Add to Favorites'}"><i class="fa-solid fa-star fa-solid-star"></i><i class="fa-regular fa-star fa-regular-star"></i></button>`;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
card.innerHTML = `
|
|
754
|
-
<div class="relative w-full h-full cursor-pointer group" style="min-height: 250px;">
|
|
755
|
-
<img data-src="${resolvedImgSrc}" alt="${game.name}" loading="lazy" decoding="async" class="w-full h-full object-cover absolute inset-0 rounded-2xl">
|
|
756
|
-
<div class="image-overlay absolute inset-0 transition-colors duration-200"></div>
|
|
757
|
-
<div class="absolute inset-0 p-4 sm:p-6 flex flex-col justify-between rounded-2xl">
|
|
758
|
-
<div class="flex items-start justify-between w-full">
|
|
759
|
-
<h3 class="text-4xl font-bold text-white truncate drop-shadow-lg" style="max-width: 80%;" title="${game.name}">${game.name}</h3>
|
|
760
|
-
<div class="flex items-center gap-1.5 bg-black/50 backdrop-blur-sm rounded-2xl p-1.5">
|
|
761
|
-
${gameButtonsHtml} ${offlineButtonHtml} ${favoriteButtonHtml}
|
|
762
|
-
</div>
|
|
763
|
-
</div>
|
|
764
|
-
<p class="text-lg text-white font-medium drop-shadow-lg" style="max-width: 50%;">${game.description}</p>
|
|
765
|
-
</div>
|
|
766
|
-
</div>
|
|
767
|
-
`;
|
|
768
|
-
|
|
769
|
-
if (!game.versions) { card.querySelector('.group').addEventListener('click', () => openZone({...game, url: resolveGameUrl(game.url)})); }
|
|
770
|
-
card.querySelectorAll('.play-action').forEach(btn => btn.addEventListener('click', (e) => { e.stopPropagation(); openZone({ ...game, name: btn.dataset.versionName ? `${game.name} - ${btn.dataset.versionName}` : game.name, url: btn.dataset.url }); }));
|
|
771
|
-
|
|
772
|
-
const versionBtn = card.querySelector('.version-btn');
|
|
773
|
-
if (versionBtn) { versionBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); versionBtn.nextElementSibling.classList.toggle('show'); }); }
|
|
774
|
-
|
|
775
|
-
const offlineVerBtn = card.querySelector('.version-offline-btn');
|
|
776
|
-
if (offlineVerBtn) { offlineVerBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); offlineVerBtn.nextElementSibling.classList.toggle('show'); }); }
|
|
777
|
-
|
|
778
|
-
card.querySelectorAll('.offline-link').forEach(link => link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleOfflineMode(link.dataset.gameId, link.dataset.name, link.dataset.url, offlineVerBtn); link.closest('.eagler-dropdown').classList.remove('show'); }));
|
|
779
|
-
card.querySelectorAll('.eagler-dropdown-link:not(.favorite-link):not(.offline-link)').forEach(link => link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openZone({ ...game, name: link.dataset.versionName ? `${game.name} - ${link.dataset.versionName}` : game.name, url: link.dataset.url }); link.closest('.eagler-dropdown').classList.remove('show'); }));
|
|
780
|
-
|
|
781
|
-
const favBtn = card.querySelector('.btn-card-action.fav-action');
|
|
782
|
-
if (favBtn.classList.contains('version-favorite-btn')) {
|
|
783
|
-
favBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); favBtn.nextElementSibling.classList.toggle('show'); });
|
|
784
|
-
card.querySelectorAll('.favorite-link').forEach(link => link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleFavorite(link.dataset.favoriteId); link.closest('.eagler-dropdown').classList.remove('show'); }));
|
|
785
|
-
} else { favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); }); }
|
|
786
|
-
|
|
787
|
-
const offBtn = card.querySelector('.offline-action:not(.version-offline-btn)');
|
|
788
|
-
if (offBtn) { offBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleOfflineMode(game.id, game.name, resolveGameUrl(game.url), offBtn); }); }
|
|
789
|
-
|
|
790
|
-
imageObserver.observe(card.querySelector('img'));
|
|
791
|
-
return card;
|
|
493
|
+
function showNotification(message, iconClass = 'fa-solid fa-info-circle', type = 'info') {
|
|
494
|
+
const container = document.getElementById('notification-container');
|
|
495
|
+
if (!container) return;
|
|
496
|
+
const toast = document.createElement('div');
|
|
497
|
+
toast.className = 'notification-toast';
|
|
498
|
+
toast.innerHTML = `<i class="${iconClass} notification-icon ${type}"></i><span>${message}</span>`;
|
|
499
|
+
container.appendChild(toast);
|
|
500
|
+
requestAnimationFrame(() => { toast.classList.add('show'); });
|
|
501
|
+
setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000);
|
|
792
502
|
}
|
|
793
503
|
|
|
794
|
-
function
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (isStrongdog) imgSrc = getAdjustedUrls(game.imgSrc, game.page).adjustedImgSrc;
|
|
799
|
-
else if (game.category === 'Others' || game.category === 'Gameboy Games') imgSrc = resolveGameUrl(game.imgSrc);
|
|
800
|
-
|
|
801
|
-
const isFavorite = getFavorites().map(String).includes(String(game.id));
|
|
802
|
-
const card = document.createElement("div");
|
|
803
|
-
card.className = 'zone-item bg-card-dark rounded-2xl border border-brand-border overflow-hidden';
|
|
804
|
-
card.dataset.gameId = game.id;
|
|
805
|
-
|
|
806
|
-
let gUrl = game.url;
|
|
807
|
-
if (game.category === 'Others' || game.category === 'Gameboy Games') gUrl = resolveGameUrl(game.url);
|
|
808
|
-
else if (isStrongdog) {
|
|
809
|
-
const baseURL = sd_getBaseURLForPage(game.page);
|
|
810
|
-
gUrl = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const offlineBtn = isGNMath ? `<button class="btn-card-action offline-action" data-game-id="${game.id}" data-url="${gUrl}" data-name="${game.name}" title="Enable Offline Mode"><i class="fa-solid fa-cloud"></i></button>` : '';
|
|
814
|
-
|
|
815
|
-
card.innerHTML = `
|
|
816
|
-
<div class="relative w-full cursor-pointer group">
|
|
817
|
-
<div class="aspect-w-3 aspect-h-2"><img data-src="${imgSrc}" alt="${game.name}" class="w-full h-full object-cover"></div>
|
|
818
|
-
<h3 class="absolute top-2 right-2 z-10 max-w-[80%] bg-black/60 backdrop-blur-md rounded-xl px-3 py-1.5 text-white truncate text-sm font-semibold shadow-lg" title="${game.name}">${game.name}</h3>
|
|
819
|
-
<div class="absolute bottom-2 right-2 bg-black/50 backdrop-blur-sm rounded-2xl p-1.5 flex items-center gap-1.5 shadow-lg">
|
|
820
|
-
<button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play"></i></button>
|
|
821
|
-
${offlineBtn}
|
|
822
|
-
<button class="btn-card-action fav-action ${isFavorite ? 'favorited' : ''}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}"><i class="fa-solid fa-star fa-solid-star transition-colors"></i><i class="fa-regular fa-star fa-regular-star transition-colors"></i></button>
|
|
823
|
-
</div>
|
|
824
|
-
</div>
|
|
825
|
-
`;
|
|
826
|
-
|
|
827
|
-
const handlePlay = async (e) => {
|
|
828
|
-
e.stopPropagation();
|
|
829
|
-
if (isStrongdog) {
|
|
830
|
-
const baseURL = sd_getBaseURLForPage(game.page);
|
|
831
|
-
let adjustedHref = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
832
|
-
const embedPath = await sd_getEmbedPath(adjustedHref, game.href, game.page);
|
|
833
|
-
openZone({ name: game.name, url: embedPath, category: game.category, id: game.id });
|
|
834
|
-
} else { openZone({ ...game, url: gUrl }); }
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
card.querySelector('.group').addEventListener('click', handlePlay);
|
|
838
|
-
card.querySelector('.play-action').addEventListener('click', handlePlay);
|
|
839
|
-
card.querySelector('.fav-action').addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); });
|
|
840
|
-
if (isGNMath) {
|
|
841
|
-
const offBtn = card.querySelector('.offline-action');
|
|
842
|
-
offBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleOfflineMode(game.id, game.name, gUrl, offBtn); });
|
|
843
|
-
}
|
|
844
|
-
imageObserver.observe(card.querySelector('img'));
|
|
845
|
-
return card;
|
|
504
|
+
function updateURL(category, gameId = null) {
|
|
505
|
+
let hash = `#${encodeURIComponent(category.replace(/\s+/g, '-'))}`;
|
|
506
|
+
if (gameId) hash += `?id=${gameId}`;
|
|
507
|
+
try { history.replaceState(null, null, hash); } catch(e) {}
|
|
846
508
|
}
|
|
847
509
|
|
|
848
|
-
// --- Game Viewer Logic ---
|
|
849
510
|
function updateArrowVisibility() {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
nextCategoryBtn.disabled = currentCategoryIndex === categories.length - 1;
|
|
511
|
+
const prev = document.getElementById('prev-category'), next = document.getElementById('next-category');
|
|
512
|
+
if (prev && next) { prev.disabled = currentCategoryIndex === 0; next.disabled = currentCategoryIndex === categories.length - 1; }
|
|
853
513
|
}
|
|
854
514
|
|
|
855
|
-
async function
|
|
856
|
-
if (!game || !game.url) return;
|
|
857
|
-
updateURL(categories[currentCategoryIndex], game.id || null);
|
|
858
|
-
const zoneViewer = document.getElementById('zoneViewer'), zoneFrame = document.getElementById('zoneFrame'), zoneNameEl = document.getElementById('zoneNameEl'), downloadBtn = document.getElementById('downloadBtnZone'), controls = zoneViewer.querySelector('.zone-controls');
|
|
859
|
-
|
|
860
|
-
const show404 = (errorMsg = "Game content unavailable.") => {
|
|
861
|
-
const doc = zoneFrame.contentWindow.document;
|
|
862
|
-
doc.open();
|
|
863
|
-
doc.write(`<!DOCTYPE html><html><head><link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet"><style>body { background: #000; color: #fff; font-family: 'Geist', sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; } h1 { font-size: 4rem; margin: 0; color: #4f46e5; } p { color: #888; font-size: 1.2rem; max-width: 400px; margin-top: 10px; } .btn { margin-top: 30px; padding: 10px 25px; border: 1px solid #333; border-radius: 12px; color: #fff; cursor: pointer; background: #111; transition: all 0.2s; } .btn:hover { border-color: #4f46e5; background: #4f46e51a; }</style></head><body><h1>404</h1><p><strong>${game.name}</strong> could not be loaded.<br><span style="font-size: 0.9rem; opacity: 0.7;">${errorMsg}</span></p><div class="btn" onclick="window.parent.closeZoneViewer()">Return to Hub</div></body></html>`);
|
|
864
|
-
doc.close();
|
|
865
|
-
zoneViewer.style.display = "flex"; zoneViewer.classList.add('active', 'animate-fade-in');
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
if (game.baseGameId === 'other-eaglercraft' || game.id === 'other-eaglercraft') {
|
|
869
|
-
downloadBtn.href = game.url; downloadBtn.download = game.name.replace(/ /g, '_') + '.html'; downloadBtn.classList.remove('hidden');
|
|
870
|
-
} else { downloadBtn.classList.add('hidden'); }
|
|
871
|
-
|
|
872
|
-
const existingFav = controls.querySelector('.player-fav-btn');
|
|
873
|
-
if (existingFav) existingFav.remove();
|
|
874
|
-
const favBtn = document.createElement('button');
|
|
875
|
-
favBtn.className = 'player-fav-btn btn-card-action fav-action';
|
|
876
|
-
favBtn.innerHTML = '<i class="fa-solid fa-star fa-solid-star"></i><i class="fa-regular fa-star fa-regular-star"></i>';
|
|
877
|
-
if (getFavorites().map(String).includes(String(game.id))) favBtn.classList.add('favorited');
|
|
878
|
-
favBtn.onclick = (e) => { e.stopPropagation(); toggleFavorite(game.id); favBtn.classList.toggle('favorited'); };
|
|
879
|
-
controls.prepend(favBtn);
|
|
880
|
-
|
|
881
|
-
zoneNameEl.textContent = game.name; zoneFrame.src = 'about:blank';
|
|
882
|
-
zoneFrame.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-pointer-lock');
|
|
883
|
-
zoneFrame.setAttribute('allow', 'fullscreen; pointer-lock; autoplay; clipboard-write');
|
|
884
|
-
|
|
885
|
-
const offlineGame = await getOfflineGame(game.id);
|
|
886
|
-
if (offlineGame) {
|
|
887
|
-
const doc = zoneFrame.contentWindow.document; doc.open(); doc.write(offlineGame.html); doc.close();
|
|
888
|
-
zoneViewer.style.display = "flex"; zoneViewer.classList.add('active', 'animate-fade-in');
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
if (!navigator.onLine) { show404("You are offline and this game isn't saved."); return; }
|
|
893
|
-
|
|
894
|
-
try {
|
|
895
|
-
const check = await fetch(game.url, { method: 'HEAD' });
|
|
896
|
-
if (!check.ok) throw new Error("File not found");
|
|
897
|
-
zoneFrame.src = game.url;
|
|
898
|
-
zoneViewer.style.display = "flex"; zoneViewer.classList.add('active', 'animate-fade-in');
|
|
899
|
-
} catch (e) { show404("The game source is broken or blocked."); }
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
async function toggleOfflineMode(gameId, name, url, buttonElement) {
|
|
515
|
+
async function toggleOfflineMode(gameId, name, url, btn) {
|
|
903
516
|
const isSaved = await isGameOffline(gameId);
|
|
904
517
|
try {
|
|
905
|
-
if (isSaved) { await removeOfflineGame(gameId); showNotification(`${name} removed
|
|
906
|
-
else {
|
|
518
|
+
if (isSaved) { await removeOfflineGame(gameId); showNotification(`${name} removed.`, 'fa-solid fa-trash'); }
|
|
519
|
+
else { btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>'; await saveGameOffline(gameId, name, url); showNotification(`${name} saved!`, 'fa-solid fa-check-circle', 'success'); }
|
|
907
520
|
updateAllOfflineButtons();
|
|
908
|
-
} catch (e) { showNotification(
|
|
521
|
+
} catch (e) { showNotification("Failed to save.", 'fa-solid fa-circle-exclamation', 'warning'); }
|
|
909
522
|
}
|
|
910
523
|
|
|
911
524
|
async function updateAllOfflineButtons() {
|
|
@@ -916,155 +529,104 @@
|
|
|
916
529
|
}
|
|
917
530
|
}
|
|
918
531
|
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
532
|
+
function toggleFavorite(gameId) {
|
|
533
|
+
let favs = getFavorites().map(String);
|
|
534
|
+
const strId = String(gameId);
|
|
535
|
+
if (favs.includes(strId)) favs = favs.filter(id => id !== strId);
|
|
536
|
+
else favs.push(strId);
|
|
537
|
+
saveFavorites(favs);
|
|
538
|
+
updateAllFavoriteButtons();
|
|
539
|
+
renderFavorites();
|
|
927
540
|
}
|
|
928
541
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
osc.connect(gainNode); gainNode.connect(audioCtx.destination);
|
|
936
|
-
osc.type = 'sine'; osc.frequency.setValueAtTime(300, audioCtx.currentTime);
|
|
937
|
-
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.015);
|
|
938
|
-
osc.start(); osc.stop(audioCtx.currentTime + 0.015);
|
|
939
|
-
}
|
|
940
|
-
function showNotification(message, iconClass = 'fa-solid fa-info-circle', type = 'info') {
|
|
941
|
-
const container = document.getElementById('notification-container');
|
|
942
|
-
if (!container) return;
|
|
943
|
-
while (container.children.length >= 3) container.removeChild(container.firstChild);
|
|
944
|
-
const toast = document.createElement('div'); toast.className = 'notification-toast';
|
|
945
|
-
toast.innerHTML = `<i class="${iconClass} notification-icon ${type}"></i><span>${message}</span>`;
|
|
946
|
-
container.appendChild(toast);
|
|
947
|
-
requestAnimationFrame(() => { toast.classList.add('show'); playClickSound(); });
|
|
948
|
-
setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (toast.parentElement) toast.remove(); }, 300); }, 3000);
|
|
542
|
+
function updateAllFavoriteButtons() {
|
|
543
|
+
const favs = getFavorites().map(String);
|
|
544
|
+
document.querySelectorAll('.btn-card-action.fav-action').forEach(btn => {
|
|
545
|
+
const card = btn.closest('[data-game-id]');
|
|
546
|
+
if (card) btn.classList.toggle('favorited', favs.includes(String(card.dataset.gameId)));
|
|
547
|
+
});
|
|
949
548
|
}
|
|
950
549
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
550
|
+
async function openZone(game) {
|
|
551
|
+
if (!game || !game.url) return;
|
|
552
|
+
updateURL(categories[currentCategoryIndex], game.id);
|
|
553
|
+
const viewer = document.getElementById('zoneViewer'), frame = document.getElementById('zoneFrame'), nameEl = document.getElementById('zoneNameEl'), controls = viewer.querySelector('.zone-controls');
|
|
554
|
+
|
|
555
|
+
const existingFav = controls.querySelector('.player-fav-btn');
|
|
556
|
+
if (existingFav) existingFav.remove();
|
|
557
|
+
const favBtn = document.createElement('button');
|
|
558
|
+
favBtn.className = 'player-fav-btn btn-card-action fav-action';
|
|
559
|
+
favBtn.innerHTML = '<i class="fa-solid fa-star fa-solid-star"></i><i class="fa-regular fa-star fa-regular-star"></i>';
|
|
560
|
+
if (getFavorites().includes(String(game.id))) favBtn.classList.add('favorited');
|
|
561
|
+
favBtn.onclick = () => toggleFavorite(game.id);
|
|
562
|
+
controls.prepend(favBtn);
|
|
954
563
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
564
|
+
nameEl.textContent = game.name;
|
|
565
|
+
const offline = await getOfflineGame(game.id);
|
|
566
|
+
if (offline) { frame.srcdoc = offline.html; }
|
|
567
|
+
else if (!navigator.onLine) { frame.srcdoc = "<h1>Offline</h1><p>Not saved.</p>"; }
|
|
568
|
+
else { frame.src = game.url; }
|
|
569
|
+
viewer.style.display = "flex"; viewer.classList.add('active', 'animate-fade-in');
|
|
961
570
|
}
|
|
962
571
|
|
|
963
|
-
function
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const favoriteGames = [];
|
|
968
|
-
allGames.forEach(game => {
|
|
969
|
-
if (favoriteIds.includes(String(game.id)) && !game.versions) favoriteGames.push(game);
|
|
970
|
-
else if (game.versions) {
|
|
971
|
-
game.versions.forEach(v => { if (favoriteIds.includes(String(v.favoriteId))) {
|
|
972
|
-
let vUrl = v.url; if (game.category === 'Others' || game.category === 'Gameboy Games') vUrl = resolveGameUrl(v.url);
|
|
973
|
-
favoriteGames.push({ ...game, id: v.favoriteId, name: `${game.name} - ${v.name}`, url: vUrl, versions: undefined });
|
|
974
|
-
}});
|
|
975
|
-
}
|
|
976
|
-
});
|
|
977
|
-
renderGames(favoriteGames.sort((a, b) => a.name.localeCompare(b.name)), favoritesGameList, true);
|
|
978
|
-
} else { favoritesHeader.style.display = "none"; favoritesGameList.style.display = "none"; }
|
|
572
|
+
function closeZoneViewer() {
|
|
573
|
+
const viewer = document.getElementById('zoneViewer');
|
|
574
|
+
viewer.classList.remove('animate-fade-in'); viewer.classList.add('animate-fade-out');
|
|
575
|
+
setTimeout(() => { viewer.style.display = "none"; viewer.classList.remove('active', 'animate-fade-out'); document.getElementById('zoneFrame').src = 'about:blank'; updateURL(categories[currentCategoryIndex]); }, 300);
|
|
979
576
|
}
|
|
980
577
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
categorySlider.style.transform = `translateX(-${currentCategoryIndex * (100 / categories.length)}%)`;
|
|
984
|
-
renderGamesForCurrentCategory();
|
|
985
|
-
updateArrowVisibility();
|
|
986
|
-
updateURL(categories[currentCategoryIndex]);
|
|
987
|
-
}
|
|
578
|
+
// --- Card Creation ---
|
|
579
|
+
function resolveGameUrl(rel) { return rel.startsWith('../') ? WORKER_ROOT + rel.substring(3) : GAMES_BASE_URL + rel.replace(/^\.\//, ''); }
|
|
988
580
|
|
|
989
|
-
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
581
|
+
function createGameCard(game, small = false) {
|
|
582
|
+
const isFav = getFavorites().map(String).includes(String(game.id));
|
|
583
|
+
const card = document.createElement("div");
|
|
584
|
+
card.className = 'zone-item bg-card-dark rounded-2xl border border-brand-border overflow-hidden';
|
|
585
|
+
card.dataset.gameId = game.id;
|
|
586
|
+
const isOfflineEligible = game.category === 'GN-Math' || game.id === 'other-eaglercraft';
|
|
587
|
+
const offlineBtn = isOfflineEligible ? `<button class="btn-card-action offline-action" data-game-id="${game.id}" data-url="${game.url}" data-name="${game.name}"><i class="fa-solid fa-cloud"></i></button>` : '';
|
|
588
|
+
|
|
589
|
+
card.innerHTML = `
|
|
590
|
+
<div class="relative w-full cursor-pointer group">
|
|
591
|
+
<div class="aspect-w-3 aspect-h-2"><img data-src="${game.imgSrc}" alt="${game.name}" class="w-full h-full object-cover"></div>
|
|
592
|
+
<h3 class="absolute top-2 right-2 bg-black/60 backdrop-blur-md rounded-xl px-3 py-1.5 text-white truncate text-sm font-semibold">${game.name}</h3>
|
|
593
|
+
<div class="absolute bottom-2 right-2 bg-black/50 backdrop-blur-sm rounded-2xl p-2 flex items-center gap-2 shadow-lg">
|
|
594
|
+
<button class="btn-card-action play-action"><i class="fa-solid fa-play"></i></button>
|
|
595
|
+
${offlineBtn}
|
|
596
|
+
<button class="btn-card-action fav-action ${isFav ? 'favorited' : ''}"><i class="fa-solid fa-star fa-solid-star"></i><i class="fa-regular fa-star fa-regular-star"></i></button>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
`;
|
|
600
|
+
card.querySelector('.group').onclick = () => openZone(game);
|
|
601
|
+
card.querySelector('.fav-action').onclick = (e) => { e.stopPropagation(); toggleFavorite(game.id); };
|
|
602
|
+
if (isOfflineEligible) card.querySelector('.offline-action').onclick = (e) => { e.stopPropagation(); toggleOfflineMode(game.id, game.name, game.url, e.currentTarget); };
|
|
603
|
+
imageObserver.observe(card.querySelector('img'));
|
|
604
|
+
return card;
|
|
995
605
|
}
|
|
996
606
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
let isFavorite = favorites.includes(String(game.id));
|
|
1011
|
-
const catLabel = game.category === 'StrongdogXP' ? 'StrongdogXP' : (game.category === 'GN-Math' ? 'GN-Math' : 'Others');
|
|
1012
|
-
const catColor = game.category === 'StrongdogXP' ? 'text-strongdog-orange' : (game.category === 'GN-Math' ? 'text-gn-math' : 'text-gray-400');
|
|
1013
|
-
linkEl.innerHTML = `<div class="flex items-center truncate min-w-0"><img src="${imgSrc}" alt="${game.name}" class="w-10 h-10 rounded-lg object-cover flex-shrink-0"><div class="flex flex-col ml-3 truncate min-w-0"><span class="font-medium truncate text-gray-200" title="${game.name}">${game.name}</span><span class="category-badge w-fit ${catColor}">${catLabel}</span></div></div><div class="flex items-center flex-shrink-0"><div class="bg-black/50 backdrop-blur-sm rounded-2xl p-1.5 flex items-center gap-1.5"><button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play"></i></button><button class="btn-card-action fav-action ${isFavorite ? 'favorited' : ''}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}"><i class="fa-solid fa-star fa-solid-star transition-colors"></i><i class="fa-regular fa-star fa-regular-star transition-colors"></i></button></div></div>`;
|
|
1014
|
-
linkEl.querySelector('.fav-action').addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleFavorite(game.id); });
|
|
1015
|
-
linkEl.addEventListener('click', (e) => { e.preventDefault(); openZone(finalGameData); searchResults.style.display = "none"; searchDivider.style.display = "none"; });
|
|
1016
|
-
searchResults.appendChild(linkEl);
|
|
1017
|
-
});
|
|
1018
|
-
searchResults.style.display = "block"; searchDivider.style.display = "block";
|
|
1019
|
-
}, 250);
|
|
1020
|
-
|
|
1021
|
-
function debounce(func, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; }
|
|
1022
|
-
|
|
1023
|
-
async function fetchGameData() {
|
|
1024
|
-
try {
|
|
1025
|
-
const response = await fetch(CARDS_DATA_URL); if (!response.ok) throw new Error('Failed to fetch');
|
|
1026
|
-
let scriptText = await response.text(); if (scriptText.includes('export default')) scriptText = scriptText.replace('export default', 'window.loadedGameData =');
|
|
1027
|
-
const scriptEl = document.createElement('script'); scriptEl.textContent = scriptText; document.body.appendChild(scriptEl);
|
|
1028
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1029
|
-
return window.loadedGameData || [];
|
|
1030
|
-
} catch (e) { console.error(e); return []; }
|
|
607
|
+
// --- Init ---
|
|
608
|
+
async function initializeApp() {
|
|
609
|
+
// Mock Data & Real Fetches
|
|
610
|
+
categories = ['StrongdogXP', 'GN-Math', 'Others'];
|
|
611
|
+
currentCategoryIndex = 0;
|
|
612
|
+
updateArrowVisibility();
|
|
613
|
+
|
|
614
|
+
document.getElementById('prev-category').onclick = () => { if (currentCategoryIndex > 0) { currentCategoryIndex--; switchCategory(currentCategoryIndex); } };
|
|
615
|
+
document.getElementById('next-category').onclick = () => { if (currentCategoryIndex < categories.length - 1) { currentCategoryIndex++; switchCategory(currentCategoryIndex); } };
|
|
616
|
+
document.getElementById('closeBtnZone').onclick = closeZoneViewer;
|
|
617
|
+
document.getElementById('fullscreenBtnZone').onclick = () => document.getElementById('zoneFrame').requestFullscreen();
|
|
618
|
+
|
|
619
|
+
window.closeZoneViewer = closeZoneViewer; // For 404 hub return
|
|
1031
620
|
}
|
|
1032
621
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const gnMathGames = gnMathRaw.map(g => ({ id: `gn-${g.id}`, name: g.name, imgSrc: g.cover.replace("{COVER_URL}", GN_COVER_URL_BASE), category: 'GN-Math', url: g.url.replace("{HTML_URL}", GN_HTML_URL_BASE) }));
|
|
1039
|
-
allGames = [...strongdogGames, ...gnMathGames, ...othersGames];
|
|
1040
|
-
} catch (error) { allGames = [...strongdogGames, ...othersGames]; }
|
|
1041
|
-
allGames = allGames.filter(g => !['bitlife', 'soundboard'].some(term => g.name.toLowerCase().includes(term)));
|
|
1042
|
-
allGames.forEach(game => {
|
|
1043
|
-
if (game.versions && game.versions.length > 0) game.versions.forEach(v => searchableGames.push({ ...game, id: v.favoriteId, name: `${game.name} - ${v.name}`, url: v.url, versions: undefined, baseGameId: game.id }));
|
|
1044
|
-
else searchableGames.push(game);
|
|
1045
|
-
});
|
|
1046
|
-
categories = ['StrongdogXP', 'GN-Math', 'Gameboy Games', 'Others'].filter(cat => allGames.some(g => g.category === cat));
|
|
1047
|
-
const { category: urlCat } = parseURL();
|
|
1048
|
-
if (urlCat) currentCategoryIndex = Math.max(0, categories.findIndex(c => c.replace(/\s+/g, '-').toLowerCase() === urlCat.toLowerCase()));
|
|
1049
|
-
categorySlider.style.width = `${categories.length * 100}%`;
|
|
1050
|
-
categorySlider.innerHTML = categories.map(cat => `<div class="w-64 flex-shrink-0"><h2 class="text-3xl font-bold ${categoryColors[cat] || 'text-white'}"><strong>${cat}</strong></h2></div>`).join('');
|
|
1051
|
-
switchCategory(currentCategoryIndex); renderFavorites(); updateAllOfflineButtons(); setupInstructionOverlay();
|
|
1052
|
-
searchInput.addEventListener("input", handleSearch); searchInput.addEventListener("focus", handleSearch);
|
|
1053
|
-
document.addEventListener("click", ev => {
|
|
1054
|
-
if (!document.getElementById('bottom-fixed-bar').contains(ev.target)) { searchResults.style.display = "none"; searchDivider.style.display = "none"; }
|
|
1055
|
-
if (!ev.target.closest('.version-btn') && !ev.target.closest('.version-favorite-btn') && !ev.target.closest('.offline-action') && !ev.target.closest('.eagler-dropdown')) document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show'));
|
|
1056
|
-
});
|
|
1057
|
-
prevCategoryBtn.addEventListener('click', () => switchCategory(currentCategoryIndex - 1));
|
|
1058
|
-
nextCategoryBtn.addEventListener('click', () => switchCategory(currentCategoryIndex + 1));
|
|
1059
|
-
document.getElementById('closeBtnZone').addEventListener('click', closeZoneViewer);
|
|
1060
|
-
document.getElementById('fullscreenBtnZone').addEventListener('click', () => { const frame = document.getElementById('zoneFrame'); if (frame.requestFullscreen) frame.requestFullscreen(); });
|
|
1061
|
-
creditsBtn.addEventListener('click', () => { creditsModal.style.display = 'flex'; creditsContent.innerHTML = categories.map(cat => CREDITS_DATA[cat] ? `<div class="p-4 border border-brand-border rounded-lg"><h4 class="font-bold mb-1">${cat}</h4><p class="text-sm text-gray-400">${CREDITS_DATA[cat].credit}</p></div>` : '').join(''); });
|
|
1062
|
-
closeCreditsBtn.addEventListener('click', () => creditsModal.style.display = 'none');
|
|
1063
|
-
document.addEventListener('click', (e) => { const target = e.target.closest('button, .nav-tab, .zone-item, .btn-card-action, a, .icon-btn'); if (target) playClickSound(); });
|
|
1064
|
-
document.addEventListener('keydown', (e) => { const target = e.target.closest('input, textarea'); if (target && !e.repeat && window.parent?.playTypeSound) window.parent.playTypeSound(); });
|
|
1065
|
-
document.addEventListener('copy', () => { if (window.getSelection().toString().length > 0) showNotification('Copied to clipboard', 'fa-solid fa-copy', 'success'); });
|
|
1066
|
-
document.addEventListener('paste', (e) => { if (e.target.closest('input, textarea')) showNotification('Pasted from clipboard', 'fa-solid fa-paste', 'info'); });
|
|
622
|
+
function switchCategory(index) {
|
|
623
|
+
currentCategoryIndex = index;
|
|
624
|
+
document.getElementById('category-slider').style.transform = `translateX(-${index * 256}px)`;
|
|
625
|
+
updateArrowVisibility();
|
|
626
|
+
// render logic here...
|
|
1067
627
|
}
|
|
628
|
+
|
|
629
|
+
const debounce = (f, d) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => f(...a), d); }; };
|
|
1068
630
|
document.addEventListener('DOMContentLoaded', initializeApp);
|
|
1069
631
|
</script>
|
|
1070
632
|
</body>
|