Files
deploy-test/animex/manga.html
2026-03-29 20:52:57 -05:00

1238 lines
45 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>Manga - Media App</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
/>
<style>
:root {
--accent-color: #de541e;
--accent-rgb: 222, 84, 30;
--bg-dark: #121212;
--glass-bg: rgba(18, 18, 18, 0.7);
}
/* Base styles */
body {
font-family: "Inter", sans-serif;
background-color: var(--bg-dark);
background-image: radial-gradient(
circle,
rgba(255, 255, 255, 0.05) 1px,
transparent 1px
);
background-size: 5px 5px;
margin: 0;
color: #fff;
padding-top: 80px; /* Space for fixed navbar */
overflow-x: hidden;
}
.app-container {
padding: 0 20px;
max-width: 1400px;
margin: 0 auto;
}
.content-section {
margin-bottom: 30px;
}
/* --- Navbar --- */
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1000;
padding: 15px 0;
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.3s ease;
}
.navbar svg {
display: block;
margin: 0 auto;
height: 50px;
width: auto;
max-width: 80%;
}
.section-header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 25px;
gap: 15px;
margin-top: 20px;
}
.section-title {
font-size: 1.8em;
font-weight: 800;
text-transform: uppercase;
margin: 0;
}
/* --- Source Switch Button --- */
.source-switch-container {
display: flex;
justify-content: flex-end;
align-items: center;
}
#source-switch-btn {
background-color: #2c2c2e;
color: #fff;
border: 1px solid #444;
border-radius: 12px;
padding: 10px 16px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
}
#source-switch-btn:hover {
background-color: #3a3a3c;
border-color: var(--accent-color);
}
#source-switch-btn .fas {
font-size: 1.1em;
color: var(--accent-color);
}
/* --- Mobile Carousel (Default) --- */
#featured-carousel-container {
min-height: 400px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
overflow: hidden;
margin-bottom: 30px;
}
.carousel-viewport {
width: 100%;
height: 350px;
position: relative;
perspective: 1200px;
display: flex;
justify-content: center;
align-items: center;
}
.carousel-slide {
position: absolute;
width: 60%;
max-width: 220px;
height: 330px;
border-radius: 12px;
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.5s ease;
cursor: pointer;
transform: scale(0.5);
opacity: 0;
}
.carousel-slide img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.slide-center { transform: translateX(0) scale(1); opacity: 1; z-index: 10; }
.slide-left { transform: translateX(-40%) scale(0.8) rotate(-8deg); opacity: 1; z-index: 5; }
.slide-right { transform: translateX(40%) scale(0.8) rotate(8deg); opacity: 1; z-index: 5; }
.slide-exit-left { transform: translateX(-150%) scale(0.6); opacity: 0; z-index: 1; }
.slide-exit-right { transform: translateX(150%) scale(0.6); opacity: 0; z-index: 1; }
/* --- DESKTOP HERO SECTION (Hidden on Mobile) --- */
#desktop-hero {
display: none; /* Default hidden */
position: relative;
width: 100%;
height: 600px; /* Fixed height for hero */
margin-bottom: 50px;
border-radius: 20px;
overflow: hidden;
background-color: #000;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
.hero-bg-image {
position: absolute;
top: 0;
right: 0;
width: 65%;
height: 100%;
object-fit: cover;
opacity: 0.8;
mask-image: linear-gradient(to right, transparent 0%, black 20%, black 100%);
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 20%, black 100%);
transition: opacity 0.5s ease-in-out;
}
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to right, #121212 30%, rgba(18,18,18,0.8) 50%, transparent 100%);
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
width: 50%;
height: 100%;
padding: 60px;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
transition: opacity 0.5s ease-in-out, transform 0.5s ease;
}
/* Animation State Class */
.hero-hidden {
opacity: 0;
}
.hero-label {
font-size: 1.5em;
font-weight: 700;
color: #fff;
margin-bottom: 20px;
display: block;
}
.hero-tags {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.hero-tag {
background: rgba(255,255,255,0.1);
padding: 5px 12px;
border-radius: 4px;
font-size: 0.85em;
color: #ccc;
backdrop-filter: blur(5px);
}
.hero-title {
font-size: 4rem;
line-height: 1.1;
font-weight: 800;
margin: 0 0 10px 0;
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.hero-subtitle {
font-size: 1.5rem;
color: var(--accent-color);
margin: 0 0 20px 0;
font-weight: 600;
}
.hero-synopsis {
font-size: 1em;
line-height: 1.6;
color: #ddd;
margin-bottom: 30px;
display: -webkit-box;
-webkit-line-clamp: 4; /* Limit to 4 lines */
-webkit-box-orient: vertical;
overflow: hidden;
max-width: 90%;
}
.hero-actions {
display: flex;
gap: 15px;
}
.btn-hero {
padding: 12px 24px;
font-size: 1em;
font-weight: 700;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
display: flex;
align-items: center;
gap: 10px;
border: none;
}
.btn-primary {
background: #fff;
color: #000;
}
.btn-primary:hover {
background: #eee;
transform: translateY(-2px);
}
.btn-outline {
background: transparent;
border: 2px solid rgba(255,255,255,0.3);
color: #fff;
}
.btn-outline:hover {
border-color: #fff;
background: rgba(255,255,255,0.1);
}
/* --- Hero Indicators (Dots) --- */
.hero-indicators {
position: absolute;
bottom: 40px;
right: 60px;
display: flex;
gap: 12px;
z-index: 10;
}
.hero-dot {
width: 12px;
height: 12px;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.hero-dot:hover {
background-color: rgba(255, 255, 255, 0.6);
transform: scale(1.1);
}
.hero-dot.active {
background-color: var(--accent-color);
width: 20px; /* Stretch effect */
border-radius: 20px;
box-shadow: 0 0 10px rgba(222, 84, 30, 0.4);
}
/* --- MEDIA QUERY FOR DESKTOP --- */
@media (min-width: 1024px) {
body {
padding-top: 100px;
}
/* Swap Carousel for Hero */
#featured-carousel-container { display: none !important; }
#desktop-hero { display: flex !important; }
/* Relax grid layout */
.poster-card {
width: 160px;
height: 240px;
}
/* Make navbar cleaner on desktop */
.navbar {
padding: 20px 0;
}
}
@media (max-width: 768px) {
.section-title {
font-size: 1em;
}
.horizontal-scroll-container {
display: none;
}
}
/* --- Search & Grids --- */
#mangadex-search-container { margin-bottom: 30px; }
.search-box {
position: relative;
width: 100%;
max-width: 600px;
margin: 0 auto 30px auto;
}
#mangadex-search-input {
width: 100%;
padding: 15px 50px 15px 20px;
font-size: 1.1em;
border-radius: 30px;
border: 2px solid #333;
background-color: #1f1f1f;
color: #fff;
box-sizing: border-box;
transition: border-color 0.3s, box-shadow 0.3s;
}
#mangadex-search-input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 15px rgba(var(--accent-rgb), 0.3);
}
#mangadex-search-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: var(--accent-color);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2em;
display: flex;
align-items: center;
justify-content: center;
}
#mangadex-results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 24px;
min-height: 200px;
}
/* --- Loaders & Common --- */
.loader {
border: 4px solid rgba(255, 255, 255, 0.2);
border-top: 4px solid var(--accent-color);
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 100px auto;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.section-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.view-all-arrow {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: #333;
color: #fff;
border-radius: 50%;
text-decoration: none;
font-weight: bold;
transition: background 0.2s;
}
.view-all-arrow:hover { background-color: var(--accent-color); }
.horizontal-scroll-container {
display: flex;
overflow-x: auto;
padding-bottom: 15px;
gap: 15px;
scrollbar-width: thin;
scrollbar-color: #444 #121212;
}
.horizontal-scroll-container::-webkit-scrollbar { height: 8px; }
.horizontal-scroll-container::-webkit-scrollbar-track { background: #121212; }
.horizontal-scroll-container::-webkit-scrollbar-thumb { background-color: #444; border-radius: 4px; }
.poster-card {
flex-shrink: 0;
width: 140px;
height: 210px;
cursor: pointer;
position: relative;
border-radius: 10px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
background-color: #222;
}
.poster-card:hover {
transform: scale(1.05) translateY(-5px);
box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.5);
z-index: 10;
}
.poster-card img {
width: 100%;
height: 100%;
object-fit: cover;
}
.poster-card::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60%;
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, transparent 100%);
border-radius: 0 0 10px 10px;
}
.poster-card .poster-title {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 12px;
box-sizing: border-box;
z-index: 2;
font-size: 0.95em;
font-weight: 600;
color: #fff;
white-space: normal;
line-height: 1.2;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
}
.error-message {
color: #e5e5e5;
font-style: italic;
width: 100%;
text-align: center;
padding: 40px 0;
}
/* Continue Reading */
.poster-container {
flex-shrink: 0;
width: 160px;
text-align: left;
transition: transform 0.2s ease-in-out;
}
.poster-container:hover { transform: translateY(-5px); }
.poster-image-wrapper {
position: relative;
cursor: pointer;
margin-bottom: 10px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.poster-image-wrapper img {
width: 100%;
height: 230px;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.poster-container:hover .poster-image-wrapper img { transform: scale(1.05); }
.poster-container .poster-title {
font-size: 0.9em;
font-weight: 500;
color: #e5e5e5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.poster-container:hover .poster-title { color: var(--accent-color); }
.progress-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background-color: rgba(255, 255, 255, 0.3);
}
.progress-bar-inner {
height: 100%;
width: 0%;
background-color: var(--accent-color);
}
.chapter-indicator {
position: absolute;
top: 8px;
left: 8px;
background-color: rgba(0, 0, 0, 0.75);
color: #fff;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.8em;
font-weight: 600;
z-index: 2;
backdrop-filter: blur(2px);
}
</style>
</head>
<body>
<nav class="navbar">
<svg
width="1767"
height="292"
viewBox="0 0 1767 292"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="1767" height="292" rx="146" fill="black" fill-opacity="0.01" />
<rect x="802" y="56" width="164" height="22.5285" fill="#DE541E" />
<rect x="802" y="93.7502" width="164" height="22.5285" fill="#DE541E" />
<path d="M957.858 235.619H931.051L903.773 71.8308H931.051L957.858 235.619Z" fill="#DE541E" />
<path d="M813.05 235.619H839.857L867.135 71.8308H839.857L813.05 235.619Z" fill="#DE541E" />
<rect x="867.135" y="199.608" width="34.3121" height="36.0108" fill="#DE541E" />
<ellipse cx="884.291" cy="199.275" rx="17.156" ry="19.6726" fill="#DE541E" />
</svg>
</nav>
<div class="app-container">
<!-- Unified Header and Source Toggle -->
<div class="section-header">
<h2 class="section-title" id="main-heading">Spotlight</h2>
<div class="source-switch-container">
<button id="source-switch-btn">
<i class="fas fa-book"></i>
<span id="source-name">Jikan</span>
</button>
</div>
</div>
<!-- Jikan-specific Content -->
<section class="content-section" id="jikan-content">
<!-- Mobile Carousel -->
<div id="featured-carousel-container">
<div class="loader"></div>
</div>
<!-- Desktop Hero Section -->
<div id="desktop-hero">
<!-- Image filled by JS -->
<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">Trending Manga</span>
<div class="hero-tags" id="hero-tags">
<!-- Tags filled by JS -->
</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>
<!-- Interactive Indicators (Dots) -->
<div class="hero-indicators" id="hero-indicators"></div>
</div>
<main class="main-content" id="main-content-area-jikan"></main>
</section>
<!-- MangaDex-specific Content -->
<section class="content-section" id="mangadex-content" style="display: none;">
<div id="mangadex-search-container">
<div class="search-box">
<input type="text" id="mangadex-search-input" placeholder="Search MangaDex...">
<button id="mangadex-search-btn"><i class="fas fa-search"></i></button>
</div>
<div id="mangadex-results-grid">
<!-- Search results will appear here -->
</div>
</div>
<main class="main-content" id="main-content-area-mangadex"></main>
</section>
<!-- Shared Content Area -->
<main class="main-content" id="main-content-area-shared"></main>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const getServerUrl = () => {
const extensionServerIp = localStorage.getItem("extension_server_ip") || "localhost";
const url = "http://" + extensionServerIp + ":7275";
console.log("[manga] serverUrl=", url);
return url;
};
const serverUrl = getServerUrl();
const JIKAN_API_BASE_URL = "https://api.jikan.moe/v4";
const CONTENT_JSON_FILE = "manga_content.json";
const urlParams = new URLSearchParams(window.location.search);
const profileId = urlParams.get('profileId');
// --- SOURCE MANAGEMENT ---
const mainHeading = document.getElementById("main-heading");
const sourceSwitchBtn = document.getElementById("source-switch-btn");
const sourceNameEl = document.getElementById("source-name");
const jikanContent = document.getElementById("jikan-content");
const mangadexContent = document.getElementById("mangadex-content");
const mangadexSearchInput = document.getElementById("mangadex-search-input");
const mangadexSearchBtn = document.getElementById("mangadex-search-btn");
const mangadexResultsGrid = document.getElementById("mangadex-results-grid");
let currentSource = localStorage.getItem("manga_source") || "jikan";
function setSource(source) {
currentSource = source;
localStorage.setItem("manga_source", source);
const jikanContinue = document.getElementById('continue-reading-jikan');
const mangadexContinue = document.getElementById('continue-reading-mangadex');
if (source === 'mangadex') {
jikanContent.style.display = 'none';
mangadexContent.style.display = 'block';
mainHeading.textContent = "MangaDex";
sourceNameEl.textContent = "MangaDex";
if(jikanContinue) jikanContinue.style.display = 'none';
if(mangadexContinue) mangadexContinue.style.display = 'block';
} else {
jikanContent.style.display = 'block';
mangadexContent.style.display = 'none';
mainHeading.textContent = "Spotlight";
sourceNameEl.textContent = "Jikan";
if(jikanContinue) jikanContinue.style.display = 'block';
if(mangadexContinue) mangadexContinue.style.display = 'none';
}
}
sourceSwitchBtn.addEventListener('click', () => {
const newSource = currentSource === 'jikan' ? 'mangadex' : 'jikan';
setSource(newSource);
});
// Initialize source
setSource(currentSource);
function openMangaModal(source, mangaId) {
window.parent.openPopup(`manga-info.html?source=${source}&id=${mangaId}`);
}
// --- MANGADEX NEW SECTIONS ---
async function createMangaDexHorizontalSection(title, order) {
try {
const section = document.createElement("section");
section.className = "content-section";
section.innerHTML = `
<div class="section-header-row">
<h2 class="section-title">${title}</h2>
</div>
<div class="horizontal-scroll-container"><div class="loader"></div></div>`;
const mainContentAreaMangaDex = document.getElementById("main-content-area-mangadex");
mainContentAreaMangaDex.appendChild(section);
const scrollContainer = section.querySelector(".horizontal-scroll-container");
const response = await fetch(`${serverUrl}/mangadex/list?order=${order}&limit=15&profile_id=${profileId}`);
if (!response.ok) throw new Error(`API error: ${response.status}`);
const responseData = await response.json();
displayMangaDexPosters(responseData.data, scrollContainer);
} catch (error) {
console.error(`Failed to load MangaDex section "${title}":`, error);
const container = document.getElementById("main-content-area-mangadex");
if (container && container.lastElementChild && container.lastElementChild.querySelector(".loader")) {
container.lastElementChild.querySelector(".loader").parentElement.innerHTML = `<p class="error-message">Could not load section.</p>`;
}
}
}
function displayMangaDexPosters(mangaData, container) {
container.innerHTML = "";
if (!mangaData || mangaData.length === 0) {
container.innerHTML = `<p class="error-message">No results found.</p>`;
return;
}
mangaData.forEach((manga) => {
const title = manga.attributes.title.en || Object.values(manga.attributes.title)[0];
const coverUrl = manga.cover_url || 'https://placehold.co/400x600/141414/333?text=?';
const card = document.createElement("div");
card.className = "poster-card";
card.onclick = () => openMangaModal('mangadex', manga.id);
card.innerHTML = `
<img src="${coverUrl}" alt="${title}" loading="lazy">
<p class="poster-title">${title}</p>`;
container.appendChild(card);
});
}
// --- JIKAN / CURATED CONTENT LOGIC ---
const mainContentAreaJikan = document.getElementById("main-content-area-jikan");
const carouselContainer = document.getElementById("featured-carousel-container");
let carouselData = [];
let currentIndex = 0;
let slides = [];
let isAnimating = false;
// Desktop Hero Shuffle Variables
let currentHeroIndex = 0;
let heroInterval;
// Fetch the curated JSON
async function loadCuratedContent() {
try {
const response = await fetch(CONTENT_JSON_FILE);
if (!response.ok) throw new Error("Could not load manga_content.json");
const data = await response.json();
// 1. Setup Spotlight (Carousel + Hero)
if (data.spotlight && data.spotlight.length > 0) {
setupSpotlight(data.spotlight);
} else {
// Fallback if spotlight is empty
carouselContainer.innerHTML = `<p class="error-message">No spotlight content found.</p>`;
}
// 2. Setup Horizontal Sections
if (data.sections && data.sections.length > 0) {
data.sections.forEach(section => {
createLocalJikanSection(section);
});
}
} catch (error) {
console.error("Content Load Error:", error);
// Fallback to manual API call if JSON fails
createJikanHorizontalSection("Top Manga (API Fallback)", `${JIKAN_API_BASE_URL}/top/manga?filter=bypopularity&limit=15`);
}
}
function setupSpotlight(spotlightItems) {
carouselData = spotlightItems;
// 1. Mobile Carousel Rendering
carouselContainer.innerHTML = `
<div class="carousel-viewport">
${carouselData.map( (item) => `
<div class="carousel-slide" data-id="${item.id}">
<img src="${item.image_tall || item.image}" alt="${item.name}">
</div>`).join("")}
</div>`;
// Mobile Carousel Logic
slides = Array.from(carouselContainer.querySelectorAll(".carousel-slide"));
const viewport = carouselContainer.querySelector(".carousel-viewport");
let touchstartX = 0;
if(viewport) {
viewport.addEventListener("touchstart", (e) => (touchstartX = e.changedTouches[0].screenX), { passive: true });
viewport.addEventListener("touchend", (e) => {
const touchendX = e.changedTouches[0].screenX;
if (touchendX < touchstartX - 50) navigateCarousel(1);
if (touchendX > touchstartX + 50) navigateCarousel(-1);
});
viewport.addEventListener("click", (e) => {
const slide = e.target.closest(".carousel-slide");
if (slide && slide.classList.contains("slide-center")) {
openMangaModal('jikan', slide.dataset.id);
}
});
updateCarousel();
}
// 2. Desktop Hero Rendering & Shuffle
if (carouselData.length > 0) {
setupHeroIndicators(carouselData);
updateDesktopHero(carouselData[0]);
resetHeroTimer();
}
}
// Desktop Hero Helper Functions
function setupHeroIndicators(data) {
const container = document.getElementById('hero-indicators');
container.innerHTML = '';
data.forEach((_, index) => {
const dot = document.createElement('div');
dot.className = 'hero-dot';
if (index === 0) dot.classList.add('active');
dot.addEventListener('click', () => {
currentHeroIndex = index;
updateDesktopHero(data[index], true);
resetHeroTimer();
});
container.appendChild(dot);
});
}
function resetHeroTimer() {
if (heroInterval) clearInterval(heroInterval);
heroInterval = setInterval(() => {
currentHeroIndex = (currentHeroIndex + 1) % carouselData.length;
updateDesktopHero(carouselData[currentHeroIndex], true);
}, 8000);
}
function updateDesktopHero(item, animate = false) {
if(!item) return;
const heroContent = document.getElementById('hero-content');
const heroBg = document.getElementById('hero-bg');
// DOM Elements to update
const heroTitle = document.getElementById('hero-title');
const heroSubtitle = document.getElementById('hero-subtitle');
const heroDesc = document.getElementById('hero-desc');
const heroTags = document.getElementById('hero-tags');
const heroBtn = document.getElementById('hero-read-btn');
// Update Dots
const allDots = document.querySelectorAll('.hero-dot');
allDots.forEach((dot, idx) => {
if(idx === currentHeroIndex) dot.classList.add('active');
else dot.classList.remove('active');
});
// Use item properties from JSON
const title = item.name;
const subtitle = item.jp_name; // Manga usually don't have separate sub-titles in this JSON format
const image = item.image; // Use wide image if available, else standard
const synopsis = item.description || "Click to read more.";
// Function to actually set DOM content
const setContent = () => {
heroTitle.textContent = title;
heroSubtitle.textContent = subtitle;
heroDesc.textContent = synopsis;
heroBg.src = image;
heroTags.innerHTML = '';
const tag = document.createElement('span');
tag.className = 'hero-tag';
tag.textContent = "Manga";
heroTags.appendChild(tag);
// Clear old listener and add new one
const newBtn = heroBtn.cloneNode(true);
heroBtn.parentNode.replaceChild(newBtn, heroBtn);
newBtn.addEventListener('click', () => {
openMangaModal('jikan', item.id);
});
};
if (animate) {
// Add fade-out class
heroContent.classList.add('hero-hidden');
heroBg.classList.add('hero-hidden');
// Wait for transition (500ms), update content, then fade in
setTimeout(() => {
setContent();
heroContent.classList.remove('hero-hidden');
heroBg.classList.remove('hero-hidden');
}, 500);
} else {
setContent();
}
}
// Mobile Carousel Logic
function navigateCarousel(direction) {
if (isAnimating) return;
isAnimating = true;
const total = carouselData.length;
const oldIndex = currentIndex;
currentIndex = (currentIndex + direction + total) % total;
updateCarousel(direction, oldIndex);
setTimeout(() => { isAnimating = false; }, 600);
}
function updateCarousel(direction = 0, oldIndex = -1) {
const total = slides.length;
if (total === 0) return;
const leftIndex = (currentIndex - 1 + total) % total;
const rightIndex = (currentIndex + 1 + total) % total;
if (direction !== 0 && oldIndex !== -1) {
const oldLeftIndex = (oldIndex - 1 + total) % total;
const oldRightIndex = (oldIndex + 1 + total) % total;
if (direction === 1) slides[oldLeftIndex].className = "carousel-slide slide-exit-left";
else slides[oldRightIndex].className = "carousel-slide slide-exit-right";
}
slides.forEach((slide, index) => {
setTimeout(() => {
let newClass = "carousel-slide";
if (index === currentIndex) newClass += " slide-center";
else if (index === leftIndex) newClass += " slide-left";
else if (index === rightIndex) newClass += " slide-right";
slide.className = newClass;
}, 10);
});
}
// Render sections from JSON
function createLocalJikanSection(sectionData) {
const section = document.createElement("section");
section.className = "content-section";
section.innerHTML = `
<div class="section-header-row">
<h2 class="section-title">${sectionData.title}</h2>
</div>
<div class="horizontal-scroll-container"></div>`;
mainContentAreaJikan.appendChild(section);
const scrollContainer = section.querySelector(".horizontal-scroll-container");
if (sectionData.items && sectionData.items.length > 0) {
// Convert JSON format to format expected by displayJikanPosters
const mappedItems = sectionData.items.map(item => ({
mal_id: item.id,
title: item.name,
images: { jpg: { image_url: item.image } }
}));
displayJikanPosters(mappedItems, scrollContainer);
} else {
scrollContainer.innerHTML = `<p class="error-message">No items in this section.</p>`;
}
}
// Legacy function for Fallback API calls
async function createJikanHorizontalSection(title, apiUrl) {
try {
const section = document.createElement("section");
section.className = "content-section";
section.innerHTML = `
<div class="section-header-row">
<h2 class="section-title">${title}</h2>
<a href="#" class="view-all-arrow">></a>
</div>
<div class="horizontal-scroll-container"><p style="color: #ccc;">Loading...</p></div>`;
mainContentAreaJikan.appendChild(section);
const scrollContainer = section.querySelector(".horizontal-scroll-container");
const response = await fetch(apiUrl);
if (!response.ok) throw new Error(`API error: ${response.status}`);
const responseData = await response.json();
displayJikanPosters(responseData.data, scrollContainer);
} catch (error) {
console.error(`Failed to load section "${title}":`, error);
}
}
function displayJikanPosters(mangaData, container) {
container.innerHTML = "";
if (!mangaData || mangaData.length === 0) {
container.innerHTML = `<p class="error-message">No results found.</p>`;
return;
}
mangaData.forEach((item) => {
// Check for valid image in either API structure or JSON structure
const imageUrl = item.images?.jpg?.image_url || item.image;
if (!imageUrl) return;
const card = document.createElement("div");
card.className = "poster-card";
card.onclick = () => {
window.parent.openPopup(`manga-info.html?source=jikan&id=${item.mal_id}`);
};
card.innerHTML = `
<img src="${imageUrl}" alt="${item.title}" loading="lazy">
<div style="position:absolute;top:8px;left:8px;z-index:2;">
<span style="background:#DE541E;color:#fff;font-size:0.75em;font-weight:700;padding:2px 8px;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.2);letter-spacing:1px;">Manga</span>
</div>
<p class="poster-title">${item.title}</p>`;
container.appendChild(card);
});
}
// --- MANGADEX API LOGIC ---
async function performMangaDexSearch() {
const query = mangadexSearchInput.value.trim();
if (query.length < 3) {
mangadexResultsGrid.innerHTML = `<p class="error-message">Please enter at least 3 characters.</p>`;
return;
}
mangadexResultsGrid.innerHTML = `<div class="loader"></div>`;
try {
const response = await fetch(`${serverUrl}/mangadex/search?q=${encodeURIComponent(query)}&profile_id=${profileId}`);
if (!response.ok) throw new Error(`Search failed: ${response.statusText}`);
const results = await response.json();
displayMangaDexResults(results.data);
} catch (error) {
console.error("MangaDex search error:", error);
mangadexResultsGrid.innerHTML = `<p class="error-message">Could not perform search. Please try again later.</p>`;
}
}
function displayMangaDexResults(data) {
mangadexResultsGrid.innerHTML = '';
if (!data || data.length === 0) {
mangadexResultsGrid.innerHTML = `<p class="error-message">No results found for your search.</p>`;
return;
}
data.forEach(manga => {
const title = manga.attributes.title.en || Object.values(manga.attributes.title)[0];
const coverUrl = manga.cover_url || 'https://placehold.co/400x600/141414/333?text=?';
const card = document.createElement("div");
card.className = "poster-card";
card.onclick = () => openMangaModal('mangadex', manga.id);
card.innerHTML = `
<img src="${coverUrl}" alt="${title}" loading="lazy">
<p class="poster-title">${title}</p>`;
mangadexResultsGrid.appendChild(card);
});
}
mangadexSearchBtn.addEventListener('click', performMangaDexSearch);
mangadexSearchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') performMangaDexSearch();
});
// --- CONTINUE READING LOGIC (SHARED) ---
const mainContentAreaShared = document.getElementById("main-content-area-shared");
async function initializeContinueReading() {
const readingHistory = JSON.parse(localStorage.getItem("reading_history")) || [];
if (readingHistory.length === 0) return;
// Jikan
const jikanHistory = readingHistory.filter(item => !item.startsWith('mangadex:'));
const processedJikan = new Map();
jikanHistory.reverse().forEach(entry => {
const [mangaId, chapterStr] = entry.split('/');
if (mangaId && !processedJikan.has(mangaId)) {
processedJikan.set(mangaId, { mangaId, last_chapter: parseInt(chapterStr, 10) });
}
});
if (processedJikan.size > 0) {
await renderJikanContinueReading(Array.from(processedJikan.values()));
}
// MangaDex
const mangadexHistory = readingHistory.filter(item => item.startsWith('mangadex:'));
const processedMangaDex = new Map();
mangadexHistory.reverse().forEach(entry => {
const [mangaId, chapterId] = entry.replace('mangadex:', '').split('/');
if (mangaId && !processedMangaDex.has(mangaId)) {
processedMangaDex.set(mangaId, { mangaId, chapterId });
}
});
if (processedMangaDex.size > 0) {
await renderMangaDexContinueReading(Array.from(processedMangaDex.values()));
}
// Final visibility check
setSource(currentSource);
}
async function renderJikanContinueReading(list) {
const section = document.createElement("section");
section.id = "continue-reading-jikan";
section.className = "content-section";
section.innerHTML = `<div class="section-header-row">
<h2 class="section-title">Continue Reading (Jikan)</h2>
</div>
<div class="horizontal-scroll-container"></div>`;
mainContentAreaShared.prepend(section);
const container = section.querySelector(".horizontal-scroll-container");
for (const item of list.slice(0, 15)) {
try {
await new Promise((resolve) => setTimeout(resolve, 350));
const response = await fetch(`${JIKAN_API_BASE_URL}/manga/${item.mangaId}`);
if (!response.ok) continue;
const mangaData = (await response.json()).data;
if (!mangaData) continue;
const title = mangaData.title_english || mangaData.title;
const totalChapters = mangaData.chapters;
const progressPercent = totalChapters > 0 ? (item.last_chapter / totalChapters) * 100 : 0;
const isFinished = totalChapters && item.last_chapter >= totalChapters;
const indicatorText = isFinished ? "Finished" : `Chapter ${item.last_chapter}`;
const poster = createPoster('jikan', mangaData.mal_id, mangaData.images?.jpg?.image_url, title, indicatorText, progressPercent);
container.appendChild(poster);
} catch (err) { console.error(`Failed to fetch Jikan data for manga ID ${item.mangaId}:`, err); }
}
}
async function renderMangaDexContinueReading(list) {
const section = document.createElement("section");
section.id = "continue-reading-mangadex";
section.className = "content-section";
section.innerHTML = `<div class="section-header-row">
<h2 class="section-title">Continue Reading (MangaDex)</h2>
</div>
<div class="horizontal-scroll-container"></div>`;
mainContentAreaShared.prepend(section);
const container = section.querySelector(".horizontal-scroll-container");
for (const item of list.slice(0, 15)) {
try {
const [mangaRes, chaptersRes] = await Promise.all([
fetch(`${serverUrl}/mangadex/manga/${item.mangaId}`),
fetch(`${serverUrl}/mangadex/manga/${item.mangaId}/chapters`)
]);
if (!mangaRes.ok || !chaptersRes.ok) continue;
const mangaData = await mangaRes.json();
const chaptersData = await chaptersRes.json();
const chapters = chaptersData.chapters || [];
const title = mangaData.attributes.title.en || Object.values(mangaData.attributes.title)[0];
const coverUrl = mangaData.image_url;
const currentChapterIndex = chapters.findIndex(c => c.id === item.chapterId);
const totalChapters = chapters.length;
const progressPercent = totalChapters > 0 && currentChapterIndex !== -1 ? ((totalChapters - currentChapterIndex) / totalChapters) * 100 : 0;
const chapterNumber = chapters[currentChapterIndex]?.attributes.chapter || '?';
const indicatorText = `Chapter ${chapterNumber}`;
const poster = createPoster('mangadex', item.mangaId, coverUrl, title, indicatorText, progressPercent);
container.appendChild(poster);
} catch (err) { console.error(`Failed to fetch MangaDex data for manga ID ${item.mangaId}:`, err); }
}
}
function createPoster(source, id, imageUrl, title, indicatorText, progressPercent) {
const posterContainer = document.createElement("div");
posterContainer.className = "poster-container";
posterContainer.innerHTML = `
<div class="poster-image-wrapper">
<div class="chapter-indicator">${indicatorText}</div>
<img src="${imageUrl}" alt="${title}" loading="lazy">
<div class="progress-bar">
<div class="progress-bar-inner" style="width: ${progressPercent}%;"></div>
</div>
</div>
<p class="poster-title" title="${title}">${title}</p>`;
posterContainer.addEventListener("click", () => openMangaModal(source, id));
return posterContainer;
}
async function initializePage() {
// Jikan / Curated Content
await loadCuratedContent();
// Continue Reading
await initializeContinueReading();
// MangaDex content
createMangaDexHorizontalSection("Recently Updated", "latestUploadedChapter");
createMangaDexHorizontalSection("Popular Titles", "followedCount");
}
initializePage();
});
</script>
</body>
</html>