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));
// --- 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";

View File

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

View File

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