4sp-dv 1.0.42 → 1.0.43
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 +2 -34
- package/logged-in/games.html +29 -133
- package/package.json +1 -1
package/4simpleproblems_v5.html
CHANGED
|
@@ -1189,8 +1189,7 @@
|
|
|
1189
1189
|
pfpLetterBg: data.pfpLetterBg,
|
|
1190
1190
|
letterAvatarText: data.letterAvatarText,
|
|
1191
1191
|
letterAvatarTextColor: data.letterAvatarTextColor, // New Field
|
|
1192
|
-
navbarTheme: data.navbarTheme
|
|
1193
|
-
verified_online: true // Mark as verified for offline use
|
|
1192
|
+
navbarTheme: data.navbarTheme // Sync theme if updated remotely
|
|
1194
1193
|
};
|
|
1195
1194
|
|
|
1196
1195
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(newUserData));
|
|
@@ -1281,26 +1280,6 @@
|
|
|
1281
1280
|
unlockBtn.classList.add('hidden');
|
|
1282
1281
|
codeInput.classList.add('hidden');
|
|
1283
1282
|
|
|
1284
|
-
// --- OFFLINE CHECK ---
|
|
1285
|
-
if (!navigator.onLine) {
|
|
1286
|
-
console.log("Client is offline. Checking for bypass...");
|
|
1287
|
-
let cachedUser = null;
|
|
1288
|
-
try { cachedUser = JSON.parse(localStorage.getItem(USER_DATA_KEY)); } catch(e){}
|
|
1289
|
-
|
|
1290
|
-
if (cachedUser && cachedUser.verified_online) {
|
|
1291
|
-
console.log("Offline bypass granted for verified user.");
|
|
1292
|
-
currentUser = cachedUser;
|
|
1293
|
-
updateUIWithUser(cachedUser);
|
|
1294
|
-
const indicator = document.getElementById('offline-indicator');
|
|
1295
|
-
if (indicator) indicator.classList.add('show');
|
|
1296
|
-
launchApp();
|
|
1297
|
-
return;
|
|
1298
|
-
} else {
|
|
1299
|
-
resetLockScreen("You are offline. Please connect to the internet to verify your code.");
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
1283
|
try {
|
|
1305
1284
|
const docRef = doc(db, "access_codes", savedCode);
|
|
1306
1285
|
const docSnap = await getDoc(docRef);
|
|
@@ -1332,18 +1311,7 @@
|
|
|
1332
1311
|
}
|
|
1333
1312
|
} catch(e) {
|
|
1334
1313
|
console.error("Auto-login error:", e);
|
|
1335
|
-
|
|
1336
|
-
let cachedUser = null;
|
|
1337
|
-
try { cachedUser = JSON.parse(localStorage.getItem(USER_DATA_KEY)); } catch(err){}
|
|
1338
|
-
if (cachedUser && cachedUser.verified_online) {
|
|
1339
|
-
currentUser = cachedUser;
|
|
1340
|
-
updateUIWithUser(cachedUser);
|
|
1341
|
-
const indicator = document.getElementById('offline-indicator');
|
|
1342
|
-
if (indicator) indicator.classList.add('show');
|
|
1343
|
-
launchApp();
|
|
1344
|
-
} else {
|
|
1345
|
-
resetLockScreen("Connection failed. Retrying...");
|
|
1346
|
-
}
|
|
1314
|
+
resetLockScreen("Connection failed. Retrying...");
|
|
1347
1315
|
}
|
|
1348
1316
|
}
|
|
1349
1317
|
}
|
package/logged-in/games.html
CHANGED
|
@@ -709,71 +709,10 @@
|
|
|
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" }
|
|
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" }
|
|
713
714
|
];
|
|
714
715
|
|
|
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
|
-
const db = await openDB();
|
|
740
|
-
return new Promise((resolve, reject) => {
|
|
741
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
742
|
-
const store = tx.objectStore(STORE_NAME);
|
|
743
|
-
store.put({ id: String(gameId), name, html, timestamp: Date.now() });
|
|
744
|
-
tx.oncomplete = () => { db.close(); resolve(); };
|
|
745
|
-
tx.onerror = () => reject(tx.error);
|
|
746
|
-
});
|
|
747
|
-
} catch (e) { console.error("Offline Save Error:", e); throw e; }
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
async function getOfflineGame(gameId) {
|
|
751
|
-
const db = await openDB();
|
|
752
|
-
return new Promise((resolve, reject) => {
|
|
753
|
-
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
754
|
-
const store = tx.objectStore(STORE_NAME);
|
|
755
|
-
const request = store.get(String(gameId));
|
|
756
|
-
request.onsuccess = () => { db.close(); resolve(request.result); };
|
|
757
|
-
request.onerror = () => reject(request.error);
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
async function removeOfflineGame(gameId) {
|
|
762
|
-
const db = await openDB();
|
|
763
|
-
return new Promise((resolve, reject) => {
|
|
764
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
765
|
-
const store = tx.objectStore(STORE_NAME);
|
|
766
|
-
store.delete(String(gameId));
|
|
767
|
-
tx.oncomplete = () => { db.close(); resolve(); };
|
|
768
|
-
tx.onerror = () => reject(tx.error);
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
async function isGameOffline(gameId) {
|
|
773
|
-
const game = await getOfflineGame(gameId);
|
|
774
|
-
return !!game;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
716
|
// --- Card Creation ---
|
|
778
717
|
|
|
779
718
|
function createOthersGameCard(game) {
|
|
@@ -851,7 +790,7 @@
|
|
|
851
790
|
|
|
852
791
|
function createGameCard(game, forceSmallCard = false) {
|
|
853
792
|
if (game.category === 'Others' && !forceSmallCard) return createOthersGameCard(game);
|
|
854
|
-
const isStrongdog = game.category === 'StrongdogXP'
|
|
793
|
+
const isStrongdog = game.category === 'StrongdogXP';
|
|
855
794
|
|
|
856
795
|
// Resolve Image Src
|
|
857
796
|
let imgSrc = game.imgSrc;
|
|
@@ -866,25 +805,12 @@
|
|
|
866
805
|
const card = document.createElement("div");
|
|
867
806
|
card.className = 'zone-item bg-card-dark rounded-2xl border border-brand-border overflow-hidden';
|
|
868
807
|
card.dataset.gameId = game.id;
|
|
869
|
-
|
|
870
|
-
let gUrl = game.url;
|
|
871
|
-
if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
872
|
-
gUrl = resolveGameUrl(game.url);
|
|
873
|
-
} else if (isStrongdog) {
|
|
874
|
-
const baseURL = sd_getBaseURLForPage(game.page);
|
|
875
|
-
gUrl = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// ONLY GN-MATH GETS OFFLINE SAVING
|
|
879
|
-
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>` : '';
|
|
880
|
-
|
|
881
808
|
card.innerHTML = `
|
|
882
809
|
<div class="relative w-full cursor-pointer group">
|
|
883
810
|
<div class="aspect-w-3 aspect-h-2"><img data-src="${imgSrc}" alt="${game.name}" class="w-full h-full object-cover"></div>
|
|
884
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>
|
|
885
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">
|
|
886
|
-
<button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play"></i></button>
|
|
887
|
-
${offlineBtn}
|
|
813
|
+
<button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play transition-colors"></i></button>
|
|
888
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>
|
|
889
815
|
</div>
|
|
890
816
|
</div>
|
|
@@ -898,6 +824,11 @@
|
|
|
898
824
|
const embedPath = await sd_getEmbedPath(adjustedHref, game.href, game.page);
|
|
899
825
|
openZone({ name: game.name, url: embedPath, category: game.category, id: game.id });
|
|
900
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
|
+
}
|
|
901
832
|
openZone({ ...game, url: gUrl });
|
|
902
833
|
}
|
|
903
834
|
};
|
|
@@ -905,12 +836,6 @@
|
|
|
905
836
|
card.querySelector('.group').addEventListener('click', handlePlay);
|
|
906
837
|
card.querySelector('.play-action').addEventListener('click', handlePlay);
|
|
907
838
|
card.querySelector('.fav-action').addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); });
|
|
908
|
-
|
|
909
|
-
if (isGNMath) {
|
|
910
|
-
const offBtn = card.querySelector('.offline-action');
|
|
911
|
-
offBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleOfflineMode(game.id, game.name, gUrl, offBtn); });
|
|
912
|
-
}
|
|
913
|
-
|
|
914
839
|
imageObserver.observe(card.querySelector('img'));
|
|
915
840
|
return card;
|
|
916
841
|
}
|
|
@@ -930,7 +855,7 @@
|
|
|
930
855
|
const show404 = (errorMsg = "Game content unavailable.") => {
|
|
931
856
|
const doc = zoneFrame.contentWindow.document;
|
|
932
857
|
doc.open();
|
|
933
|
-
doc.write(
|
|
858
|
+
doc.write(`
|
|
934
859
|
<!DOCTYPE html>
|
|
935
860
|
<html>
|
|
936
861
|
<head>
|
|
@@ -945,11 +870,11 @@
|
|
|
945
870
|
</head>
|
|
946
871
|
<body>
|
|
947
872
|
<h1>404</h1>
|
|
948
|
-
<p><strong
|
|
873
|
+
<p><strong>${game.name}</strong> could not be loaded.<br><span style="font-size: 0.9rem; opacity: 0.7;">${errorMsg}</span></p>
|
|
949
874
|
<div class="btn" onclick="window.parent.closeZoneViewer()">Return to Hub</div>
|
|
950
875
|
</body>
|
|
951
876
|
</html>
|
|
952
|
-
|
|
877
|
+
`);
|
|
953
878
|
doc.close();
|
|
954
879
|
zoneViewer.style.display = "flex";
|
|
955
880
|
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
@@ -959,12 +884,16 @@
|
|
|
959
884
|
if (game.baseGameId === 'other-eaglercraft' || game.id === 'other-eaglercraft') {
|
|
960
885
|
downloadBtn.href = game.url;
|
|
961
886
|
downloadBtn.download = game.name.replace(/ /g, '_') + '.html';
|
|
887
|
+
downloadBtn.removeAttribute('target');
|
|
962
888
|
downloadBtn.classList.remove('hidden');
|
|
963
889
|
} else {
|
|
964
890
|
downloadBtn.classList.add('hidden');
|
|
891
|
+
downloadBtn.href = '#';
|
|
892
|
+
downloadBtn.removeAttribute('download');
|
|
965
893
|
}
|
|
966
894
|
|
|
967
895
|
// --- INJECT FAVORITE BUTTON IN PLAYER HEADER ---
|
|
896
|
+
// Remove existing if any
|
|
968
897
|
const existingFav = controls.querySelector('.player-fav-btn');
|
|
969
898
|
if (existingFav) existingFav.remove();
|
|
970
899
|
|
|
@@ -979,28 +908,21 @@
|
|
|
979
908
|
toggleFavorite(game.id);
|
|
980
909
|
favBtn.classList.toggle('favorited');
|
|
981
910
|
};
|
|
911
|
+
|
|
912
|
+
// Prepend to controls (Left side)
|
|
982
913
|
controls.prepend(favBtn);
|
|
983
914
|
|
|
984
915
|
zoneNameEl.textContent = game.name;
|
|
916
|
+
|
|
917
|
+
// RESET FRAME BEFORE LOAD
|
|
985
918
|
zoneFrame.src = 'about:blank';
|
|
986
919
|
|
|
920
|
+
// SECURITY: Set sandbox permissions
|
|
987
921
|
const sandboxRules = 'allow-scripts allow-same-origin allow-forms allow-pointer-lock';
|
|
988
922
|
zoneFrame.setAttribute('sandbox', sandboxRules);
|
|
989
923
|
zoneFrame.setAttribute('allow', 'fullscreen; pointer-lock; autoplay; clipboard-write');
|
|
990
924
|
|
|
991
|
-
// ---
|
|
992
|
-
const offlineGame = await getOfflineGame(game.id);
|
|
993
|
-
if (offlineGame) {
|
|
994
|
-
console.log("Loading offline version of", game.name);
|
|
995
|
-
const doc = zoneFrame.contentWindow.document;
|
|
996
|
-
doc.open();
|
|
997
|
-
doc.write(offlineGame.html);
|
|
998
|
-
doc.close();
|
|
999
|
-
zoneViewer.style.display = "flex";
|
|
1000
|
-
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
925
|
+
// --- REVISED LOGIC FOR LOADING GAMES ---
|
|
1004
926
|
const isStandardURLGame = game.category === 'StrongdogXP' || game.category === 'Others' || game.category === 'GN-Math';
|
|
1005
927
|
|
|
1006
928
|
if (isStandardURLGame) {
|
|
@@ -1009,7 +931,10 @@
|
|
|
1009
931
|
const check = await fetch(game.url, { method: 'HEAD' });
|
|
1010
932
|
if (!check.ok) throw new Error("File not found");
|
|
1011
933
|
zoneFrame.src = game.url;
|
|
934
|
+
|
|
935
|
+
// SHOW WITH ANIMATION
|
|
1012
936
|
zoneViewer.style.display = "flex";
|
|
937
|
+
zoneViewer.classList.remove('animate-fade-out');
|
|
1013
938
|
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
1014
939
|
} catch (e) {
|
|
1015
940
|
show404("The game source is broken or blocked.");
|
|
@@ -1025,46 +950,18 @@
|
|
|
1025
950
|
doc.open();
|
|
1026
951
|
doc.write(html);
|
|
1027
952
|
doc.close();
|
|
953
|
+
|
|
954
|
+
// SHOW WITH ANIMATION
|
|
1028
955
|
zoneViewer.style.display = "flex";
|
|
956
|
+
zoneViewer.classList.remove('animate-fade-out');
|
|
1029
957
|
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
1030
958
|
})
|
|
1031
959
|
.catch(error => {
|
|
1032
|
-
|
|
1033
|
-
zoneFrame.src = game.url;
|
|
1034
|
-
zoneViewer.style.display = "flex";
|
|
1035
|
-
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
960
|
+
show404(error.message);
|
|
1036
961
|
});
|
|
1037
962
|
}
|
|
1038
963
|
}
|
|
1039
964
|
|
|
1040
|
-
async function toggleOfflineMode(gameId, name, url, buttonElement) {
|
|
1041
|
-
const isSaved = await isGameOffline(gameId);
|
|
1042
|
-
try {
|
|
1043
|
-
if (isSaved) {
|
|
1044
|
-
await removeOfflineGame(gameId);
|
|
1045
|
-
showNotification(\`\${name} removed from storage.\`, 'fa-solid fa-trash', 'info');
|
|
1046
|
-
} else {
|
|
1047
|
-
buttonElement.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
|
1048
|
-
await saveGameOffline(gameId, name, url);
|
|
1049
|
-
showNotification(\`\${name} saved for offline play!\`, 'fa-solid fa-check-circle', 'success');
|
|
1050
|
-
}
|
|
1051
|
-
updateAllOfflineButtons();
|
|
1052
|
-
} catch (e) {
|
|
1053
|
-
showNotification(\`Failed to save \${name}.\`, 'fa-solid fa-circle-exclamation', 'warning');
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
async function updateAllOfflineButtons() {
|
|
1058
|
-
const btns = document.querySelectorAll('.offline-action');
|
|
1059
|
-
for (const btn of btns) {
|
|
1060
|
-
const gameId = btn.dataset.gameId;
|
|
1061
|
-
const isSaved = await isGameOffline(gameId);
|
|
1062
|
-
btn.classList.toggle('favorited', isSaved); // reuse favorited color
|
|
1063
|
-
btn.innerHTML = isSaved ? '<i class="fa-solid fa-cloud-arrow-down text-green-400"></i>' : '<i class="fa-solid fa-cloud"></i>';
|
|
1064
|
-
btn.title = isSaved ? 'Disable Offline Mode' : 'Enable Offline Mode';
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
965
|
function closeZoneViewer() {
|
|
1069
966
|
const zoneViewer = document.getElementById('zoneViewer');
|
|
1070
967
|
const zoneFrame = document.getElementById('zoneFrame');
|
|
@@ -1330,7 +1227,6 @@
|
|
|
1330
1227
|
|
|
1331
1228
|
switchCategory(currentCategoryIndex);
|
|
1332
1229
|
renderFavorites();
|
|
1333
|
-
updateAllOfflineButtons();
|
|
1334
1230
|
setupInstructionOverlay();
|
|
1335
1231
|
|
|
1336
1232
|
searchInput.placeholder = `Search all games...`;
|