another set of updates
This commit is contained in:
@@ -2406,10 +2406,14 @@
|
|||||||
|
|
||||||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||||
|
|
||||||
|
// --- TUNNEL / BACKEND STATE ---
|
||||||
|
let backendDown = false;
|
||||||
|
let tunnelPollTimer = null;
|
||||||
|
|
||||||
async function checkServerStatus(ip, isDeployed = false) {
|
async function checkServerStatus(ip, isDeployed = false) {
|
||||||
statusIndicator.textContent = "Connecting...";
|
statusIndicator.textContent = "Connecting...";
|
||||||
statusIndicator.className = "status-indicator connecting";
|
statusIndicator.className = "status-indicator connecting";
|
||||||
const url = isDeployed ? "/identify" : `https://{ip}:7275/identify`;
|
const url = isDeployed ? "/identify" : `https://${ip}:7275/identify`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -2430,11 +2434,8 @@
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
serverModal.classList.remove("active");
|
serverModal.classList.remove("active");
|
||||||
}, 1000);
|
}, 1000);
|
||||||
// Check tunnel health
|
// Return tunnel state alongside connected flag
|
||||||
if (data.tunnel_active === false) {
|
return { connected: true, tunnelActive: data.tunnel_active !== false };
|
||||||
setTimeout(() => showTunnelWarning(), 1200);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("Not a valid Animex server.");
|
throw new Error("Not a valid Animex server.");
|
||||||
@@ -2444,15 +2445,18 @@
|
|||||||
? "Connection to server failed. Please refresh."
|
? "Connection to server failed. Please refresh."
|
||||||
: "Connection failed. Check IP and ensure server is running.";
|
: "Connection failed. Check IP and ensure server is running.";
|
||||||
statusIndicator.className = "status-indicator error";
|
statusIndicator.className = "status-indicator error";
|
||||||
return false;
|
return { connected: false, tunnelActive: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectBtn.addEventListener("click", async () => {
|
connectBtn.addEventListener("click", async () => {
|
||||||
const ip = ipInput.value.trim();
|
const ip = ipInput.value.trim();
|
||||||
if (ip) {
|
if (ip) {
|
||||||
const connected = await checkServerStatus(ip, false);
|
const result = await checkServerStatus(ip, false);
|
||||||
if (connected) {
|
if (result.connected) {
|
||||||
|
if (!result.tunnelActive) {
|
||||||
|
setTimeout(() => showTunnelWarning(), 1200);
|
||||||
|
}
|
||||||
proceedToApp();
|
proceedToApp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2462,12 +2466,83 @@
|
|||||||
const modal = document.getElementById("tunnel-warning-modal");
|
const modal = document.getElementById("tunnel-warning-modal");
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
modal.classList.add("active");
|
modal.classList.add("active");
|
||||||
document.getElementById("tunnel-refresh-btn").addEventListener("click", () => {
|
|
||||||
|
const refreshBtn = document.getElementById("tunnel-refresh-btn");
|
||||||
|
const dismissBtn = document.getElementById("tunnel-dismiss-btn");
|
||||||
|
|
||||||
|
// Use { once: true } to avoid stacking listeners on repeated calls
|
||||||
|
refreshBtn.addEventListener("click", () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
}, { once: true });
|
||||||
document.getElementById("tunnel-dismiss-btn").addEventListener("click", () => {
|
|
||||||
|
dismissBtn.addEventListener("click", () => {
|
||||||
modal.classList.remove("active");
|
modal.classList.remove("active");
|
||||||
|
// User acknowledged the backend is down — set state and begin polling
|
||||||
|
backendDown = true;
|
||||||
|
startTunnelPolling();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TUNNEL POLLING ---
|
||||||
|
function startTunnelPolling() {
|
||||||
|
if (tunnelPollTimer) return; // already running
|
||||||
|
const POLL_INTERVAL = 7000; // 7 seconds
|
||||||
|
const isDeployed = window.location.protocol.startsWith("http");
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
try {
|
||||||
|
const url = isDeployed
|
||||||
|
? "/identify"
|
||||||
|
: `https://${localStorage.getItem("extension_server_ip")}:7275/identify`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
mode: isDeployed ? "same-origin" : "cors",
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const tunnelNowActive = data.tunnel_active !== false;
|
||||||
|
|
||||||
|
if (backendDown && tunnelNowActive) {
|
||||||
|
// Tunnel came back up
|
||||||
|
backendDown = false;
|
||||||
|
stopTunnelPolling();
|
||||||
|
showToast("Backend connection restored. All features are available.", "success", 5000);
|
||||||
|
} else if (!backendDown && !tunnelNowActive) {
|
||||||
|
// Tunnel went down while we were healthy
|
||||||
|
backendDown = true;
|
||||||
|
showToast("Backend connection lost. Some features may be unavailable.", "error", 6000);
|
||||||
|
// Keep polling so we catch recovery
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-OK response — treat as still down, no toast spam
|
||||||
|
if (!backendDown) {
|
||||||
|
backendDown = true;
|
||||||
|
showToast("Backend connection lost. Some features may be unavailable.", "error", 6000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fetch failed — treat as down
|
||||||
|
if (!backendDown) {
|
||||||
|
backendDown = true;
|
||||||
|
showToast("Backend connection lost. Some features may be unavailable.", "error", 6000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule next poll only if still needed
|
||||||
|
if (tunnelPollTimer !== null) {
|
||||||
|
tunnelPollTimer = setTimeout(poll, POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelPollTimer = setTimeout(poll, POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTunnelPolling() {
|
||||||
|
if (tunnelPollTimer) {
|
||||||
|
clearTimeout(tunnelPollTimer);
|
||||||
|
tunnelPollTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initApp() {
|
async function initApp() {
|
||||||
@@ -2476,23 +2551,32 @@
|
|||||||
startupStatus.classList.add("visible");
|
startupStatus.classList.add("visible");
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
|
|
||||||
let connected = false;
|
// Reset tunnel state fresh on every page load
|
||||||
|
backendDown = false;
|
||||||
|
stopTunnelPolling();
|
||||||
|
|
||||||
|
let result = { connected: false, tunnelActive: false };
|
||||||
if (isDeployed) {
|
if (isDeployed) {
|
||||||
connected = await checkServerStatus(null, true);
|
result = await checkServerStatus(null, true);
|
||||||
} else {
|
} else {
|
||||||
const savedIp = localStorage.getItem("extension_server_ip");
|
const savedIp = localStorage.getItem("extension_server_ip");
|
||||||
if (savedIp) {
|
if (savedIp) {
|
||||||
connected = await checkServerStatus(savedIp, false);
|
result = await checkServerStatus(savedIp, false);
|
||||||
} else {
|
|
||||||
connected = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connected) {
|
if (result.connected) {
|
||||||
startupStatusText.textContent = "Connection Found";
|
startupStatusText.textContent = "Connection Found";
|
||||||
startupStatusDot.classList.remove("active");
|
startupStatusDot.classList.remove("active");
|
||||||
startupStatusDot.classList.add("success");
|
startupStatusDot.classList.add("success");
|
||||||
await delay(500);
|
await delay(500);
|
||||||
|
|
||||||
|
if (!result.tunnelActive) {
|
||||||
|
// Tunnel is down on initial load — show warning; polling starts when user dismisses
|
||||||
|
setTimeout(() => showTunnelWarning(), 1200);
|
||||||
|
}
|
||||||
|
// backendDown stays false until user clicks "Continue Anyway"
|
||||||
|
|
||||||
proceedToApp();
|
proceedToApp();
|
||||||
} else {
|
} else {
|
||||||
startupStatusText.textContent = "Connection Failed";
|
startupStatusText.textContent = "Connection Failed";
|
||||||
|
|||||||
@@ -74,9 +74,9 @@
|
|||||||
#desktop-hero {
|
#desktop-hero {
|
||||||
display: none;
|
display: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 90vw; /* Hero div now spans 90% of the viewport width */
|
||||||
height: 580px;
|
height: 580px;
|
||||||
margin: 24px 0 56px;
|
margin: 24px auto 56px; /* Centered horizontally with auto margins */
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
@@ -538,8 +538,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="app-container">
|
|
||||||
|
|
||||||
<!-- Desktop Hero (shown on 1024px+) -->
|
<!-- Desktop Hero (shown on 1024px+) -->
|
||||||
<div id="desktop-hero">
|
<div id="desktop-hero">
|
||||||
<img class="hero-bg-image" id="hero-bg" src="" alt="" />
|
<img class="hero-bg-image" id="hero-bg" src="" alt="" />
|
||||||
@@ -562,6 +560,8 @@
|
|||||||
<div class="hero-indicators" id="hero-indicators"></div>
|
<div class="hero-indicators" id="hero-indicators"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="app-container">
|
||||||
|
|
||||||
<!-- Jikan Content (carousel on mobile, hero replaces on desktop) -->
|
<!-- Jikan Content (carousel on mobile, hero replaces on desktop) -->
|
||||||
<section class="content-section" id="jikan-content">
|
<section class="content-section" id="jikan-content">
|
||||||
<!-- Mobile Carousel -->
|
<!-- Mobile Carousel -->
|
||||||
|
|||||||
@@ -1767,10 +1767,23 @@ iframe { width: 100%; height: 100%; border: none; }
|
|||||||
for (let i = 1; i <= epCount; i++) {
|
for (let i = 1; i <= epCount; i++) {
|
||||||
episodes.push({ attributes: { number: i, canonicalTitle: `Episode ${i}`, thumbnail: null } });
|
episodes.push({ attributes: { number: i, canonicalTitle: `Episode ${i}`, thumbnail: null } });
|
||||||
}
|
}
|
||||||
hideThumbnails = true; // Force hide broken placeholders
|
// hideThumbnails = true; // Force hide broken placeholders - Removed for new auto-toggle logic
|
||||||
}
|
}
|
||||||
|
|
||||||
episodesData = episodes.sort((a, b) => a.attributes.number - b.attributes.number);
|
episodesData = episodes.sort((a, b) => a.attributes.number - b.attributes.number);
|
||||||
|
|
||||||
|
// Auto-toggle thumbnail view (unless user prefers it off)
|
||||||
|
const userPrefSet = localStorage.getItem("hideThumbnails");
|
||||||
|
const hasThumbnails = episodesData.some(ep => ep.attributes.thumbnail?.original);
|
||||||
|
|
||||||
|
if (userPrefSet === null) { // User hasn't set a preference yet
|
||||||
|
hideThumbnails = !hasThumbnails; // If no thumbnails, hide them by default
|
||||||
|
localStorage.setItem("hideThumbnails", hideThumbnails); // Save this initial auto-toggle state
|
||||||
|
} else if (!hasThumbnails && hideThumbnails === false) { // User prefers thumbnails ON, but none are available
|
||||||
|
hideThumbnails = true; // Force hide if no thumbnails exist
|
||||||
|
localStorage.setItem("hideThumbnails", true); // Persist this force-hide state
|
||||||
|
}
|
||||||
|
|
||||||
renderEpisodes();
|
renderEpisodes();
|
||||||
setupLocateButton();
|
setupLocateButton();
|
||||||
}
|
}
|
||||||
@@ -2011,6 +2024,8 @@ iframe { width: 100%; height: 100%; border: none; }
|
|||||||
const url = `view.html?id=${currentMalId}&ep=${ep}`;
|
const url = `view.html?id=${currentMalId}&ep=${ep}`;
|
||||||
document.getElementById("episode-popup-iframe").src = url;
|
document.getElementById("episode-popup-iframe").src = url;
|
||||||
document.getElementById("episode-popup-overlay").classList.add("active");
|
document.getElementById("episode-popup-overlay").classList.add("active");
|
||||||
|
document.body.style.overflow = "hidden"; // Disable body scrolling when modal is open
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePlayerPopup() {
|
function closePlayerPopup() {
|
||||||
@@ -2020,6 +2035,7 @@ iframe { width: 100%; height: 100%; border: none; }
|
|||||||
localWatchHistory = JSON.parse(localStorage.getItem("animex_watch_history") || "{}");
|
localWatchHistory = JSON.parse(localStorage.getItem("animex_watch_history") || "{}");
|
||||||
renderEpisodes();
|
renderEpisodes();
|
||||||
setupLocateButton();
|
setupLocateButton();
|
||||||
|
document.body.style.overflow = ""; // Re-enable body scrolling when modal is closed
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PHASE 1: MODERNIZED RX GATE ---
|
// --- PHASE 1: MODERNIZED RX GATE ---
|
||||||
|
|||||||
@@ -771,6 +771,38 @@
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Source connectivity probe ────────────────────────────────
|
||||||
|
// Attempt to reach the player/source URL directly once.
|
||||||
|
// If it's reachable → load as-is. If blocked → wrap in proxy.
|
||||||
|
let _srcProbeCache = {}; // keyed by origin, value: true (direct) | false (proxy)
|
||||||
|
|
||||||
|
async function probeSourceUrl(src) {
|
||||||
|
try {
|
||||||
|
const origin = new URL(src).origin;
|
||||||
|
if (_srcProbeCache[origin] !== undefined) return _srcProbeCache[origin];
|
||||||
|
|
||||||
|
const ok = await new Promise(resolve => {
|
||||||
|
const img = new Image();
|
||||||
|
const timer = setTimeout(() => { img.src = ""; resolve(false); }, 6000);
|
||||||
|
// Use a tiny pixel endpoint at the same origin if it exists, else probe the src itself
|
||||||
|
img.onload = () => { clearTimeout(timer); resolve(true); };
|
||||||
|
img.onerror = () => { clearTimeout(timer); resolve(false); };
|
||||||
|
img.src = src + (src.includes("?") ? "&" : "?") + "_probe=" + Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
_srcProbeCache[origin] = ok;
|
||||||
|
console.log(`[view] source probe for ${origin}: ${ok ? "✓ direct" : "✗ using proxy"}`);
|
||||||
|
return ok;
|
||||||
|
} catch (e) {
|
||||||
|
return true; // can't parse URL — just try direct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxySrc(src) {
|
||||||
|
return `/proxy-image?url=${encodeURIComponent(src)}`;
|
||||||
|
}
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function loadPlayer(dub, module) {
|
async function loadPlayer(dub, module) {
|
||||||
DOM.loading.style.display = "flex";
|
DOM.loading.style.display = "flex";
|
||||||
DOM.loading.style.opacity = "1";
|
DOM.loading.style.opacity = "1";
|
||||||
@@ -783,7 +815,11 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.src) throw new Error("No source found");
|
if (!data.src) throw new Error("No source found");
|
||||||
|
|
||||||
DOM.player.innerHTML = `<iframe src="${data.src}" allowfullscreen sandbox="allow-scripts allow-same-origin allow-presentation"></iframe>`;
|
// ── Probe once: can we reach the source directly? ──
|
||||||
|
const directOk = await probeSourceUrl(data.src);
|
||||||
|
const finalSrc = directOk ? data.src : proxySrc(data.src);
|
||||||
|
|
||||||
|
DOM.player.innerHTML = `<iframe src="${finalSrc}" allowfullscreen sandbox="allow-scripts allow-same-origin allow-presentation"></iframe>`;
|
||||||
|
|
||||||
if (data.source_module) DOM.sourceSelect.value = data.source_module;
|
if (data.source_module) DOM.sourceSelect.value = data.source_module;
|
||||||
|
|
||||||
|
|||||||
@@ -1168,11 +1168,82 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// PROXY DETECTION — probe once, then route all images
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// null = unknown (probe pending), true = direct works, false = must proxy
|
||||||
|
let _directAccessOk = null;
|
||||||
|
// Queue of callbacks waiting on the probe result
|
||||||
|
let _probeCallbacks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run (or await) the one-time connectivity probe.
|
||||||
|
* Resolves with true (direct) or false (proxy fallback).
|
||||||
|
*/
|
||||||
|
async function probeDirectAccess(sampleUrl) {
|
||||||
|
if (_directAccessOk !== null) return _directAccessOk;
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
_probeCallbacks.push(resolve);
|
||||||
|
|
||||||
|
// Only the first caller kicks off the real probe
|
||||||
|
if (_probeCallbacks.length > 1) return;
|
||||||
|
|
||||||
|
const probe = new Image();
|
||||||
|
const timer = setTimeout(() => settle(false), 8000); // 8 s timeout
|
||||||
|
|
||||||
|
function settle(ok) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
_directAccessOk = ok;
|
||||||
|
console.log(`[reader] direct-access probe: ${ok ? "✓ direct" : "✗ using proxy"}`);
|
||||||
|
_probeCallbacks.forEach(cb => cb(ok));
|
||||||
|
_probeCallbacks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
probe.onload = () => settle(true);
|
||||||
|
probe.onerror = () => settle(false);
|
||||||
|
// Cache-bust so the browser actually fires a network request
|
||||||
|
probe.src = sampleUrl + (sampleUrl.includes("?") ? "&" : "?") + "_probe=" + Date.now();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function proxyUrl(src) {
|
function proxyUrl(src) {
|
||||||
if (!src || src.startsWith("/")) return src;
|
if (!src || src.startsWith("/")) return src;
|
||||||
return `${serverUrl}/proxy-image?url=${encodeURIComponent(src)}`;
|
return `${serverUrl}/proxy-image?url=${encodeURIComponent(src)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the URL to use for an image.
|
||||||
|
* Uses cached probe result — call after probeDirectAccess() has settled.
|
||||||
|
*/
|
||||||
|
function resolvedUrl(src) {
|
||||||
|
if (!src || src.startsWith("/")) return src;
|
||||||
|
return _directAccessOk ? src : proxyUrl(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach src to an img element with automatic proxy retry on error.
|
||||||
|
* @param {HTMLImageElement} img
|
||||||
|
* @param {string} rawSrc — the original (un-proxied) URL
|
||||||
|
*/
|
||||||
|
function setImgSrc(img, rawSrc) {
|
||||||
|
if (!rawSrc) return;
|
||||||
|
const direct = _directAccessOk !== false; // use direct if probe passed or still unknown
|
||||||
|
img.src = direct && !rawSrc.startsWith("/") ? rawSrc : proxyUrl(rawSrc);
|
||||||
|
img.dataset.rawSrc = rawSrc; // store original for retry
|
||||||
|
|
||||||
|
img.onerror = function retryWithProxy() {
|
||||||
|
img.onerror = null; // prevent infinite loop
|
||||||
|
const proxied = proxyUrl(rawSrc);
|
||||||
|
if (img.src !== proxied) {
|
||||||
|
console.warn("[reader] direct load failed, retrying via proxy:", rawSrc);
|
||||||
|
_directAccessOk = false; // cache result for future images
|
||||||
|
img.src = proxied;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Build the DOM skeleton for images (flat or spread-based). */
|
/** Build the DOM skeleton for images (flat or spread-based). */
|
||||||
function buildImageDOM() {
|
function buildImageDOM() {
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
@@ -1210,10 +1281,16 @@
|
|||||||
const allImgs = getAllImages();
|
const allImgs = getAllImages();
|
||||||
const toPreload = allImgs.slice(0, 4);
|
const toPreload = allImgs.slice(0, 4);
|
||||||
|
|
||||||
|
// ── ONE-TIME CONNECTIVITY PROBE ──
|
||||||
|
// Try the first raw image URL directly; fall back to proxy for everything if it fails.
|
||||||
|
if (imageLinks[0]) {
|
||||||
|
await probeDirectAccess(imageLinks[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Auto-guesser: detect tall (webtoon-style) images ──
|
// ── Auto-guesser: detect tall (webtoon-style) images ──
|
||||||
if (toPreload[0] && imageLinks[0]) {
|
if (toPreload[0] && imageLinks[0]) {
|
||||||
const tempImg = new Image();
|
const tempImg = new Image();
|
||||||
tempImg.src = proxyUrl(imageLinks[0]);
|
tempImg.src = resolvedUrl(imageLinks[0]);
|
||||||
await new Promise(r => {
|
await new Promise(r => {
|
||||||
tempImg.onload = () => {
|
tempImg.onload = () => {
|
||||||
const isTall = tempImg.height > tempImg.width * 2;
|
const isTall = tempImg.height > tempImg.width * 2;
|
||||||
@@ -1235,12 +1312,15 @@
|
|||||||
const imgs = getAllImages();
|
const imgs = getAllImages();
|
||||||
const firstFour = imgs.slice(0, 4);
|
const firstFour = imgs.slice(0, 4);
|
||||||
|
|
||||||
// Preload first 4 images
|
// Preload first 4 images (probe already settled, so setImgSrc picks the right path)
|
||||||
const promises = firstFour.map((img, idx) =>
|
const promises = firstFour.map((img, idx) =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
img.onload = resolve;
|
img.onload = resolve;
|
||||||
img.onerror = resolve;
|
// onerror handled inside setImgSrc (proxy retry); resolve after that too
|
||||||
img.src = proxyUrl(imageLinks[idx]);
|
const origOnerror = null;
|
||||||
|
img.addEventListener("load", resolve, { once: true });
|
||||||
|
img.addEventListener("error", resolve, { once: true });
|
||||||
|
setImgSrc(img, imageLinks[idx]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1276,9 +1356,10 @@
|
|||||||
footerLoader.classList.add("hidden");
|
footerLoader.classList.add("hidden");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
img.onload = done;
|
// Use resolved URL with automatic proxy-retry on individual failures
|
||||||
img.onerror = done;
|
img.addEventListener("load", done, { once: true });
|
||||||
img.src = proxyUrl(imageLinks[idx + 4]);
|
img.addEventListener("error", done, { once: true });
|
||||||
|
setImgSrc(img, imageLinks[idx + 4]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1290,12 +1371,13 @@
|
|||||||
function rebuildImageContainer() {
|
function rebuildImageContainer() {
|
||||||
if (imageLinks.length === 0) return;
|
if (imageLinks.length === 0) return;
|
||||||
|
|
||||||
const existingSrcs = getAllImages().map(img => img.src || "");
|
// Preserve original (raw) srcs so we don't double-proxy
|
||||||
|
const existingRawSrcs = getAllImages().map(img => img.dataset.rawSrc || img.src || "");
|
||||||
buildImageDOM();
|
buildImageDOM();
|
||||||
|
|
||||||
const newImgs = getAllImages();
|
const newImgs = getAllImages();
|
||||||
newImgs.forEach((img, i) => {
|
newImgs.forEach((img, i) => {
|
||||||
if (existingSrcs[i]) img.src = existingSrcs[i];
|
if (existingRawSrcs[i]) setImgSrc(img, existingRawSrcs[i]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore scroll position after rebuild
|
// Restore scroll position after rebuild
|
||||||
|
|||||||
Reference in New Issue
Block a user