4sp-dv 1.0.37 → 1.0.38
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 +4 -58
- package/logged-in/games.html +427 -252
- 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.38/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,33 +468,9 @@
|
|
|
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; }
|
|
491
471
|
</style>
|
|
492
472
|
</head>
|
|
493
473
|
<body>
|
|
494
|
-
<div id="offline-indicator">
|
|
495
|
-
<i class="fa-solid fa-plane"></i>
|
|
496
|
-
<span>Offline Mode</span>
|
|
497
|
-
</div>
|
|
498
474
|
<div id="lock-screen" class="w-full h-full flex flex-col items-center justify-center p-4">
|
|
499
475
|
<div class="max-w-5xl w-full bg-[#040404] flex flex-col md:flex-row border border-[#252525] rounded-3xl overflow-hidden shadow-2xl">
|
|
500
476
|
<div class="flex-1 p-8 flex flex-col justify-center items-center border-b md:border-b-0 md:border-r border-[#252525]">
|
|
@@ -802,7 +778,7 @@
|
|
|
802
778
|
const displayUsername = document.getElementById('display-username');
|
|
803
779
|
const displayEmail = document.getElementById('display-email');
|
|
804
780
|
|
|
805
|
-
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.
|
|
781
|
+
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.38/logged-in/';
|
|
806
782
|
|
|
807
783
|
// Preload Logos
|
|
808
784
|
const preloadImgs = [
|
|
@@ -1213,8 +1189,7 @@
|
|
|
1213
1189
|
pfpLetterBg: data.pfpLetterBg,
|
|
1214
1190
|
letterAvatarText: data.letterAvatarText,
|
|
1215
1191
|
letterAvatarTextColor: data.letterAvatarTextColor, // New Field
|
|
1216
|
-
navbarTheme: data.navbarTheme
|
|
1217
|
-
verified_online: true // Mark as verified for offline use
|
|
1192
|
+
navbarTheme: data.navbarTheme // Sync theme if updated remotely
|
|
1218
1193
|
};
|
|
1219
1194
|
|
|
1220
1195
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(newUserData));
|
|
@@ -1305,25 +1280,6 @@
|
|
|
1305
1280
|
unlockBtn.classList.add('hidden');
|
|
1306
1281
|
codeInput.classList.add('hidden');
|
|
1307
1282
|
|
|
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
|
-
document.getElementById('offline-indicator').classList.add('show');
|
|
1319
|
-
launchApp();
|
|
1320
|
-
return;
|
|
1321
|
-
} else {
|
|
1322
|
-
resetLockScreen("You are offline. Please connect to the internet to verify your code.");
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
1283
|
try {
|
|
1328
1284
|
const docRef = doc(db, "access_codes", savedCode);
|
|
1329
1285
|
const docSnap = await getDoc(docRef);
|
|
@@ -1355,17 +1311,7 @@
|
|
|
1355
1311
|
}
|
|
1356
1312
|
} catch(e) {
|
|
1357
1313
|
console.error("Auto-login error:", e);
|
|
1358
|
-
|
|
1359
|
-
let cachedUser = null;
|
|
1360
|
-
try { cachedUser = JSON.parse(localStorage.getItem(USER_DATA_KEY)); } catch(err){}
|
|
1361
|
-
if (cachedUser && cachedUser.verified_online) {
|
|
1362
|
-
currentUser = cachedUser;
|
|
1363
|
-
updateUIWithUser(cachedUser);
|
|
1364
|
-
document.getElementById('offline-indicator').classList.add('show');
|
|
1365
|
-
launchApp();
|
|
1366
|
-
} else {
|
|
1367
|
-
resetLockScreen("Connection failed. Retrying...");
|
|
1368
|
-
}
|
|
1314
|
+
resetLockScreen("Connection failed. Retrying...");
|
|
1369
1315
|
}
|
|
1370
1316
|
}
|
|
1371
1317
|
}
|
package/logged-in/games.html
CHANGED
|
@@ -331,16 +331,6 @@
|
|
|
331
331
|
.custom-scrollbar::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); border-radius: 4px; }
|
|
332
332
|
.custom-scrollbar::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
|
333
333
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #555; }
|
|
334
|
-
|
|
335
|
-
/* Notifications */
|
|
336
|
-
#notification-container { position: fixed; bottom: 2rem; right: 2rem; display: flex; flex-direction: column; gap: 0.75rem; z-index: 9999; pointer-events: none; }
|
|
337
|
-
.notification-toast { background-color: #0a0a0a; border: 1px solid #333; border-radius: 14px; padding: 0.75rem 1.25rem; color: #fff; box-shadow: 0 4px 15px rgba(0,0,0,0.5); display: flex; align-items: center; gap: 0.75rem; font-size: 0.9rem; min-width: 200px; transform: translateX(120%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease, background-color 0.2s; opacity: 0; pointer-events: auto; cursor: default; }
|
|
338
|
-
.notification-toast.show { transform: translateX(0); opacity: 1; }
|
|
339
|
-
.notification-toast.show:hover { transform: scale(1.05) translateX(-5px); background-color: #151515; border-color: #555; box-shadow: 0 8px 25px rgba(0,0,0,0.7); }
|
|
340
|
-
.notification-icon { font-size: 1.1rem; }
|
|
341
|
-
.notification-icon.success { color: #4ade80; }
|
|
342
|
-
.notification-icon.info { color: #60a5fa; }
|
|
343
|
-
.notification-icon.warning { color: #fbbf24; }
|
|
344
334
|
</style>
|
|
345
335
|
</head>
|
|
346
336
|
|
|
@@ -460,16 +450,19 @@
|
|
|
460
450
|
</div>
|
|
461
451
|
</div>
|
|
462
452
|
|
|
463
|
-
<div id="notification-container"></div>
|
|
464
|
-
|
|
465
453
|
<script>
|
|
466
454
|
// --- Configuration & Globals ---
|
|
455
|
+
|
|
467
456
|
const GN_ZONES_URL = "https://cdn.jsdelivr.net/gh/gn-math/assets@main/zones.json";
|
|
468
457
|
const GN_COVER_URL_BASE = "https://cdn.jsdelivr.net/gh/gn-math/covers@main";
|
|
458
|
+
|
|
459
|
+
// FIXED: Use raw.githack.com for HTML to prevent "showing code" issue
|
|
469
460
|
const GN_HTML_URL_BASE = "https://raw.githack.com/gn-math/html/main";
|
|
461
|
+
|
|
470
462
|
const WORKER_ROOT = "https://dv-service-lfs.4simpleproblems.workers.dev/";
|
|
471
463
|
const GAMES_BASE_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/";
|
|
472
464
|
const CARDS_DATA_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/cards-data.js";
|
|
465
|
+
|
|
473
466
|
const FAVORITES_KEY = 'gameHubFavorites_v3';
|
|
474
467
|
|
|
475
468
|
let allGames = [];
|
|
@@ -499,26 +492,37 @@
|
|
|
499
492
|
const prevCategoryBtn = document.getElementById('prev-category');
|
|
500
493
|
const nextCategoryBtn = document.getElementById('next-category');
|
|
501
494
|
const gamesGridContainer = document.getElementById('games-grid-container');
|
|
495
|
+
|
|
502
496
|
const creditsModal = document.getElementById('creditsModal');
|
|
503
497
|
const creditsContent = document.getElementById('creditsContent');
|
|
504
498
|
const creditsBtn = document.getElementById('creditsBtn');
|
|
505
499
|
const closeCreditsBtn = document.getElementById('closeCreditsBtn');
|
|
500
|
+
|
|
506
501
|
const INSTRUCTION_KEY = '4sp-games-instruction-extended-seen';
|
|
507
502
|
const instructionOverlay = document.getElementById('instruction-overlay');
|
|
508
503
|
const instructionInput = document.getElementById('instruction-input');
|
|
509
504
|
|
|
510
|
-
// --- Intersection Observer ---
|
|
505
|
+
// --- Intersection Observer for Image Virtualization ---
|
|
511
506
|
const imageObserver = new IntersectionObserver((entries, observer) => {
|
|
512
507
|
entries.forEach(entry => {
|
|
513
508
|
const img = entry.target;
|
|
514
509
|
if (entry.isIntersecting) {
|
|
515
|
-
if (img.dataset.src)
|
|
510
|
+
if (img.dataset.src) {
|
|
511
|
+
img.src = img.dataset.src;
|
|
512
|
+
}
|
|
516
513
|
} else {
|
|
517
|
-
if(img.src && !img.src.includes('placehold.co') && !img.dataset.src)
|
|
518
|
-
|
|
514
|
+
if(img.src && !img.src.includes('placehold.co') && !img.dataset.src) {
|
|
515
|
+
img.dataset.src = img.src;
|
|
516
|
+
}
|
|
517
|
+
if(img.dataset.src) {
|
|
518
|
+
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
519
|
+
}
|
|
519
520
|
}
|
|
520
521
|
});
|
|
521
|
-
}, {
|
|
522
|
+
}, {
|
|
523
|
+
rootMargin: "300px 0px",
|
|
524
|
+
threshold: 0.01
|
|
525
|
+
});
|
|
522
526
|
|
|
523
527
|
function observeImages(container) {
|
|
524
528
|
const images = container.querySelectorAll('img[data-src]');
|
|
@@ -529,7 +533,11 @@
|
|
|
529
533
|
function updateURL(category, gameId = null) {
|
|
530
534
|
let hash = `#${encodeURIComponent(category.replace(/\s+/g, '-'))}`;
|
|
531
535
|
if (gameId) hash += `?id=${gameId}`;
|
|
532
|
-
try {
|
|
536
|
+
try {
|
|
537
|
+
history.replaceState(null, null, hash);
|
|
538
|
+
} catch(e) {
|
|
539
|
+
console.warn('History API not supported in this environment');
|
|
540
|
+
}
|
|
533
541
|
}
|
|
534
542
|
|
|
535
543
|
function parseURL() {
|
|
@@ -537,6 +545,7 @@
|
|
|
537
545
|
if (!hash) return { category: null, gameId: null };
|
|
538
546
|
const parts = hash.split('?');
|
|
539
547
|
let rawCategory = parts[0];
|
|
548
|
+
let category = decodeURIComponent(rawCategory).replace(/-/g, ' ');
|
|
540
549
|
let gameId = null;
|
|
541
550
|
if (parts[1]) {
|
|
542
551
|
const params = new URLSearchParams(parts[1]);
|
|
@@ -567,7 +576,7 @@
|
|
|
567
576
|
}
|
|
568
577
|
}
|
|
569
578
|
|
|
570
|
-
// --- Favorites ---
|
|
579
|
+
// --- Favorites Management (Live Updates) ---
|
|
571
580
|
const getFavorites = () => JSON.parse(localStorage.getItem(FAVORITES_KEY)) || [];
|
|
572
581
|
const saveFavorites = (favs) => localStorage.setItem(FAVORITES_KEY, JSON.stringify(favs));
|
|
573
582
|
|
|
@@ -580,9 +589,9 @@
|
|
|
580
589
|
favorites = favorites.filter(id => id !== strGameId);
|
|
581
590
|
saveFavorites(favorites);
|
|
582
591
|
updateAllFavoriteButtons();
|
|
583
|
-
const favCard = favoritesGameList.querySelector(
|
|
592
|
+
const favCard = favoritesGameList.querySelector(`.zone-item[data-game-id='${strGameId}'], .other-zone-item[data-game-id='${strGameId}']`);
|
|
584
593
|
if (favCard) {
|
|
585
|
-
favCard.classList.add('
|
|
594
|
+
favCard.classList.add('fade-out');
|
|
586
595
|
setTimeout(() => {
|
|
587
596
|
favCard.remove();
|
|
588
597
|
if (favoritesGameList.children.length === 0) {
|
|
@@ -595,11 +604,25 @@
|
|
|
595
604
|
favorites.push(strGameId);
|
|
596
605
|
saveFavorites(favorites);
|
|
597
606
|
updateAllFavoriteButtons();
|
|
598
|
-
let gameData =
|
|
607
|
+
let gameData = allGames.find(g => String(g.id) === strGameId);
|
|
608
|
+
if (!gameData) {
|
|
609
|
+
allGames.some(g => {
|
|
610
|
+
if (g.versions) {
|
|
611
|
+
const ver = g.versions.find(v => String(v.favoriteId) === strGameId);
|
|
612
|
+
if (ver) {
|
|
613
|
+
gameData = { ...g, id: ver.favoriteId, name: `${g.name} - ${ver.name}`, url: ver.url, versions: undefined };
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return false;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
599
620
|
if (gameData) {
|
|
600
621
|
favoritesHeader.style.display = "block";
|
|
601
622
|
favoritesGameList.style.display = "grid";
|
|
602
|
-
favoritesGameList.
|
|
623
|
+
favoritesGameList.classList.add('grid', 'grid-cols-2', 'sm:grid-cols-3', 'md:grid-cols-5', 'lg:grid-cols-7', 'gap-4');
|
|
624
|
+
const newCard = createGameCard(gameData, true);
|
|
625
|
+
favoritesGameList.appendChild(newCard);
|
|
603
626
|
}
|
|
604
627
|
}
|
|
605
628
|
}
|
|
@@ -607,84 +630,37 @@
|
|
|
607
630
|
function updateAllFavoriteButtons() {
|
|
608
631
|
const favorites = getFavorites().map(String);
|
|
609
632
|
document.querySelectorAll('.btn-card-action.fav-action').forEach(btn => {
|
|
610
|
-
const card = btn.closest('
|
|
633
|
+
const card = btn.closest('.zone-item, .other-zone-item, .search-item');
|
|
611
634
|
if (!card) return;
|
|
612
635
|
const cardGameId = String(card.dataset.gameId);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const DB_VERSION = 1;
|
|
622
|
-
const STORE_NAME = 'games';
|
|
623
|
-
|
|
624
|
-
function openDB() {
|
|
625
|
-
return new Promise((resolve, reject) => {
|
|
626
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
627
|
-
request.onupgradeneeded = (e) => {
|
|
628
|
-
const db = e.target.result;
|
|
629
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
|
630
|
-
};
|
|
631
|
-
request.onsuccess = (e) => resolve(e.target.result);
|
|
632
|
-
request.onerror = (e) => reject(e.target.error);
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
async function saveGameOffline(gameId, name, url) {
|
|
637
|
-
try {
|
|
638
|
-
const res = await fetch(url);
|
|
639
|
-
if (!res.ok) throw new Error('Fetch failed');
|
|
640
|
-
const html = await res.text();
|
|
641
|
-
const db = await openDB();
|
|
642
|
-
return new Promise((resolve, reject) => {
|
|
643
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
644
|
-
const store = tx.objectStore(STORE_NAME);
|
|
645
|
-
store.put({ id: String(gameId), name, html, timestamp: Date.now() });
|
|
646
|
-
tx.oncomplete = () => { db.close(); resolve(); };
|
|
647
|
-
tx.onerror = () => reject(tx.error);
|
|
648
|
-
});
|
|
649
|
-
} catch (e) { console.error("Offline Save Error:", e); throw e; }
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
async function getOfflineGame(gameId) {
|
|
653
|
-
const db = await openDB();
|
|
654
|
-
return new Promise((resolve, reject) => {
|
|
655
|
-
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
656
|
-
const store = tx.objectStore(STORE_NAME);
|
|
657
|
-
const request = store.get(String(gameId));
|
|
658
|
-
request.onsuccess = () => { db.close(); resolve(request.result); };
|
|
659
|
-
request.onerror = () => reject(request.error);
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async function removeOfflineGame(gameId) {
|
|
664
|
-
const db = await openDB();
|
|
665
|
-
return new Promise((resolve, reject) => {
|
|
666
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
667
|
-
const store = tx.objectStore(STORE_NAME);
|
|
668
|
-
store.delete(String(gameId));
|
|
669
|
-
tx.oncomplete = () => { db.close(); resolve(); };
|
|
670
|
-
tx.onerror = () => reject(tx.error);
|
|
636
|
+
if (btn.classList.contains('version-favorite-btn')) {
|
|
637
|
+
const isAnyVersionFavorited = favorites.some(favId => favId.startsWith(cardGameId + '_'));
|
|
638
|
+
btn.classList.toggle('favorited', isAnyVersionFavorited);
|
|
639
|
+
} else {
|
|
640
|
+
const isFavorited = favorites.includes(cardGameId);
|
|
641
|
+
btn.classList.toggle('favorited', isFavorited);
|
|
642
|
+
btn.title = isFavorited ? 'Remove from Favorites' : 'Add to Favorites';
|
|
643
|
+
}
|
|
671
644
|
});
|
|
672
645
|
}
|
|
673
646
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return !!game;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// --- URL Helpers ---
|
|
647
|
+
// --- URL Helpers (Strongdog) ---
|
|
648
|
+
// Modified to support injected URLs using WORKER_ROOT + directory logic
|
|
680
649
|
function getAdjustedUrls(imgSrc, page) {
|
|
681
|
-
if (page && page > 1)
|
|
650
|
+
if (page && page > 1) {
|
|
651
|
+
// ../strongdog2/img/... -> exists outside GAMES dir
|
|
652
|
+
return { adjustedImgSrc: `${WORKER_ROOT}strongdog${page}/img/${imgSrc}` };
|
|
653
|
+
}
|
|
654
|
+
// ./img -> assumed inside GAMES dir or wherever relative
|
|
682
655
|
return { adjustedImgSrc: `${GAMES_BASE_URL}img/${imgSrc}` };
|
|
683
656
|
}
|
|
684
657
|
|
|
685
658
|
function sd_getBaseURLForPage(page) {
|
|
659
|
+
// Updated to point to WORKER_ROOT for numbered pages, GAMES_BASE_URL for default
|
|
686
660
|
if (page > 1) return `${WORKER_ROOT}strongdog${page}/`;
|
|
687
|
-
return `${WORKER_ROOT}STRONGDOG/`;
|
|
661
|
+
return `${WORKER_ROOT}STRONGDOG/`; // Assumes default strongdog is at root/STRONGDOG? Or inside GAMES?
|
|
662
|
+
// Re-reading logic: "stuff like ../DOOM/ will stay in GAMES directory but strongdog# directories exit"
|
|
663
|
+
// Let's assume default STRONGDOG is adjacent to strongdog2 etc. at root level based on prior naming conventions
|
|
688
664
|
}
|
|
689
665
|
|
|
690
666
|
async function sd_getEmbedPath(adjustedHref, originalHref, page) {
|
|
@@ -702,52 +678,73 @@
|
|
|
702
678
|
}
|
|
703
679
|
}
|
|
704
680
|
} catch (error) {}
|
|
705
|
-
for (const path of pathsToTry) {
|
|
681
|
+
for (const path of pathsToTry) {
|
|
682
|
+
if (await sd_fileExists(path)) return path;
|
|
683
|
+
}
|
|
706
684
|
return adjustedHref;
|
|
707
685
|
}
|
|
708
|
-
async function sd_fileExists(url) {
|
|
686
|
+
async function sd_fileExists(url) {
|
|
687
|
+
try { const response = await fetch(url, { method: "HEAD" }); return response.ok; } catch { return false; }
|
|
688
|
+
}
|
|
709
689
|
|
|
690
|
+
// --- URL Resolution Helper for "Others" ---
|
|
710
691
|
function resolveGameUrl(relativePath) {
|
|
711
|
-
|
|
692
|
+
// Logic:
|
|
693
|
+
// If path starts with ../ -> Go to WORKER_ROOT + path (stripping ../)
|
|
694
|
+
// If path starts with ../GAMES/ -> It will effectively be WORKER_ROOT/GAMES/ (which matches GAMES_BASE_URL)
|
|
695
|
+
// This allows us to access root folders like ../GTA-JSDOS/ while keeping ../GAMES/sm64 intact.
|
|
696
|
+
|
|
697
|
+
if (relativePath.startsWith('../')) {
|
|
698
|
+
const pathPart = relativePath.substring(3); // Remove '../'
|
|
699
|
+
return `${WORKER_ROOT}${pathPart}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// If it doesn't start with ../, assume it's relative to GAMES base
|
|
712
703
|
return `${GAMES_BASE_URL}${relativePath.replace(/^\.\//, '')}`;
|
|
713
704
|
}
|
|
714
705
|
|
|
706
|
+
// --- Game Data (Others) ---
|
|
707
|
+
// We keep the original relative paths here so the resolver logic above can handle them dynamically
|
|
715
708
|
const othersGames = [
|
|
716
709
|
{ 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
710
|
{ 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
711
|
{ 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" }
|
|
712
|
+
{ 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" },
|
|
713
|
+
{ id: "other-sm64", name: "Super Mario 64", imgSrc: "./images/sm64.png", description: "The iconic 3D platformer that revolutionized gaming. Run, jump, and triple-jump your way through vast worlds to collect stars and save the princess.", category: "Others", url: "../GAMES/sm64/index.html" }
|
|
720
714
|
];
|
|
721
715
|
|
|
722
716
|
// --- Card Creation ---
|
|
717
|
+
|
|
723
718
|
function createOthersGameCard(game) {
|
|
724
719
|
const favorites = getFavorites().map(String);
|
|
725
720
|
const isAnyFavorite = game.versions ? favorites.some(favId => game.versions.some(v => String(v.favoriteId) === favId)) : favorites.includes(String(game.id));
|
|
726
721
|
const card = document.createElement("div");
|
|
727
722
|
card.className = 'other-zone-item bg-card-dark rounded-2xl border border-brand-border col-span-full shadow-lg';
|
|
728
723
|
card.dataset.gameId = game.id;
|
|
724
|
+
|
|
725
|
+
// Resolve Image URL
|
|
729
726
|
const resolvedImgSrc = resolveGameUrl(game.imgSrc);
|
|
730
|
-
let gameButtonsHtml = '', favoriteButtonHtml = '', offlineButtonHtml = '';
|
|
731
|
-
const isEaglercraft = game.id === 'other-eaglercraft';
|
|
732
727
|
|
|
728
|
+
let gameButtonsHtml = '';
|
|
729
|
+
let favoriteButtonHtml = '';
|
|
730
|
+
|
|
733
731
|
if (game.versions && game.versions.length > 0) {
|
|
734
732
|
const availableVersions = game.versions;
|
|
735
733
|
if (availableVersions.length === 1) {
|
|
736
734
|
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>`;
|
|
735
|
+
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 transition-colors"></i></button>`;
|
|
738
736
|
} else {
|
|
739
|
-
const dropdownLinks = availableVersions.map(v =>
|
|
740
|
-
|
|
737
|
+
const dropdownLinks = availableVersions.map(v => {
|
|
738
|
+
const vUrl = resolveGameUrl(v.url);
|
|
739
|
+
return `<a href="#" class="eagler-dropdown-link" data-url="${vUrl}" data-version-name="${v.name}">${v.name}</a>`
|
|
740
|
+
}).join('');
|
|
741
|
+
gameButtonsHtml = `<div class="relative"><button class="btn-card-action version-btn" title="Select Version"><i class="fa-solid fa-chevron-up transition-colors"></i></button><div class="eagler-dropdown">${dropdownLinks}</div></div>`;
|
|
741
742
|
}
|
|
742
|
-
|
|
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>`;
|
|
743
|
+
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 transition-colors"></i><i class="fa-regular fa-star fa-regular-star transition-colors"></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
744
|
} else {
|
|
747
745
|
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
|
-
|
|
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>`;
|
|
746
|
+
gameButtonsHtml = `<button class="btn-card-action play-action" data-url="${gUrl}" title="Play Game"><i class="fa-solid fa-play transition-colors"></i></button>`;
|
|
747
|
+
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 transition-colors"></i><i class="fa-regular fa-star fa-regular-star transition-colors"></i></button>`;
|
|
751
748
|
}
|
|
752
749
|
|
|
753
750
|
card.innerHTML = `
|
|
@@ -758,7 +755,8 @@
|
|
|
758
755
|
<div class="flex items-start justify-between w-full">
|
|
759
756
|
<h3 class="text-4xl font-bold text-white truncate drop-shadow-lg" style="max-width: 80%;" title="${game.name}">${game.name}</h3>
|
|
760
757
|
<div class="flex items-center gap-1.5 bg-black/50 backdrop-blur-sm rounded-2xl p-1.5">
|
|
761
|
-
${gameButtonsHtml}
|
|
758
|
+
${gameButtonsHtml}
|
|
759
|
+
${favoriteButtonHtml}
|
|
762
760
|
</div>
|
|
763
761
|
</div>
|
|
764
762
|
<p class="text-lg text-white font-medium drop-shadow-lg" style="max-width: 50%;">${game.description}</p>
|
|
@@ -766,59 +764,53 @@
|
|
|
766
764
|
</div>
|
|
767
765
|
`;
|
|
768
766
|
|
|
769
|
-
if (!game.versions) {
|
|
767
|
+
if (!game.versions) {
|
|
768
|
+
card.querySelector('.group').addEventListener('click', () => {
|
|
769
|
+
const gUrl = resolveGameUrl(game.url);
|
|
770
|
+
openZone({...game, url: gUrl});
|
|
771
|
+
});
|
|
772
|
+
}
|
|
770
773
|
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
774
|
|
|
772
775
|
const versionBtn = card.querySelector('.version-btn');
|
|
773
776
|
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
777
|
|
|
775
|
-
|
|
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'); }));
|
|
778
|
+
card.querySelectorAll('.eagler-dropdown-link:not(.favorite-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
779
|
|
|
781
780
|
const favBtn = card.querySelector('.btn-card-action.fav-action');
|
|
782
781
|
if (favBtn.classList.contains('version-favorite-btn')) {
|
|
783
782
|
favBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); favBtn.nextElementSibling.classList.toggle('show'); });
|
|
784
783
|
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 {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (offBtn) { offBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleOfflineMode(game.id, game.name, resolveGameUrl(game.url), offBtn); }); }
|
|
789
|
-
|
|
784
|
+
} else {
|
|
785
|
+
favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); });
|
|
786
|
+
}
|
|
790
787
|
imageObserver.observe(card.querySelector('img'));
|
|
791
788
|
return card;
|
|
792
789
|
}
|
|
793
790
|
|
|
794
791
|
function createGameCard(game, forceSmallCard = false) {
|
|
795
792
|
if (game.category === 'Others' && !forceSmallCard) return createOthersGameCard(game);
|
|
796
|
-
const isStrongdog = game.category === 'StrongdogXP'
|
|
793
|
+
const isStrongdog = game.category === 'StrongdogXP';
|
|
794
|
+
|
|
795
|
+
// Resolve Image Src
|
|
797
796
|
let imgSrc = game.imgSrc;
|
|
798
|
-
if (isStrongdog)
|
|
799
|
-
|
|
797
|
+
if (isStrongdog) {
|
|
798
|
+
imgSrc = getAdjustedUrls(game.imgSrc, game.page).adjustedImgSrc;
|
|
799
|
+
} else if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
800
|
+
imgSrc = resolveGameUrl(game.imgSrc);
|
|
801
|
+
}
|
|
800
802
|
|
|
801
803
|
const isFavorite = getFavorites().map(String).includes(String(game.id));
|
|
804
|
+
|
|
802
805
|
const card = document.createElement("div");
|
|
803
806
|
card.className = 'zone-item bg-card-dark rounded-2xl border border-brand-border overflow-hidden';
|
|
804
807
|
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
808
|
card.innerHTML = `
|
|
816
809
|
<div class="relative w-full cursor-pointer group">
|
|
817
810
|
<div class="aspect-w-3 aspect-h-2"><img data-src="${imgSrc}" alt="${game.name}" class="w-full h-full object-cover"></div>
|
|
818
811
|
<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
812
|
<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}
|
|
813
|
+
<button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play transition-colors"></i></button>
|
|
822
814
|
<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
815
|
</div>
|
|
824
816
|
</div>
|
|
@@ -831,121 +823,136 @@
|
|
|
831
823
|
let adjustedHref = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
832
824
|
const embedPath = await sd_getEmbedPath(adjustedHref, game.href, game.page);
|
|
833
825
|
openZone({ name: game.name, url: embedPath, category: game.category, id: game.id });
|
|
834
|
-
} else {
|
|
826
|
+
} else {
|
|
827
|
+
// For GN-Math, url is absolute (handled in init). For Others/Gameboy, we might need to resolve
|
|
828
|
+
let gUrl = game.url;
|
|
829
|
+
if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
830
|
+
gUrl = resolveGameUrl(game.url);
|
|
831
|
+
}
|
|
832
|
+
openZone({ ...game, url: gUrl });
|
|
833
|
+
}
|
|
835
834
|
};
|
|
836
835
|
|
|
837
836
|
card.querySelector('.group').addEventListener('click', handlePlay);
|
|
838
837
|
card.querySelector('.play-action').addEventListener('click', handlePlay);
|
|
839
838
|
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
839
|
imageObserver.observe(card.querySelector('img'));
|
|
845
840
|
return card;
|
|
846
841
|
}
|
|
847
842
|
|
|
848
843
|
// --- Game Viewer Logic ---
|
|
849
|
-
|
|
844
|
+
function openZone(game) {
|
|
850
845
|
if (!game || !game.url) return;
|
|
851
846
|
updateURL(categories[currentCategoryIndex], game.id || null);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
doc.close();
|
|
859
|
-
zoneViewer.style.display = "flex"; zoneViewer.classList.add('active', 'animate-fade-in');
|
|
860
|
-
};
|
|
847
|
+
|
|
848
|
+
const zoneViewer = document.getElementById('zoneViewer');
|
|
849
|
+
const zoneFrame = document.getElementById('zoneFrame');
|
|
850
|
+
const zoneNameEl = document.getElementById('zoneNameEl');
|
|
851
|
+
const downloadBtn = document.getElementById('downloadBtnZone');
|
|
852
|
+
const controls = zoneViewer.querySelector('.zone-controls');
|
|
861
853
|
|
|
854
|
+
// Setup Download Button (Specific to Eaglercraft)
|
|
862
855
|
if (game.baseGameId === 'other-eaglercraft' || game.id === 'other-eaglercraft') {
|
|
863
|
-
downloadBtn.href = game.url;
|
|
864
|
-
|
|
856
|
+
downloadBtn.href = game.url;
|
|
857
|
+
downloadBtn.download = game.name.replace(/ /g, '_') + '.html';
|
|
858
|
+
downloadBtn.removeAttribute('target');
|
|
859
|
+
downloadBtn.classList.remove('hidden');
|
|
860
|
+
} else {
|
|
861
|
+
downloadBtn.classList.add('hidden');
|
|
862
|
+
downloadBtn.href = '#';
|
|
863
|
+
downloadBtn.removeAttribute('download');
|
|
864
|
+
}
|
|
865
865
|
|
|
866
|
+
// --- INJECT FAVORITE BUTTON IN PLAYER HEADER ---
|
|
867
|
+
// Remove existing if any
|
|
866
868
|
const existingFav = controls.querySelector('.player-fav-btn');
|
|
867
869
|
if (existingFav) existingFav.remove();
|
|
870
|
+
|
|
868
871
|
const favBtn = document.createElement('button');
|
|
869
872
|
favBtn.className = 'player-fav-btn btn-card-action fav-action';
|
|
870
873
|
favBtn.innerHTML = '<i class="fa-solid fa-star fa-solid-star"></i><i class="fa-regular fa-star fa-regular-star"></i>';
|
|
871
|
-
|
|
872
|
-
|
|
874
|
+
const isFavorited = getFavorites().map(String).includes(String(game.id));
|
|
875
|
+
if (isFavorited) favBtn.classList.add('favorited');
|
|
876
|
+
|
|
877
|
+
favBtn.onclick = (e) => {
|
|
878
|
+
e.stopPropagation();
|
|
879
|
+
toggleFavorite(game.id);
|
|
880
|
+
favBtn.classList.toggle('favorited');
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// Prepend to controls (Left side)
|
|
873
884
|
controls.prepend(favBtn);
|
|
874
885
|
|
|
875
|
-
zoneNameEl.textContent = game.name;
|
|
876
|
-
|
|
886
|
+
zoneNameEl.textContent = game.name;
|
|
887
|
+
|
|
888
|
+
// RESET FRAME BEFORE LOAD
|
|
889
|
+
zoneFrame.src = 'about:blank';
|
|
890
|
+
|
|
891
|
+
// SECURITY: Set sandbox permissions
|
|
892
|
+
const sandboxRules = 'allow-scripts allow-same-origin allow-forms allow-pointer-lock';
|
|
893
|
+
zoneFrame.setAttribute('sandbox', sandboxRules);
|
|
877
894
|
zoneFrame.setAttribute('allow', 'fullscreen; pointer-lock; autoplay; clipboard-write');
|
|
878
895
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (!navigator.onLine) { show404("You are offline and this game isn't saved."); return; }
|
|
887
|
-
|
|
888
|
-
try {
|
|
889
|
-
const check = await fetch(game.url, { method: 'HEAD' });
|
|
890
|
-
if (!check.ok) throw new Error("File not found");
|
|
896
|
+
// --- REVISED LOGIC FOR LOADING GAMES ---
|
|
897
|
+
const isStandardURLGame = game.category === 'StrongdogXP' || game.category === 'Others' || game.category === 'GN-Math';
|
|
898
|
+
|
|
899
|
+
if (isStandardURLGame) {
|
|
891
900
|
zoneFrame.src = game.url;
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
901
|
+
|
|
902
|
+
// SHOW WITH ANIMATION
|
|
903
|
+
zoneViewer.style.display = "flex";
|
|
904
|
+
zoneViewer.classList.remove('animate-fade-out');
|
|
905
|
+
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
906
|
+
} else {
|
|
907
|
+
fetch(`${game.url}?t=${Date.now()}`)
|
|
908
|
+
.then(response => {
|
|
909
|
+
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
|
|
910
|
+
return response.text();
|
|
911
|
+
})
|
|
912
|
+
.then(html => {
|
|
913
|
+
const doc = zoneFrame.contentWindow.document;
|
|
914
|
+
doc.open();
|
|
915
|
+
doc.write(html);
|
|
916
|
+
doc.close();
|
|
917
|
+
|
|
918
|
+
// SHOW WITH ANIMATION
|
|
919
|
+
zoneViewer.style.display = "flex";
|
|
920
|
+
zoneViewer.classList.remove('animate-fade-out');
|
|
921
|
+
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
922
|
+
})
|
|
923
|
+
.catch(error => {
|
|
924
|
+
console.error(`Failed to load game "${game.name}": ${error.message}`);
|
|
925
|
+
zoneFrame.src = game.url;
|
|
926
|
+
|
|
927
|
+
zoneViewer.style.display = "flex";
|
|
928
|
+
zoneViewer.classList.remove('animate-fade-out');
|
|
929
|
+
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
930
|
+
});
|
|
910
931
|
}
|
|
911
932
|
}
|
|
912
933
|
|
|
913
934
|
function closeZoneViewer() {
|
|
914
|
-
const zoneViewer = document.getElementById('zoneViewer')
|
|
915
|
-
|
|
935
|
+
const zoneViewer = document.getElementById('zoneViewer');
|
|
936
|
+
const zoneFrame = document.getElementById('zoneFrame');
|
|
937
|
+
|
|
938
|
+
zoneViewer.classList.remove('animate-fade-in');
|
|
939
|
+
zoneViewer.classList.add('animate-fade-out');
|
|
940
|
+
|
|
916
941
|
setTimeout(() => {
|
|
917
|
-
zoneViewer.style.display = "none";
|
|
918
|
-
|
|
942
|
+
zoneViewer.style.display = "none";
|
|
943
|
+
zoneViewer.classList.remove('active', 'animate-fade-out');
|
|
944
|
+
if (zoneFrame) {
|
|
945
|
+
zoneFrame.src = 'about:blank';
|
|
946
|
+
}
|
|
919
947
|
updateURL(categories[currentCategoryIndex]);
|
|
948
|
+
const downloadBtn = document.getElementById('downloadBtnZone');
|
|
949
|
+
downloadBtn.classList.add('hidden');
|
|
950
|
+
downloadBtn.href = '#';
|
|
951
|
+
downloadBtn.removeAttribute('download');
|
|
952
|
+
downloadBtn.removeAttribute('target');
|
|
920
953
|
}, 300);
|
|
921
954
|
}
|
|
922
955
|
|
|
923
|
-
// --- Sound & Notification Logic ---
|
|
924
|
-
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
925
|
-
let isMuted = false;
|
|
926
|
-
function playClickSound() {
|
|
927
|
-
if (isMuted) return; if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
928
|
-
const osc = audioCtx.createOscillator(), gainNode = audioCtx.createGain();
|
|
929
|
-
osc.connect(gainNode); gainNode.connect(audioCtx.destination);
|
|
930
|
-
osc.type = 'sine'; osc.frequency.setValueAtTime(300, audioCtx.currentTime);
|
|
931
|
-
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.015);
|
|
932
|
-
osc.start(); osc.stop(audioCtx.currentTime + 0.015);
|
|
933
|
-
}
|
|
934
|
-
function showNotification(message, iconClass = 'fa-solid fa-info-circle', type = 'info') {
|
|
935
|
-
const container = document.getElementById('notification-container');
|
|
936
|
-
if (!container) return;
|
|
937
|
-
while (container.children.length >= 3) container.removeChild(container.firstChild);
|
|
938
|
-
const toast = document.createElement('div'); toast.className = 'notification-toast';
|
|
939
|
-
toast.innerHTML = `<i class="${iconClass} notification-icon ${type}"></i><span>${message}</span>`;
|
|
940
|
-
container.appendChild(toast);
|
|
941
|
-
requestAnimationFrame(() => { toast.classList.add('show'); playClickSound(); });
|
|
942
|
-
setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (toast.parentElement) toast.remove(); }, 300); }, 3000);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Expose to window
|
|
946
|
-
window.openZone = openZone; window.closeZoneViewer = closeZoneViewer; window.toggleOfflineMode = toggleOfflineMode;
|
|
947
|
-
window.showNotification = showNotification; window.toggleFavorite = toggleFavorite; window.getUserBrowserLocation = async function() {};
|
|
948
|
-
|
|
949
956
|
// --- Render Lists ---
|
|
950
957
|
function renderGames(gamesToRender, targetElement, forceSmallCard = false) {
|
|
951
958
|
targetElement.innerHTML = '';
|
|
@@ -957,19 +964,34 @@
|
|
|
957
964
|
function renderFavorites() {
|
|
958
965
|
const favoriteIds = getFavorites().map(String);
|
|
959
966
|
if (favoriteIds.length > 0) {
|
|
960
|
-
favoritesHeader.style.display = "block";
|
|
967
|
+
favoritesHeader.style.display = "block";
|
|
968
|
+
favoritesGameList.style.display = "grid";
|
|
961
969
|
const favoriteGames = [];
|
|
962
970
|
allGames.forEach(game => {
|
|
963
|
-
if (favoriteIds.includes(String(game.id)) && !game.versions) favoriteGames.push(game);
|
|
971
|
+
if (favoriteIds.includes(String(game.id)) && !game.versions) { favoriteGames.push(game); }
|
|
964
972
|
else if (game.versions) {
|
|
965
|
-
game.versions.forEach(v => {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
973
|
+
game.versions.forEach(v => {
|
|
974
|
+
if (favoriteIds.includes(String(v.favoriteId))) {
|
|
975
|
+
let vUrl = v.url;
|
|
976
|
+
if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
977
|
+
vUrl = resolveGameUrl(v.url);
|
|
978
|
+
}
|
|
979
|
+
favoriteGames.push({ ...game, id: v.favoriteId, name: `${game.name} - ${v.name}`, url: vUrl, versions: undefined });
|
|
980
|
+
}
|
|
981
|
+
});
|
|
969
982
|
}
|
|
970
983
|
});
|
|
971
984
|
renderGames(favoriteGames.sort((a, b) => a.name.localeCompare(b.name)), favoritesGameList, true);
|
|
972
|
-
} else {
|
|
985
|
+
} else {
|
|
986
|
+
favoritesHeader.style.display = "none";
|
|
987
|
+
favoritesGameList.style.display = "none";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// --- Category Nav ---
|
|
992
|
+
function updateArrowVisibility() {
|
|
993
|
+
prevCategoryBtn.disabled = currentCategoryIndex === 0;
|
|
994
|
+
nextCategoryBtn.disabled = currentCategoryIndex === categories.length - 1;
|
|
973
995
|
}
|
|
974
996
|
|
|
975
997
|
function switchCategory(newIndex) {
|
|
@@ -988,77 +1010,230 @@
|
|
|
988
1010
|
gamesGridContainer.classList.remove('opacity-0');
|
|
989
1011
|
}
|
|
990
1012
|
|
|
1013
|
+
// --- Search ---
|
|
1014
|
+
function debounce(func, delay) {
|
|
1015
|
+
let timeout;
|
|
1016
|
+
return function(...args) {
|
|
1017
|
+
clearTimeout(timeout);
|
|
1018
|
+
timeout = setTimeout(() => func.apply(this, args), delay);
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
991
1022
|
const handleSearch = debounce(() => {
|
|
992
1023
|
const text = searchInput.value.toLowerCase().trim();
|
|
993
1024
|
const filteredDropdown = text ? searchableGames.filter(g => g.name.toLowerCase().includes(text)).slice(0, 5) : [];
|
|
1025
|
+
|
|
994
1026
|
searchResults.innerHTML = "";
|
|
995
|
-
|
|
1027
|
+
|
|
1028
|
+
if (filteredDropdown.length === 0 || !text) {
|
|
1029
|
+
searchResults.style.display = "none";
|
|
1030
|
+
searchDivider.style.display = "none";
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
996
1034
|
const favorites = getFavorites().map(String);
|
|
997
1035
|
filteredDropdown.forEach(game => {
|
|
998
1036
|
const isStrongdog = game.category === 'StrongdogXP';
|
|
999
|
-
let imgSrc
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1037
|
+
let imgSrc;
|
|
1038
|
+
let finalGameData = { ...game }; // clone
|
|
1039
|
+
|
|
1040
|
+
if (isStrongdog) {
|
|
1041
|
+
const { adjustedImgSrc } = getAdjustedUrls(game.imgSrc, game.page);
|
|
1042
|
+
imgSrc = adjustedImgSrc;
|
|
1043
|
+
} else if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
1044
|
+
imgSrc = resolveGameUrl(game.imgSrc);
|
|
1045
|
+
finalGameData.url = resolveGameUrl(game.url);
|
|
1046
|
+
} else {
|
|
1047
|
+
imgSrc = game.imgSrc;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const linkEl = document.createElement("a");
|
|
1051
|
+
linkEl.href = '#';
|
|
1052
|
+
linkEl.className = 'search-item flex justify-between items-center';
|
|
1053
|
+
linkEl.dataset.gameId = game.id;
|
|
1004
1054
|
let isFavorite = favorites.includes(String(game.id));
|
|
1055
|
+
|
|
1005
1056
|
const catLabel = game.category === 'StrongdogXP' ? 'StrongdogXP' : (game.category === 'GN-Math' ? 'GN-Math' : 'Others');
|
|
1006
1057
|
const catColor = game.category === 'StrongdogXP' ? 'text-strongdog-orange' : (game.category === 'GN-Math' ? 'text-gn-math' : 'text-gray-400');
|
|
1007
|
-
|
|
1008
|
-
linkEl.
|
|
1009
|
-
|
|
1058
|
+
|
|
1059
|
+
linkEl.innerHTML = `
|
|
1060
|
+
<div class="flex items-center truncate min-w-0">
|
|
1061
|
+
<img src="${imgSrc}" alt="${game.name}" class="w-10 h-10 rounded-lg object-cover flex-shrink-0">
|
|
1062
|
+
<div class="flex flex-col ml-3 truncate min-w-0">
|
|
1063
|
+
<span class="font-medium truncate text-gray-200" title="${game.name}">${game.name}</span>
|
|
1064
|
+
<span class="category-badge w-fit ${catColor}">${catLabel}</span>
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="flex items-center flex-shrink-0">
|
|
1068
|
+
<div class="bg-black/50 backdrop-blur-sm rounded-2xl p-1.5 flex items-center gap-1.5">
|
|
1069
|
+
<button class="btn-card-action play-action" title="Play Game">
|
|
1070
|
+
<i class="fa-solid fa-play transition-colors"></i>
|
|
1071
|
+
</button>
|
|
1072
|
+
<button class="btn-card-action fav-action ${isFavorite ? 'favorited' : ''}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}">
|
|
1073
|
+
<i class="fa-solid fa-star fa-solid-star transition-colors"></i>
|
|
1074
|
+
<i class="fa-regular fa-star fa-regular-star transition-colors"></i>
|
|
1075
|
+
</button>
|
|
1076
|
+
</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
`;
|
|
1079
|
+
|
|
1080
|
+
const favBtn = linkEl.querySelector('.fav-action');
|
|
1081
|
+
favBtn.addEventListener('click', (e) => {
|
|
1082
|
+
e.preventDefault(); e.stopPropagation();
|
|
1083
|
+
toggleFavorite(game.id);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
linkEl.addEventListener('click', async (e) => {
|
|
1087
|
+
e.preventDefault();
|
|
1088
|
+
if (isStrongdog) {
|
|
1089
|
+
const baseURL = sd_getBaseURLForPage(game.page);
|
|
1090
|
+
let adjustedHref = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
1091
|
+
const embedPath = await sd_getEmbedPath(adjustedHref, game.href, game.page);
|
|
1092
|
+
const strongdogGameData = { name: game.name, url: embedPath, category: game.category, id: game.id };
|
|
1093
|
+
openZone(strongdogGameData);
|
|
1094
|
+
} else {
|
|
1095
|
+
openZone(finalGameData);
|
|
1096
|
+
}
|
|
1097
|
+
searchResults.style.display = "none";
|
|
1098
|
+
searchDivider.style.display = "none";
|
|
1099
|
+
});
|
|
1010
1100
|
searchResults.appendChild(linkEl);
|
|
1011
1101
|
});
|
|
1012
|
-
searchResults.style.display = "block";
|
|
1102
|
+
searchResults.style.display = "block";
|
|
1103
|
+
searchDivider.style.display = "block";
|
|
1013
1104
|
}, 250);
|
|
1014
1105
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1106
|
+
// --- FETCHING DATA MANUALLY (NO MODULES) ---
|
|
1107
|
+
// This function fetches the JS file as text, transforms it to set a global variable, and evals it.
|
|
1108
|
+
// This bypasses module loading restrictions on file:// protocols.
|
|
1017
1109
|
async function fetchGameData() {
|
|
1018
1110
|
try {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1111
|
+
// Fetch the file
|
|
1112
|
+
const response = await fetch(CARDS_DATA_URL);
|
|
1113
|
+
if (!response.ok) throw new Error('Failed to fetch game data');
|
|
1114
|
+
let scriptText = await response.text();
|
|
1115
|
+
|
|
1116
|
+
// Transform 'export default' to 'window.loadedGameData ='
|
|
1117
|
+
// This assumes the file structure is simple "export default [...]"
|
|
1118
|
+
if (scriptText.includes('export default')) {
|
|
1119
|
+
scriptText = scriptText.replace('export default', 'window.loadedGameData =');
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Execute the script
|
|
1123
|
+
// We use new Function instead of eval for slight isolation, though still unsafe for untrusted code
|
|
1124
|
+
// (But we trust this source).
|
|
1125
|
+
// However, new Function creates a local scope. We need global assignment.
|
|
1126
|
+
// Standard eval or appending a script tag is better.
|
|
1127
|
+
const scriptEl = document.createElement('script');
|
|
1128
|
+
scriptEl.textContent = scriptText;
|
|
1129
|
+
document.body.appendChild(scriptEl);
|
|
1130
|
+
|
|
1131
|
+
// Wait a tick for execution
|
|
1022
1132
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1023
|
-
|
|
1024
|
-
|
|
1133
|
+
|
|
1134
|
+
if (window.loadedGameData) {
|
|
1135
|
+
return window.loadedGameData;
|
|
1136
|
+
} else {
|
|
1137
|
+
console.warn("Script loaded but window.loadedGameData is undefined");
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
console.error("Error manual loading games:", e);
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1025
1144
|
}
|
|
1026
1145
|
|
|
1146
|
+
// --- Init ---
|
|
1027
1147
|
async function initializeApp() {
|
|
1028
|
-
|
|
1148
|
+
// Load Game Data Manually
|
|
1149
|
+
let gamesData = [];
|
|
1150
|
+
try {
|
|
1151
|
+
gamesData = await fetchGameData();
|
|
1152
|
+
} catch (e) {
|
|
1153
|
+
console.error("Could not load external games data", e);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1029
1156
|
const strongdogGames = gamesData.map(g => ({ ...g, category: 'StrongdogXP' }));
|
|
1157
|
+
|
|
1030
1158
|
try {
|
|
1031
|
-
const res = await fetch(GN_ZONES_URL)
|
|
1159
|
+
const res = await fetch(GN_ZONES_URL);
|
|
1160
|
+
const gnMathRaw = await res.json();
|
|
1032
1161
|
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) }));
|
|
1033
1162
|
allGames = [...strongdogGames, ...gnMathGames, ...othersGames];
|
|
1034
|
-
} catch (error) {
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
console.warn("Failed to load GN-Math, falling back to others.", error);
|
|
1165
|
+
allGames = [...strongdogGames, ...othersGames];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1035
1168
|
allGames = allGames.filter(g => !['bitlife', 'soundboard'].some(term => g.name.toLowerCase().includes(term)));
|
|
1169
|
+
|
|
1170
|
+
searchableGames = [];
|
|
1036
1171
|
allGames.forEach(game => {
|
|
1037
|
-
if (game.versions && game.versions.length > 0)
|
|
1038
|
-
|
|
1172
|
+
if (game.versions && game.versions.length > 0) {
|
|
1173
|
+
game.versions.forEach(version => {
|
|
1174
|
+
searchableGames.push({
|
|
1175
|
+
...game,
|
|
1176
|
+
id: version.favoriteId,
|
|
1177
|
+
name: `${game.name} - ${version.name}`,
|
|
1178
|
+
url: version.url,
|
|
1179
|
+
wasm: version.wasm,
|
|
1180
|
+
versions: undefined,
|
|
1181
|
+
baseGameId: game.id
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
} else {
|
|
1185
|
+
searchableGames.push(game);
|
|
1186
|
+
}
|
|
1039
1187
|
});
|
|
1188
|
+
|
|
1040
1189
|
categories = ['StrongdogXP', 'GN-Math', 'Gameboy Games', 'Others'].filter(cat => allGames.some(g => g.category === cat));
|
|
1190
|
+
|
|
1041
1191
|
const { category: urlCat } = parseURL();
|
|
1042
1192
|
if (urlCat) currentCategoryIndex = Math.max(0, categories.findIndex(c => c.replace(/\s+/g, '-').toLowerCase() === urlCat.toLowerCase()));
|
|
1193
|
+
|
|
1043
1194
|
categorySlider.style.width = `${categories.length * 100}%`;
|
|
1044
1195
|
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('');
|
|
1045
|
-
|
|
1046
|
-
|
|
1196
|
+
|
|
1197
|
+
switchCategory(currentCategoryIndex);
|
|
1198
|
+
renderFavorites();
|
|
1199
|
+
setupInstructionOverlay();
|
|
1200
|
+
|
|
1201
|
+
searchInput.placeholder = `Search all games...`;
|
|
1202
|
+
searchInput.addEventListener("input", handleSearch);
|
|
1203
|
+
searchInput.addEventListener("focus", handleSearch);
|
|
1204
|
+
|
|
1047
1205
|
document.addEventListener("click", ev => {
|
|
1048
|
-
if (!document.getElementById('bottom-fixed-bar').contains(ev.target)) {
|
|
1049
|
-
|
|
1206
|
+
if (!document.getElementById('bottom-fixed-bar').contains(ev.target)) {
|
|
1207
|
+
searchResults.style.display = "none";
|
|
1208
|
+
searchDivider.style.display = "none";
|
|
1209
|
+
}
|
|
1210
|
+
if (!ev.target.closest('.version-btn') && !ev.target.closest('.version-favorite-btn') && !ev.target.closest('.eagler-dropdown')) {
|
|
1211
|
+
document.querySelectorAll('.eagler-dropdown.show').forEach(d => {
|
|
1212
|
+
d.classList.remove('show');
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1050
1215
|
});
|
|
1216
|
+
|
|
1051
1217
|
prevCategoryBtn.addEventListener('click', () => switchCategory(currentCategoryIndex - 1));
|
|
1052
1218
|
nextCategoryBtn.addEventListener('click', () => switchCategory(currentCategoryIndex + 1));
|
|
1053
1219
|
document.getElementById('closeBtnZone').addEventListener('click', closeZoneViewer);
|
|
1054
|
-
|
|
1220
|
+
|
|
1221
|
+
// --- FULLSCREEN BUTTON LISTENER ---
|
|
1222
|
+
document.getElementById('fullscreenBtnZone').addEventListener('click', () => {
|
|
1223
|
+
const zoneFrame = document.getElementById('zoneFrame');
|
|
1224
|
+
if (zoneFrame && zoneFrame.requestFullscreen) {
|
|
1225
|
+
zoneFrame.requestFullscreen();
|
|
1226
|
+
} else if (zoneFrame && zoneFrame.webkitRequestFullscreen) {
|
|
1227
|
+
zoneFrame.webkitRequestFullscreen();
|
|
1228
|
+
} else if (zoneFrame && zoneFrame.msRequestFullscreen) {
|
|
1229
|
+
zoneFrame.msRequestFullscreen();
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1055
1233
|
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(''); });
|
|
1056
1234
|
closeCreditsBtn.addEventListener('click', () => creditsModal.style.display = 'none');
|
|
1057
|
-
document.addEventListener('click', (e) => { const target = e.target.closest('button, .nav-tab, .zone-item, .btn-card-action, a, .icon-btn'); if (target) playClickSound(); });
|
|
1058
|
-
document.addEventListener('keydown', (e) => { const target = e.target.closest('input, textarea'); if (target && !e.repeat && window.parent?.playTypeSound) window.parent.playTypeSound(); });
|
|
1059
|
-
document.addEventListener('copy', () => { if (window.getSelection().toString().length > 0) showNotification('Copied to clipboard', 'fa-solid fa-copy', 'success'); });
|
|
1060
|
-
document.addEventListener('paste', (e) => { if (e.target.closest('input, textarea')) showNotification('Pasted from clipboard', 'fa-solid fa-paste', 'info'); });
|
|
1061
1235
|
}
|
|
1236
|
+
|
|
1062
1237
|
document.addEventListener('DOMContentLoaded', initializeApp);
|
|
1063
1238
|
</script>
|
|
1064
1239
|
</body>
|