another set of updates

This commit is contained in:
2026-04-01 23:24:41 -05:00
parent 812f775754
commit ef2a685561
5 changed files with 271 additions and 53 deletions

View File

@@ -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";

View File

@@ -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,29 +538,29 @@
</svg> </svg>
</nav> </nav>
<div class="app-container"> <!-- Desktop Hero (shown on 1024px+) -->
<div id="desktop-hero">
<!-- Desktop Hero (shown on 1024px+) --> <img class="hero-bg-image" id="hero-bg" src="" alt="" />
<div id="desktop-hero"> <div class="hero-overlay"></div>
<img class="hero-bg-image" id="hero-bg" src="" alt="" /> <div class="hero-content" id="hero-content">
<div class="hero-overlay"></div> <span class="hero-label"><i class="fas fa-fire"></i> Trending Manga</span>
<div class="hero-content" id="hero-content"> <div class="hero-tags" id="hero-tags"></div>
<span class="hero-label"><i class="fas fa-fire"></i> Trending Manga</span> <h1 class="hero-title" id="hero-title">Loading...</h1>
<div class="hero-tags" id="hero-tags"></div> <h2 class="hero-subtitle" id="hero-subtitle"></h2>
<h1 class="hero-title" id="hero-title">Loading...</h1> <p class="hero-synopsis" id="hero-desc"></p>
<h2 class="hero-subtitle" id="hero-subtitle"></h2> <div class="hero-actions">
<p class="hero-synopsis" id="hero-desc"></p> <button class="btn-hero btn-primary" id="hero-read-btn">
<div class="hero-actions"> Read Now <i class="fas fa-arrow-right"></i>
<button class="btn-hero btn-primary" id="hero-read-btn"> </button>
Read Now <i class="fas fa-arrow-right"></i> <button class="btn-hero btn-outline">
</button> <i class="far fa-bookmark"></i>
<button class="btn-hero btn-outline"> </button>
<i class="far fa-bookmark"></i>
</button>
</div>
</div> </div>
<div class="hero-indicators" id="hero-indicators"></div>
</div> </div>
<div class="hero-indicators" id="hero-indicators"></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">

View File

@@ -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 ---

View File

@@ -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;

View File

@@ -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