322 lines
14 KiB
HTML
322 lines
14 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="../public/home.css">
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="navbar" id="navbar">
|
||
<div class="nav-brand">
|
||
<div class="brand-icon">SF</div>
|
||
StreamFlow
|
||
</div>
|
||
|
||
<div class="nav-center">
|
||
<button class="nav-button active" onclick="window.location.href='/'">Anime</button>
|
||
<button class="nav-button" onclick="window.location.href='/books'">Books</button>
|
||
<button class="nav-button" onclick="window.location.href='/art'">Gallery</button>
|
||
<button class="nav-button">Schedule</button>
|
||
<button class="nav-button">My List</button>
|
||
<button class="nav-button">Marketplace</button>
|
||
</div>
|
||
|
||
<div class="search-wrapper">
|
||
<svg class="search-icon" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||
<input type="text" class="search-input" id="search-input" placeholder="Search anime..." autocomplete="off">
|
||
<!-- Search Results Dropdown -->
|
||
<div class="search-results" id="search-results"></div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- HERO -->
|
||
<div class="hero-wrapper">
|
||
<div class="hero-background">
|
||
<img id="hero-bg-media" src="" alt="">
|
||
<div id="player" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 120vw; height: 120vh; pointer-events: none; opacity: 0; transition: opacity 1s;"></div>
|
||
</div>
|
||
<div class="hero-vignette"></div>
|
||
|
||
<div class="hero-content">
|
||
<div class="hero-poster-card">
|
||
<div class="skeleton poster-skeleton" id="hero-poster-skeleton"></div>
|
||
<img id="hero-poster" src="" alt="" style="display: none;" onload="this.style.display='block'; document.getElementById('hero-poster-skeleton').style.display='none'">
|
||
</div>
|
||
<div class="hero-text">
|
||
<div id="hero-loading-ui">
|
||
<div class="skeleton title-skeleton"></div>
|
||
<div class="skeleton text-skeleton" style="width: 40%"></div>
|
||
<div class="skeleton text-skeleton" style="width: 100%; height: 4em;"></div>
|
||
</div>
|
||
|
||
<div id="hero-real-ui" style="display: none;">
|
||
<h1 class="hero-title" id="hero-title"></h1>
|
||
<div class="hero-meta">
|
||
<span class="score-badge" id="hero-score"></span>
|
||
<span id="hero-year"></span>
|
||
<span id="hero-type"></span>
|
||
</div>
|
||
<p class="hero-desc" id="hero-desc"></p>
|
||
<div class="hero-buttons">
|
||
<button class="btn-primary" id="watch-btn">Watch Now</button>
|
||
<button class="btn-blur">+ Add to List</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<main>
|
||
<!-- Trending -->
|
||
<section class="section">
|
||
<div class="section-header"><div class="section-title">Trending This Season</div></div>
|
||
<div class="carousel-wrapper">
|
||
<button class="scroll-btn left" onclick="scrollCarousel('trending', -1)">‹</button>
|
||
<div class="carousel" id="trending">
|
||
<!-- Loading Skeletons -->
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
</div>
|
||
<button class="scroll-btn right" onclick="scrollCarousel('trending', 1)">›</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Top Airing -->
|
||
<section class="section">
|
||
<div class="section-header"><div class="section-title">Top Airing Now</div></div>
|
||
<div class="carousel-wrapper">
|
||
<button class="scroll-btn left" onclick="scrollCarousel('top-airing', -1)">‹</button>
|
||
<div class="carousel" id="top-airing">
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
<div class="card"><div class="card-img-wrap skeleton"></div></div>
|
||
</div>
|
||
<button class="scroll-btn right" onclick="scrollCarousel('top-airing', 1)">›</button>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
// --- SEARCH LOGIC ---
|
||
const searchInput = document.getElementById('search-input');
|
||
const searchResults = document.getElementById('search-results');
|
||
let searchTimeout;
|
||
|
||
searchInput.addEventListener('input', (e) => {
|
||
const query = e.target.value;
|
||
clearTimeout(searchTimeout);
|
||
if (query.length < 2) {
|
||
searchResults.classList.remove('active');
|
||
searchResults.innerHTML = '';
|
||
searchInput.style.borderRadius = '99px';
|
||
return;
|
||
}
|
||
searchTimeout = setTimeout(() => {
|
||
fetchLocalSearch(query);
|
||
}, 300);
|
||
});
|
||
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.search-wrapper')) {
|
||
searchResults.classList.remove('active');
|
||
searchInput.style.borderRadius = '99px';
|
||
}
|
||
});
|
||
|
||
async function fetchLocalSearch(query) {
|
||
try {
|
||
const res = await fetch(`/api/search/local?q=${encodeURIComponent(query)}`);
|
||
const data = await res.json();
|
||
renderSearchResults(data.results);
|
||
} catch (err) { console.error("Search Error:", err); }
|
||
}
|
||
|
||
function renderSearchResults(results) {
|
||
searchResults.innerHTML = '';
|
||
if (results.length === 0) {
|
||
searchResults.innerHTML = '<div style="padding:1rem; color:#888; text-align:center">No results found</div>';
|
||
} else {
|
||
results.forEach(anime => {
|
||
const title = getTitle(anime);
|
||
const img = anime.coverImage.medium || anime.coverImage.large;
|
||
const rating = anime.averageScore ? `${anime.averageScore}%` : 'N/A';
|
||
const year = anime.seasonYear || '';
|
||
const format = anime.format || 'TV';
|
||
|
||
const item = document.createElement('a');
|
||
item.className = 'search-item';
|
||
item.href = `/anime/${anime.id}`; // DIRECT LINK
|
||
item.innerHTML = `
|
||
<img src="${img}" class="search-poster" alt="${title}">
|
||
<div class="search-info">
|
||
<div class="search-title">${title}</div>
|
||
<div class="search-meta">
|
||
<span class="rating-pill">${rating}</span>
|
||
<span>• ${year}</span>
|
||
<span>• ${format}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
searchResults.appendChild(item);
|
||
});
|
||
}
|
||
searchResults.classList.add('active');
|
||
searchInput.style.borderRadius = '12px 12px 0 0';
|
||
}
|
||
|
||
// --- CAROUSEL LOGIC ---
|
||
function scrollCarousel(id, direction) {
|
||
const container = document.getElementById(id);
|
||
if(container) {
|
||
const scrollAmount = container.clientWidth * 0.75;
|
||
container.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
|
||
}
|
||
}
|
||
|
||
// --- MAIN APP LOGIC ---
|
||
let trendingAnimes = [];
|
||
let currentHeroIndex = 0;
|
||
let player;
|
||
let heroInterval;
|
||
|
||
var tag = document.createElement('script');
|
||
tag.src = "https://www.youtube.com/iframe_api";
|
||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||
|
||
function onYouTubeIframeAPIReady() {
|
||
player = new YT.Player('player', {
|
||
height: '100%', width: '100%',
|
||
playerVars: { 'autoplay': 1, 'controls': 0, 'mute': 1, 'loop': 1, 'showinfo': 0, 'modestbranding': 1 },
|
||
events: { 'onReady': (e) => { e.target.mute(); if(trendingAnimes.length) updateHeroVideo(trendingAnimes[currentHeroIndex]); } }
|
||
});
|
||
}
|
||
|
||
async function fetchContent(isUpdate = false) {
|
||
try {
|
||
const trendingRes = await fetch('/api/trending');
|
||
const trendingData = await trendingRes.json();
|
||
|
||
if (trendingData.results && trendingData.results.length > 0) {
|
||
trendingAnimes = trendingData.results;
|
||
if (!isUpdate) {
|
||
updateHeroUI(trendingAnimes[0]);
|
||
startHeroCycle();
|
||
}
|
||
renderList('trending', trendingAnimes);
|
||
} else if (!isUpdate) {
|
||
setTimeout(() => fetchContent(false), 2000);
|
||
}
|
||
|
||
const topRes = await fetch('/api/top-airing');
|
||
const topData = await topRes.json();
|
||
if (topData.results && topData.results.length > 0) {
|
||
renderList('top-airing', topData.results);
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error("Fetch Error:", e);
|
||
if(!isUpdate) setTimeout(() => fetchContent(false), 5000);
|
||
}
|
||
}
|
||
|
||
function startHeroCycle() {
|
||
if(heroInterval) clearInterval(heroInterval);
|
||
heroInterval = setInterval(() => {
|
||
if(trendingAnimes.length > 0) {
|
||
currentHeroIndex = (currentHeroIndex + 1) % trendingAnimes.length;
|
||
updateHeroUI(trendingAnimes[currentHeroIndex]);
|
||
}
|
||
}, 10000);
|
||
}
|
||
|
||
function getTitle(anime) {
|
||
return anime.title.english || anime.title.romaji || "Unknown Title";
|
||
}
|
||
|
||
function updateHeroUI(anime) {
|
||
if(!anime) return;
|
||
const title = getTitle(anime);
|
||
const score = anime.averageScore ? anime.averageScore + '% Match' : 'N/A';
|
||
const year = anime.seasonYear || '';
|
||
const type = anime.format || 'TV';
|
||
const desc = anime.description || 'No description available.';
|
||
const poster = anime.coverImage ? anime.coverImage.extraLarge : '';
|
||
const banner = anime.bannerImage || poster;
|
||
|
||
document.getElementById('hero-title').innerText = title;
|
||
document.getElementById('hero-desc').innerHTML = desc;
|
||
document.getElementById('hero-score').innerText = score;
|
||
document.getElementById('hero-year').innerText = year;
|
||
document.getElementById('hero-type').innerText = type;
|
||
document.getElementById('hero-poster').src = poster;
|
||
|
||
// Set Watch Button Link
|
||
const watchBtn = document.getElementById('watch-btn');
|
||
if(watchBtn) watchBtn.onclick = () => window.location.href = `/anime/${anime.id}`;
|
||
|
||
const bgImg = document.getElementById('hero-bg-media');
|
||
if(bgImg && bgImg.src !== banner) bgImg.src = banner;
|
||
|
||
updateHeroVideo(anime);
|
||
|
||
document.getElementById('hero-loading-ui').style.display = 'none';
|
||
document.getElementById('hero-real-ui').style.display = 'block';
|
||
}
|
||
|
||
function updateHeroVideo(anime) {
|
||
if (!player || !player.loadVideoById) return;
|
||
const videoContainer = document.getElementById('player');
|
||
if (anime.trailer && anime.trailer.site === 'youtube' && anime.trailer.id) {
|
||
if(player.getVideoData && player.getVideoData().video_id !== anime.trailer.id) {
|
||
player.loadVideoById(anime.trailer.id);
|
||
player.mute();
|
||
}
|
||
videoContainer.style.opacity = "1";
|
||
} else {
|
||
videoContainer.style.opacity = "0";
|
||
player.stopVideo();
|
||
}
|
||
}
|
||
|
||
function renderList(id, list) {
|
||
const container = document.getElementById(id);
|
||
const firstId = list.length > 0 ? list[0].id : null;
|
||
const currentFirstId = container.firstElementChild?.dataset?.id;
|
||
if (currentFirstId && parseInt(currentFirstId) === firstId && container.children.length === list.length) {
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = '';
|
||
list.forEach(anime => {
|
||
const title = getTitle(anime);
|
||
const cover = anime.coverImage ? anime.coverImage.large : '';
|
||
const ep = anime.nextAiringEpisode ? 'Ep ' + anime.nextAiringEpisode.episode : (anime.episodes ? anime.episodes + ' Eps' : 'TV');
|
||
const score = anime.averageScore || '--';
|
||
|
||
const el = document.createElement('div');
|
||
el.className = 'card';
|
||
el.dataset.id = anime.id;
|
||
// CLICK GOES TO ANIME PAGE
|
||
el.onclick = () => window.location.href = `/anime/${anime.id}`;
|
||
el.innerHTML = `
|
||
<div class="card-img-wrap"><img src="${cover}" loading="lazy"></div>
|
||
<div class="card-content">
|
||
<h3>${title}</h3>
|
||
<p>${score}% • ${ep}</p>
|
||
</div>
|
||
`;
|
||
container.appendChild(el);
|
||
});
|
||
}
|
||
|
||
fetchContent();
|
||
setInterval(() => fetchContent(true), 60000);
|
||
</script>
|
||
</body>
|
||
</html> |