485 lines
18 KiB
HTML
485 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>StreamFlow</title>
|
|
<link rel="stylesheet" href="./styles/anime.css">
|
|
<style>
|
|
/* --- MODAL STYLES --- */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
backdrop-filter: blur(8px);
|
|
z-index: 2000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
opacity: 1;
|
|
}
|
|
.modal-content {
|
|
background: #18181b;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 24px;
|
|
padding: 2.5rem;
|
|
max-width: 650px;
|
|
width: 90%;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
position: relative;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
transform: scale(0.95);
|
|
transition: transform 0.3s;
|
|
}
|
|
.modal-overlay.active .modal-content {
|
|
transform: scale(1);
|
|
}
|
|
.modal-close {
|
|
position: absolute;
|
|
top: 1.5rem;
|
|
right: 1.5rem;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: none;
|
|
color: white;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: 0.2s;
|
|
}
|
|
.modal-close:hover { background: rgba(255, 255, 255, 0.2); }
|
|
.modal-text {
|
|
line-height: 1.8;
|
|
font-size: 1.1rem;
|
|
color: #e4e4e7;
|
|
}
|
|
.modal-title { margin-top: 0; margin-bottom: 1.5rem; font-size: 1.5rem; }
|
|
|
|
.read-more-btn {
|
|
background: none;
|
|
border: none;
|
|
color: #8b5cf6;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
padding: 0;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.95rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
.read-more-btn:hover { text-decoration: underline; }
|
|
|
|
/* --- EPISODE TOOLBAR --- */
|
|
.episodes-header-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
}
|
|
.episodes-header-row h2 {
|
|
margin: 0;
|
|
font-size: 1.8rem;
|
|
border-left: 4px solid #8b5cf6;
|
|
padding-left: 1rem;
|
|
}
|
|
.episode-search-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.episode-search-input {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 99px;
|
|
padding: 0.6rem 1rem;
|
|
color: white;
|
|
width: 140px;
|
|
text-align: center;
|
|
font-family: inherit;
|
|
transition: 0.2s;
|
|
-moz-appearance: textfield; /* Firefox spinner hide */
|
|
}
|
|
.episode-search-input:focus {
|
|
border-color: #8b5cf6;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
outline: none;
|
|
}
|
|
/* Hide number spinners for Webkit/Chrome */
|
|
.episode-search-input::-webkit-outer-spin-button,
|
|
.episode-search-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
|
|
/* --- PAGINATION --- */
|
|
.pagination-controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
.page-btn {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
color: white;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
font-weight: 500;
|
|
}
|
|
.page-btn:hover:not(:disabled) {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-color: #8b5cf6;
|
|
}
|
|
.page-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
.page-info {
|
|
color: #a1a1aa;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Description Modal -->
|
|
<div class="modal-overlay" id="desc-modal">
|
|
<div class="modal-content">
|
|
<button class="modal-close" onclick="closeModal()">✕</button>
|
|
<h2 class="modal-title">Synopsis</h2>
|
|
<div class="modal-text" id="full-description"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Back Button -->
|
|
<a href="/" class="back-btn">
|
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg>
|
|
Back to Home
|
|
</a>
|
|
|
|
<!-- Video Hero (Trailer on Loop) -->
|
|
<div class="hero-wrapper">
|
|
<div class="video-background">
|
|
<div id="player"></div>
|
|
</div>
|
|
<div class="hero-overlay"></div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="content-container">
|
|
|
|
<!-- Left Sidebar (Info) -->
|
|
<aside class="sidebar">
|
|
<div class="poster-card">
|
|
<img id="poster" src="" alt="">
|
|
</div>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<h4>Format</h4>
|
|
<span id="format">--</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<h4>Episodes</h4>
|
|
<span id="episodes">--</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<h4>Status</h4>
|
|
<span id="status">--</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<h4>Season</h4>
|
|
<span id="season">--</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<h4>Studio</h4>
|
|
<span id="studio">--</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<h4>Main Characters</h4>
|
|
<div class="character-list" id="char-list">
|
|
<!-- Populated via JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Right Content -->
|
|
<main class="main-content">
|
|
<div class="anime-header">
|
|
<h1 class="anime-title" id="title">Loading...</h1>
|
|
|
|
<div class="meta-row">
|
|
<div class="pill score" id="score">--% Score</div>
|
|
<div class="pill" id="year">----</div>
|
|
<div class="pill" id="genres">Action</div>
|
|
</div>
|
|
|
|
<div class="action-row">
|
|
<button class="btn-watch" id="watch-btn">
|
|
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
|
Start Watching
|
|
</button>
|
|
<button class="btn-secondary">+ Add to List</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description Box -->
|
|
<div class="description-box">
|
|
<div id="description-preview"></div>
|
|
<button id="read-more-btn" class="read-more-btn" style="display: none;" onclick="openModal()">
|
|
Read More
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="episodes-section">
|
|
<!-- Header Row with Search -->
|
|
<div class="episodes-header-row">
|
|
<div class="section-title" style="margin:0; border:none; padding:0;">
|
|
<h2 style="font-size: 1.8rem; border-left: 4px solid #8b5cf6; padding-left: 1rem;">Episodes</h2>
|
|
</div>
|
|
<div class="episode-search-wrapper">
|
|
<input type="number" id="ep-search" class="episode-search-input" placeholder="Jump to Ep #" min="1">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="episodes-grid" id="episodes-grid"></div>
|
|
|
|
<!-- Pagination Controls -->
|
|
<div class="pagination-controls" id="pagination-controls">
|
|
<button class="page-btn" id="prev-page" onclick="changePage(-1)">Previous</button>
|
|
<span class="page-info" id="page-info">Page 1 of 1</span>
|
|
<button class="page-btn" id="next-page" onclick="changePage(1)">Next</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const animeId = window.location.pathname.split('/').pop();
|
|
let player;
|
|
|
|
// Episode State
|
|
let totalEpisodes = 0;
|
|
let currentPage = 1;
|
|
const itemsPerPage = 12;
|
|
|
|
// Load YouTube API
|
|
var tag = document.createElement('script');
|
|
tag.src = "https://www.youtube.com/iframe_api";
|
|
var firstScriptTag = document.getElementsByTagName('script')[0];
|
|
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
|
|
|
async function loadAnime() {
|
|
try {
|
|
const res = await fetch(`/api/anime/${animeId}`);
|
|
const data = await res.json();
|
|
|
|
if(data.error) {
|
|
document.getElementById('title').innerText = "Anime Not Found";
|
|
return;
|
|
}
|
|
|
|
// Populate Data
|
|
const title = data.title.english || data.title.romaji;
|
|
document.title = `${title} | StreamFlow`;
|
|
document.getElementById('title').innerText = title;
|
|
document.getElementById('poster').src = data.coverImage.extraLarge;
|
|
|
|
// Description Logic
|
|
const rawDesc = data.description || "No description available.";
|
|
handleDescription(rawDesc);
|
|
|
|
document.getElementById('score').innerText = (data.averageScore || '?') + '% Score';
|
|
document.getElementById('year').innerText = data.seasonYear || '????';
|
|
document.getElementById('genres').innerText = data.genres ? data.genres.slice(0, 3).join(' • ') : '';
|
|
|
|
document.getElementById('format').innerText = data.format || 'TV';
|
|
document.getElementById('episodes').innerText = data.episodes || '?';
|
|
document.getElementById('status').innerText = data.status || 'Unknown';
|
|
document.getElementById('season').innerText = `${data.season || ''} ${data.seasonYear || ''}`;
|
|
|
|
if (data.studios && data.studios.nodes.length > 0) {
|
|
document.getElementById('studio').innerText = data.studios.nodes[0].name;
|
|
}
|
|
|
|
if (data.characters && data.characters.nodes) {
|
|
const charContainer = document.getElementById('char-list');
|
|
data.characters.nodes.slice(0, 5).forEach(char => {
|
|
charContainer.innerHTML += `
|
|
<div class="character-item">
|
|
<div class="char-dot"></div> ${char.name.full}
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
// Watch Button
|
|
document.getElementById('watch-btn').onclick = () => {
|
|
window.location.href = `/watch/${animeId}/1`;
|
|
};
|
|
|
|
// Setup Trailer
|
|
if (data.trailer && data.trailer.site === 'youtube') {
|
|
window.onYouTubeIframeAPIReady = function() {
|
|
player = new YT.Player('player', {
|
|
height: '100%',
|
|
width: '100%',
|
|
videoId: data.trailer.id,
|
|
playerVars: {
|
|
'autoplay': 1, 'controls': 0, 'mute': 1,
|
|
'loop': 1, 'playlist': data.trailer.id,
|
|
'showinfo': 0, 'modestbranding': 1, 'disablekb': 1
|
|
},
|
|
events: { 'onReady': (e) => e.target.playVideo() }
|
|
});
|
|
};
|
|
} else {
|
|
const banner = data.bannerImage || data.coverImage.extraLarge;
|
|
document.querySelector('.video-background').innerHTML = `<img src="${banner}" style="width:100%; height:100%; object-fit:cover;">`;
|
|
}
|
|
|
|
// --- MODIFIED EPISODE LOGIC ---
|
|
// Prioritize "nextAiringEpisode" to determine actual released count.
|
|
// If nextAiringEpisode exists (e.g. ep 4), then 3 are out.
|
|
// If it doesn't exist, we assume the show is finished or has no airing data, so use total.
|
|
|
|
if (data.nextAiringEpisode) {
|
|
// Show is currently Airing: use (Next Episode - 1)
|
|
totalEpisodes = data.nextAiringEpisode.episode - 1;
|
|
} else {
|
|
// Show is Finished or data unavailable: use Total Episodes
|
|
totalEpisodes = data.episodes || 12; // fallback
|
|
}
|
|
|
|
// Clamp safe range
|
|
totalEpisodes = Math.min(Math.max(totalEpisodes, 1), 5000);
|
|
|
|
renderEpisodes();
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// --- DESCRIPTION MODAL LOGIC ---
|
|
function handleDescription(text) {
|
|
// Strip HTML tags for sentence counting (rough approximation)
|
|
const tmp = document.createElement("DIV");
|
|
tmp.innerHTML = text;
|
|
const cleanText = tmp.textContent || tmp.innerText || "";
|
|
|
|
// Split by punctuation to count sentences
|
|
const sentences = cleanText.match(/[^\.!\?]+[\.!\?]+/g) || [cleanText];
|
|
|
|
// Full description in modal
|
|
document.getElementById('full-description').innerHTML = text; // Keep HTML for modal
|
|
|
|
if (sentences.length > 4) {
|
|
// Truncate
|
|
const shortText = sentences.slice(0, 4).join(' ');
|
|
document.getElementById('description-preview').innerText = shortText + '...';
|
|
document.getElementById('read-more-btn').style.display = 'inline-flex';
|
|
} else {
|
|
document.getElementById('description-preview').innerHTML = text;
|
|
document.getElementById('read-more-btn').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function openModal() {
|
|
document.getElementById('desc-modal').classList.add('active');
|
|
document.body.style.overflow = 'hidden'; // Prevent scrolling bg
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('desc-modal').classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Close modal on outside click
|
|
document.getElementById('desc-modal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'desc-modal') closeModal();
|
|
});
|
|
|
|
// --- EPISODE LOGIC ---
|
|
|
|
function renderEpisodes() {
|
|
const grid = document.getElementById('episodes-grid');
|
|
grid.innerHTML = '';
|
|
|
|
const start = (currentPage - 1) * itemsPerPage + 1;
|
|
const end = Math.min(start + itemsPerPage - 1, totalEpisodes);
|
|
|
|
for(let i = start; i <= end; i++) {
|
|
createEpisodeButton(i, grid);
|
|
}
|
|
updatePaginationControls();
|
|
}
|
|
|
|
function createEpisodeButton(num, container) {
|
|
const btn = document.createElement('div');
|
|
btn.className = 'episode-btn';
|
|
btn.innerText = `Ep ${num}`;
|
|
btn.onclick = () => window.location.href = `/watch/${animeId}/${num}`;
|
|
container.appendChild(btn);
|
|
}
|
|
|
|
function updatePaginationControls() {
|
|
const totalPages = Math.ceil(totalEpisodes / itemsPerPage);
|
|
document.getElementById('page-info').innerText = `Page ${currentPage} of ${totalPages}`;
|
|
document.getElementById('prev-page').disabled = currentPage === 1;
|
|
document.getElementById('next-page').disabled = currentPage === totalPages;
|
|
|
|
// Show pagination only if searching is not active (search clears pagination view)
|
|
document.getElementById('pagination-controls').style.display = 'flex';
|
|
}
|
|
|
|
function changePage(delta) {
|
|
currentPage += delta;
|
|
renderEpisodes();
|
|
}
|
|
|
|
// Search Logic
|
|
const searchInput = document.getElementById('ep-search');
|
|
searchInput.addEventListener('input', (e) => {
|
|
const val = parseInt(e.target.value);
|
|
const grid = document.getElementById('episodes-grid');
|
|
|
|
if (val > 0 && val <= totalEpisodes) {
|
|
// Show specific result
|
|
grid.innerHTML = '';
|
|
createEpisodeButton(val, grid);
|
|
document.getElementById('pagination-controls').style.display = 'none';
|
|
} else if (!e.target.value) {
|
|
// Restore pagination if empty
|
|
renderEpisodes();
|
|
} else {
|
|
// Invalid input (out of range)
|
|
grid.innerHTML = '<div style="color:#666; width:100%; text-align:center;">Episode not found</div>';
|
|
document.getElementById('pagination-controls').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
loadAnime();
|
|
</script>
|
|
</body>
|
|
</html> |