4sp-dv 1.0.32 → 1.0.34
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 +58 -4
- package/logged-in/games.html +209 -45
- 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.33/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.33/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,25 @@
|
|
|
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
|
+
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
|
+
|
|
1283
1327
|
try {
|
|
1284
1328
|
const docRef = doc(db, "access_codes", savedCode);
|
|
1285
1329
|
const docSnap = await getDoc(docRef);
|
|
@@ -1311,7 +1355,17 @@
|
|
|
1311
1355
|
}
|
|
1312
1356
|
} catch(e) {
|
|
1313
1357
|
console.error("Auto-login error:", e);
|
|
1314
|
-
|
|
1358
|
+
// Check if we can fallback to offline even on error (e.g. timeout)
|
|
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
|
+
}
|
|
1315
1369
|
}
|
|
1316
1370
|
}
|
|
1317
1371
|
}
|
package/logged-in/games.html
CHANGED
|
@@ -709,10 +709,84 @@
|
|
|
709
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 } ] },
|
|
710
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" } ] },
|
|
711
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" } ] },
|
|
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" }
|
|
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" }
|
|
714
713
|
];
|
|
715
714
|
|
|
715
|
+
// --- Offline Mode Logic (IndexedDB) ---
|
|
716
|
+
const DB_NAME = '4SP_OfflineGames';
|
|
717
|
+
const DB_VERSION = 1;
|
|
718
|
+
const STORE_NAME = 'games';
|
|
719
|
+
|
|
720
|
+
function openDB() {
|
|
721
|
+
return new Promise((resolve, reject) => {
|
|
722
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
723
|
+
request.onupgradeneeded = (e) => {
|
|
724
|
+
const db = e.target.result;
|
|
725
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
726
|
+
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
730
|
+
request.onerror = (e) => reject(e.target.error);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function saveGameOffline(gameId, name, url) {
|
|
735
|
+
try {
|
|
736
|
+
const res = await fetch(url);
|
|
737
|
+
if (!res.ok) throw new Error('Fetch failed');
|
|
738
|
+
const html = await res.text();
|
|
739
|
+
|
|
740
|
+
const db = await openDB();
|
|
741
|
+
return new Promise((resolve, reject) => {
|
|
742
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
743
|
+
const store = tx.objectStore(STORE_NAME);
|
|
744
|
+
store.put({ id: String(gameId), name, html, timestamp: Date.now() });
|
|
745
|
+
tx.oncomplete = () => {
|
|
746
|
+
db.close();
|
|
747
|
+
resolve();
|
|
748
|
+
};
|
|
749
|
+
tx.onerror = () => reject(tx.error);
|
|
750
|
+
});
|
|
751
|
+
} catch (e) {
|
|
752
|
+
console.error("Offline Save Error:", e);
|
|
753
|
+
throw e;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function getOfflineGame(gameId) {
|
|
758
|
+
const db = await openDB();
|
|
759
|
+
return new Promise((resolve, reject) => {
|
|
760
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
761
|
+
const store = tx.objectStore(STORE_NAME);
|
|
762
|
+
const request = store.get(String(gameId));
|
|
763
|
+
request.onsuccess = () => {
|
|
764
|
+
db.close();
|
|
765
|
+
resolve(request.result);
|
|
766
|
+
};
|
|
767
|
+
request.onerror = () => reject(request.error);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function removeOfflineGame(gameId) {
|
|
772
|
+
const db = await openDB();
|
|
773
|
+
return new Promise((resolve, reject) => {
|
|
774
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
775
|
+
const store = tx.objectStore(STORE_NAME);
|
|
776
|
+
store.delete(String(gameId));
|
|
777
|
+
tx.oncomplete = () => {
|
|
778
|
+
db.close();
|
|
779
|
+
resolve();
|
|
780
|
+
};
|
|
781
|
+
tx.onerror = () => reject(tx.error);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function isGameOffline(gameId) {
|
|
786
|
+
const game = await getOfflineGame(gameId);
|
|
787
|
+
return !!game;
|
|
788
|
+
}
|
|
789
|
+
|
|
716
790
|
// --- Card Creation ---
|
|
717
791
|
|
|
718
792
|
function createOthersGameCard(game) {
|
|
@@ -727,24 +801,30 @@
|
|
|
727
801
|
|
|
728
802
|
let gameButtonsHtml = '';
|
|
729
803
|
let favoriteButtonHtml = '';
|
|
804
|
+
let offlineButtonHtml = '';
|
|
730
805
|
|
|
731
806
|
if (game.versions && game.versions.length > 0) {
|
|
732
807
|
const availableVersions = game.versions;
|
|
733
808
|
if (availableVersions.length === 1) {
|
|
734
809
|
const vUrl = resolveGameUrl(availableVersions[0].url);
|
|
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
|
|
810
|
+
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>`;
|
|
736
811
|
} else {
|
|
737
812
|
const dropdownLinks = availableVersions.map(v => {
|
|
738
813
|
const vUrl = resolveGameUrl(v.url);
|
|
739
814
|
return `<a href="#" class="eagler-dropdown-link" data-url="${vUrl}" data-version-name="${v.name}">${v.name}</a>`
|
|
740
815
|
}).join('');
|
|
741
|
-
gameButtonsHtml = `<div class="relative"><button class="btn-card-action version-btn" title="Select Version"><i class="fa-solid fa-chevron-up
|
|
816
|
+
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>`;
|
|
742
817
|
}
|
|
743
|
-
|
|
818
|
+
|
|
819
|
+
// Offline Toggle for versions
|
|
820
|
+
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>`;
|
|
821
|
+
|
|
822
|
+
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>`;
|
|
744
823
|
} else {
|
|
745
824
|
const gUrl = resolveGameUrl(game.url);
|
|
746
|
-
gameButtonsHtml = `<button class="btn-card-action play-action" data-url="${gUrl}" title="Play Game"><i class="fa-solid fa-play
|
|
747
|
-
|
|
825
|
+
gameButtonsHtml = `<button class="btn-card-action play-action" data-url="${gUrl}" title="Play Game"><i class="fa-solid fa-play"></i></button>`;
|
|
826
|
+
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>`;
|
|
827
|
+
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>`;
|
|
748
828
|
}
|
|
749
829
|
|
|
750
830
|
card.innerHTML = `
|
|
@@ -754,8 +834,9 @@
|
|
|
754
834
|
<div class="absolute inset-0 p-4 sm:p-6 flex flex-col justify-between rounded-2xl">
|
|
755
835
|
<div class="flex items-start justify-between w-full">
|
|
756
836
|
<h3 class="text-4xl font-bold text-white truncate drop-shadow-lg" style="max-width: 80%;" title="${game.name}">${game.name}</h3>
|
|
757
|
-
<div class="flex items-center gap-
|
|
837
|
+
<div class="flex items-center gap-1.5 bg-black/50 backdrop-blur-sm rounded-2xl p-1.5">
|
|
758
838
|
${gameButtonsHtml}
|
|
839
|
+
${offlineButtonHtml}
|
|
759
840
|
${favoriteButtonHtml}
|
|
760
841
|
</div>
|
|
761
842
|
</div>
|
|
@@ -775,7 +856,12 @@
|
|
|
775
856
|
const versionBtn = card.querySelector('.version-btn');
|
|
776
857
|
if (versionBtn) { versionBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); versionBtn.nextElementSibling.classList.toggle('show'); }); }
|
|
777
858
|
|
|
778
|
-
|
|
859
|
+
const offlineVerBtn = card.querySelector('.version-offline-btn');
|
|
860
|
+
if (offlineVerBtn) { offlineVerBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); offlineVerBtn.nextElementSibling.classList.toggle('show'); }); }
|
|
861
|
+
|
|
862
|
+
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'); }));
|
|
863
|
+
|
|
864
|
+
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'); }));
|
|
779
865
|
|
|
780
866
|
const favBtn = card.querySelector('.btn-card-action.fav-action');
|
|
781
867
|
if (favBtn.classList.contains('version-favorite-btn')) {
|
|
@@ -784,6 +870,12 @@
|
|
|
784
870
|
} else {
|
|
785
871
|
favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); });
|
|
786
872
|
}
|
|
873
|
+
|
|
874
|
+
const offBtn = card.querySelector('.offline-action:not(.version-offline-btn)');
|
|
875
|
+
if (offBtn) {
|
|
876
|
+
offBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleOfflineMode(game.id, game.name, resolveGameUrl(game.url), offBtn); });
|
|
877
|
+
}
|
|
878
|
+
|
|
787
879
|
imageObserver.observe(card.querySelector('img'));
|
|
788
880
|
return card;
|
|
789
881
|
}
|
|
@@ -805,13 +897,23 @@
|
|
|
805
897
|
const card = document.createElement("div");
|
|
806
898
|
card.className = 'zone-item bg-card-dark rounded-2xl border border-brand-border overflow-hidden';
|
|
807
899
|
card.dataset.gameId = game.id;
|
|
900
|
+
|
|
901
|
+
let gUrl = game.url;
|
|
902
|
+
if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
903
|
+
gUrl = resolveGameUrl(game.url);
|
|
904
|
+
} else if (isStrongdog) {
|
|
905
|
+
const baseURL = sd_getBaseURLForPage(game.page);
|
|
906
|
+
gUrl = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
907
|
+
}
|
|
908
|
+
|
|
808
909
|
card.innerHTML = `
|
|
809
910
|
<div class="relative w-full cursor-pointer group">
|
|
810
911
|
<div class="aspect-w-3 aspect-h-2"><img data-src="${imgSrc}" alt="${game.name}" class="w-full h-full object-cover"></div>
|
|
811
912
|
<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>
|
|
812
|
-
<div class="absolute bottom-2 right-2 bg-black/
|
|
813
|
-
<button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play
|
|
814
|
-
<button class="btn-card-action
|
|
913
|
+
<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">
|
|
914
|
+
<button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play"></i></button>
|
|
915
|
+
<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>
|
|
916
|
+
<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"></i><i class="fa-regular fa-star fa-regular-star"></i></button>
|
|
815
917
|
</div>
|
|
816
918
|
</div>
|
|
817
919
|
`;
|
|
@@ -824,11 +926,6 @@
|
|
|
824
926
|
const embedPath = await sd_getEmbedPath(adjustedHref, game.href, game.page);
|
|
825
927
|
openZone({ name: game.name, url: embedPath, category: game.category, id: game.id });
|
|
826
928
|
} 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
929
|
openZone({ ...game, url: gUrl });
|
|
833
930
|
}
|
|
834
931
|
};
|
|
@@ -836,12 +933,16 @@
|
|
|
836
933
|
card.querySelector('.group').addEventListener('click', handlePlay);
|
|
837
934
|
card.querySelector('.play-action').addEventListener('click', handlePlay);
|
|
838
935
|
card.querySelector('.fav-action').addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); });
|
|
936
|
+
|
|
937
|
+
const offBtn = card.querySelector('.offline-action');
|
|
938
|
+
offBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleOfflineMode(game.id, game.name, gUrl, offBtn); });
|
|
939
|
+
|
|
839
940
|
imageObserver.observe(card.querySelector('img'));
|
|
840
941
|
return card;
|
|
841
942
|
}
|
|
842
943
|
|
|
843
944
|
// --- Game Viewer Logic ---
|
|
844
|
-
function openZone(game) {
|
|
945
|
+
async function openZone(game) {
|
|
845
946
|
if (!game || !game.url) return;
|
|
846
947
|
updateURL(categories[currentCategoryIndex], game.id || null);
|
|
847
948
|
|
|
@@ -851,20 +952,45 @@
|
|
|
851
952
|
const downloadBtn = document.getElementById('downloadBtnZone');
|
|
852
953
|
const controls = zoneViewer.querySelector('.zone-controls');
|
|
853
954
|
|
|
854
|
-
//
|
|
955
|
+
// --- Custom 404 Page Helper ---
|
|
956
|
+
const show404 = (errorMsg = "Game content unavailable.") => {
|
|
957
|
+
const doc = zoneFrame.contentWindow.document;
|
|
958
|
+
doc.open();
|
|
959
|
+
doc.write(`
|
|
960
|
+
<!DOCTYPE html>
|
|
961
|
+
<html>
|
|
962
|
+
<head>
|
|
963
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
|
964
|
+
<style>
|
|
965
|
+
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; }
|
|
966
|
+
h1 { font-size: 4rem; margin: 0; color: #4f46e5; }
|
|
967
|
+
p { color: #888; font-size: 1.2rem; max-width: 400px; margin-top: 10px; }
|
|
968
|
+
.btn { margin-top: 30px; padding: 10px 25px; border: 1px solid #333; border-radius: 12px; color: #fff; cursor: pointer; background: #111; transition: all 0.2s; }
|
|
969
|
+
.btn:hover { border-color: #4f46e5; background: #4f46e51a; }
|
|
970
|
+
</style>
|
|
971
|
+
</head>
|
|
972
|
+
<body>
|
|
973
|
+
<h1>404</h1>
|
|
974
|
+
<p><strong>${game.name}</strong> could not be loaded.<br><span style="font-size: 0.9rem; opacity: 0.7;">${errorMsg}</span></p>
|
|
975
|
+
<div class="btn" onclick="window.parent.closeZoneViewer()">Return to Hub</div>
|
|
976
|
+
</body>
|
|
977
|
+
</html>
|
|
978
|
+
`);
|
|
979
|
+
doc.close();
|
|
980
|
+
zoneViewer.style.display = "flex";
|
|
981
|
+
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// Setup Download Button (Only for Eaglercraft)
|
|
855
985
|
if (game.baseGameId === 'other-eaglercraft' || game.id === 'other-eaglercraft') {
|
|
856
986
|
downloadBtn.href = game.url;
|
|
857
987
|
downloadBtn.download = game.name.replace(/ /g, '_') + '.html';
|
|
858
|
-
downloadBtn.removeAttribute('target');
|
|
859
988
|
downloadBtn.classList.remove('hidden');
|
|
860
989
|
} else {
|
|
861
990
|
downloadBtn.classList.add('hidden');
|
|
862
|
-
downloadBtn.href = '#';
|
|
863
|
-
downloadBtn.removeAttribute('download');
|
|
864
991
|
}
|
|
865
992
|
|
|
866
993
|
// --- INJECT FAVORITE BUTTON IN PLAYER HEADER ---
|
|
867
|
-
// Remove existing if any
|
|
868
994
|
const existingFav = controls.querySelector('.player-fav-btn');
|
|
869
995
|
if (existingFav) existingFav.remove();
|
|
870
996
|
|
|
@@ -879,30 +1005,47 @@
|
|
|
879
1005
|
toggleFavorite(game.id);
|
|
880
1006
|
favBtn.classList.toggle('favorited');
|
|
881
1007
|
};
|
|
882
|
-
|
|
883
|
-
// Prepend to controls (Left side)
|
|
884
1008
|
controls.prepend(favBtn);
|
|
885
1009
|
|
|
886
1010
|
zoneNameEl.textContent = game.name;
|
|
887
|
-
|
|
888
|
-
// RESET FRAME BEFORE LOAD
|
|
889
1011
|
zoneFrame.src = 'about:blank';
|
|
890
1012
|
|
|
891
|
-
// SECURITY: Set sandbox permissions
|
|
892
1013
|
const sandboxRules = 'allow-scripts allow-same-origin allow-forms allow-pointer-lock';
|
|
893
1014
|
zoneFrame.setAttribute('sandbox', sandboxRules);
|
|
894
1015
|
zoneFrame.setAttribute('allow', 'fullscreen; pointer-lock; autoplay; clipboard-write');
|
|
895
1016
|
|
|
896
|
-
// ---
|
|
1017
|
+
// --- CHECK FOR OFFLINE VERSION FIRST ---
|
|
1018
|
+
const offlineGame = await getOfflineGame(game.id);
|
|
1019
|
+
if (offlineGame) {
|
|
1020
|
+
console.log("Loading offline version of", game.name);
|
|
1021
|
+
const doc = zoneFrame.contentWindow.document;
|
|
1022
|
+
doc.open();
|
|
1023
|
+
doc.write(offlineGame.html);
|
|
1024
|
+
doc.close();
|
|
1025
|
+
zoneViewer.style.display = "flex";
|
|
1026
|
+
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// --- ONLINE LOAD LOGIC ---
|
|
1031
|
+
if (!navigator.onLine) {
|
|
1032
|
+
show404("You are offline and this game isn't saved.");
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
897
1036
|
const isStandardURLGame = game.category === 'StrongdogXP' || game.category === 'Others' || game.category === 'GN-Math';
|
|
898
1037
|
|
|
899
1038
|
if (isStandardURLGame) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1039
|
+
// Try fetching to see if it exists (Fixes the jsdelivr redirect bug)
|
|
1040
|
+
try {
|
|
1041
|
+
const check = await fetch(game.url, { method: 'HEAD' });
|
|
1042
|
+
if (!check.ok) throw new Error("File not found");
|
|
1043
|
+
zoneFrame.src = game.url;
|
|
1044
|
+
zoneViewer.style.display = "flex";
|
|
1045
|
+
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
show404("The game source is broken or blocked.");
|
|
1048
|
+
}
|
|
906
1049
|
} else {
|
|
907
1050
|
fetch(`${game.url}?t=${Date.now()}`)
|
|
908
1051
|
.then(response => {
|
|
@@ -914,23 +1057,43 @@
|
|
|
914
1057
|
doc.open();
|
|
915
1058
|
doc.write(html);
|
|
916
1059
|
doc.close();
|
|
917
|
-
|
|
918
|
-
// SHOW WITH ANIMATION
|
|
919
1060
|
zoneViewer.style.display = "flex";
|
|
920
|
-
zoneViewer.classList.remove('animate-fade-out');
|
|
921
1061
|
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
922
1062
|
})
|
|
923
1063
|
.catch(error => {
|
|
924
|
-
|
|
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');
|
|
1064
|
+
show404(error.message);
|
|
930
1065
|
});
|
|
931
1066
|
}
|
|
932
1067
|
}
|
|
933
1068
|
|
|
1069
|
+
async function toggleOfflineMode(gameId, name, url, buttonElement) {
|
|
1070
|
+
const isSaved = await isGameOffline(gameId);
|
|
1071
|
+
try {
|
|
1072
|
+
if (isSaved) {
|
|
1073
|
+
await removeOfflineGame(gameId);
|
|
1074
|
+
showNotification(`${name} removed from offline storage.`, 'fa-solid fa-trash', 'info');
|
|
1075
|
+
} else {
|
|
1076
|
+
buttonElement.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
|
1077
|
+
await saveGameOffline(gameId, name, url);
|
|
1078
|
+
showNotification(`${name} saved for offline play!`, 'fa-solid fa-check-circle', 'success');
|
|
1079
|
+
}
|
|
1080
|
+
updateAllOfflineButtons();
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
showNotification(`Failed to save ${name}.`, 'fa-solid fa-circle-exclamation', 'warning');
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function updateAllOfflineButtons() {
|
|
1087
|
+
const btns = document.querySelectorAll('.offline-action');
|
|
1088
|
+
for (const btn of btns) {
|
|
1089
|
+
const gameId = btn.dataset.gameId;
|
|
1090
|
+
const isSaved = await isGameOffline(gameId);
|
|
1091
|
+
btn.classList.toggle('favorited', isSaved); // reuse favorited color
|
|
1092
|
+
btn.innerHTML = isSaved ? '<i class="fa-solid fa-cloud-arrow-down text-green-400"></i>' : '<i class="fa-solid fa-cloud"></i>';
|
|
1093
|
+
btn.title = isSaved ? 'Disable Offline Mode' : 'Enable Offline Mode';
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
934
1097
|
function closeZoneViewer() {
|
|
935
1098
|
const zoneViewer = document.getElementById('zoneViewer');
|
|
936
1099
|
const zoneFrame = document.getElementById('zoneFrame');
|
|
@@ -1065,7 +1228,7 @@
|
|
|
1065
1228
|
</div>
|
|
1066
1229
|
</div>
|
|
1067
1230
|
<div class="flex items-center flex-shrink-0">
|
|
1068
|
-
<div class="bg-black/
|
|
1231
|
+
<div class="bg-black/50 backdrop-blur-sm rounded-2xl p-1.5 flex items-center gap-1.5">
|
|
1069
1232
|
<button class="btn-card-action play-action" title="Play Game">
|
|
1070
1233
|
<i class="fa-solid fa-play transition-colors"></i>
|
|
1071
1234
|
</button>
|
|
@@ -1196,6 +1359,7 @@
|
|
|
1196
1359
|
|
|
1197
1360
|
switchCategory(currentCategoryIndex);
|
|
1198
1361
|
renderFavorites();
|
|
1362
|
+
updateAllOfflineButtons();
|
|
1199
1363
|
setupInstructionOverlay();
|
|
1200
1364
|
|
|
1201
1365
|
searchInput.placeholder = `Search all games...`;
|