diff --git a/animex/index.html b/animex/index.html index ac10b8d..00723f8 100644 --- a/animex/index.html +++ b/animex/index.html @@ -2406,10 +2406,14 @@ const delay = (ms) => new Promise((res) => setTimeout(res, ms)); + // --- TUNNEL / BACKEND STATE --- + let backendDown = false; + let tunnelPollTimer = null; + async function checkServerStatus(ip, isDeployed = false) { statusIndicator.textContent = "Connecting..."; statusIndicator.className = "status-indicator connecting"; - const url = isDeployed ? "/identify" : `https://{ip}:7275/identify`; + const url = isDeployed ? "/identify" : `https://${ip}:7275/identify`; try { const response = await fetch(url, { @@ -2430,11 +2434,8 @@ setTimeout(() => { serverModal.classList.remove("active"); }, 1000); - // Check tunnel health - if (data.tunnel_active === false) { - setTimeout(() => showTunnelWarning(), 1200); - } - return true; + // Return tunnel state alongside connected flag + return { connected: true, tunnelActive: data.tunnel_active !== false }; } } throw new Error("Not a valid Animex server."); @@ -2444,15 +2445,18 @@ ? "Connection to server failed. Please refresh." : "Connection failed. Check IP and ensure server is running."; statusIndicator.className = "status-indicator error"; - return false; + return { connected: false, tunnelActive: false }; } } connectBtn.addEventListener("click", async () => { const ip = ipInput.value.trim(); if (ip) { - const connected = await checkServerStatus(ip, false); - if (connected) { + const result = await checkServerStatus(ip, false); + if (result.connected) { + if (!result.tunnelActive) { + setTimeout(() => showTunnelWarning(), 1200); + } proceedToApp(); } } @@ -2462,12 +2466,83 @@ const modal = document.getElementById("tunnel-warning-modal"); if (!modal) return; 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(); - }); - document.getElementById("tunnel-dismiss-btn").addEventListener("click", () => { + }, { once: true }); + + dismissBtn.addEventListener("click", () => { 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() { @@ -2476,23 +2551,32 @@ startupStatus.classList.add("visible"); 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) { - connected = await checkServerStatus(null, true); + result = await checkServerStatus(null, true); } else { const savedIp = localStorage.getItem("extension_server_ip"); if (savedIp) { - connected = await checkServerStatus(savedIp, false); - } else { - connected = false; + result = await checkServerStatus(savedIp, false); } } - if (connected) { + if (result.connected) { startupStatusText.textContent = "Connection Found"; startupStatusDot.classList.remove("active"); startupStatusDot.classList.add("success"); 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(); } else { startupStatusText.textContent = "Connection Failed"; diff --git a/animex/manga.html b/animex/manga.html index 62e49b5..4b737eb 100644 --- a/animex/manga.html +++ b/animex/manga.html @@ -74,9 +74,9 @@ #desktop-hero { display: none; position: relative; - width: 100%; + width: 90vw; /* Hero div now spans 90% of the viewport width */ height: 580px; - margin: 24px 0 56px; + margin: 24px auto 56px; /* Centered horizontally with auto margins */ border-radius: 20px; overflow: hidden; background-color: #000; @@ -538,29 +538,29 @@ -
- - -
- -
-
- Trending Manga -
-

Loading...

-

-

-
- - -
+ +
+ +
+
+ Trending Manga +
+

Loading...

+

+

+
+ +
-
+
+
+ +
diff --git a/animex/series-info.html b/animex/series-info.html index 89b09b0..bf4ed44 100644 --- a/animex/series-info.html +++ b/animex/series-info.html @@ -1767,10 +1767,23 @@ iframe { width: 100%; height: 100%; border: none; } for (let i = 1; i <= epCount; i++) { 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); + + // 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(); setupLocateButton(); } @@ -2011,6 +2024,8 @@ iframe { width: 100%; height: 100%; border: none; } const url = `view.html?id=${currentMalId}&ep=${ep}`; document.getElementById("episode-popup-iframe").src = url; document.getElementById("episode-popup-overlay").classList.add("active"); + document.body.style.overflow = "hidden"; // Disable body scrolling when modal is open + } function closePlayerPopup() { @@ -2020,6 +2035,7 @@ iframe { width: 100%; height: 100%; border: none; } localWatchHistory = JSON.parse(localStorage.getItem("animex_watch_history") || "{}"); renderEpisodes(); setupLocateButton(); + document.body.style.overflow = ""; // Re-enable body scrolling when modal is closed } // --- PHASE 1: MODERNIZED RX GATE --- diff --git a/animex/view.html b/animex/view.html index a57a117..76d3a28 100644 --- a/animex/view.html +++ b/animex/view.html @@ -771,6 +771,38 @@ } 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) { DOM.loading.style.display = "flex"; DOM.loading.style.opacity = "1"; @@ -783,7 +815,11 @@ const data = await res.json(); if (!data.src) throw new Error("No source found"); - DOM.player.innerHTML = ``; + // ── 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 = ``; if (data.source_module) DOM.sourceSelect.value = data.source_module; diff --git a/templates/reader.html b/templates/reader.html index b0a6740..be2a7cb 100644 --- a/templates/reader.html +++ b/templates/reader.html @@ -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) { if (!src || src.startsWith("/")) return 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). */ function buildImageDOM() { container.innerHTML = ""; @@ -1210,10 +1281,16 @@ const allImgs = getAllImages(); 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 ── if (toPreload[0] && imageLinks[0]) { const tempImg = new Image(); - tempImg.src = proxyUrl(imageLinks[0]); + tempImg.src = resolvedUrl(imageLinks[0]); await new Promise(r => { tempImg.onload = () => { const isTall = tempImg.height > tempImg.width * 2; @@ -1235,12 +1312,15 @@ const imgs = getAllImages(); 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) => new Promise(resolve => { img.onload = resolve; - img.onerror = resolve; - img.src = proxyUrl(imageLinks[idx]); + // onerror handled inside setImgSrc (proxy retry); resolve after that too + 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"); } }; - img.onload = done; - img.onerror = done; - img.src = proxyUrl(imageLinks[idx + 4]); + // Use resolved URL with automatic proxy-retry on individual failures + img.addEventListener("load", done, { once: true }); + img.addEventListener("error", done, { once: true }); + setImgSrc(img, imageLinks[idx + 4]); }); } @@ -1290,12 +1371,13 @@ function rebuildImageContainer() { 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(); const newImgs = getAllImages(); newImgs.forEach((img, i) => { - if (existingSrcs[i]) img.src = existingSrcs[i]; + if (existingRawSrcs[i]) setImgSrc(img, existingRawSrcs[i]); }); // Restore scroll position after rebuild