Files
deploy-test/animex/series-info.html
2026-04-01 23:24:41 -05:00

2220 lines
66 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>Series Info - 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;600;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"
/>
<!-- csPlayer CSS -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/abtp2/csPlayer/src/csPlayer.css"
/>
<!-- csPlayer JS -->
<script src="https://cdn.jsdelivr.net/gh/abtp2/csPlayer/src/csPlayer.js"></script>
<style>
/* =========================================
1. VARIABLES & RESET
========================================= */
:root {
--brand-accent: #ff9500;
--brand-accent-hover: #ffae40;
--background-primary: #0a0a0a;
--background-secondary: #121212;
--background-tertiary: #1a1a1a;
--text-primary: #eaeaea;
--text-secondary: #a0a0a0;
--text-muted: #666666;
--border-color: rgba(255, 255, 255, 0.08); /* Softened from solid gray */
--shadow-color: rgba(0, 0, 0, 0.6);
--border-radius: 16px; /* Modernized from 12px */
--border-radius-sm: 10px;
--transition-duration: 0.3s;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
scroll-behavior: smooth;
}
body {
background-color: var(--background-primary);
color: var(--text-primary);
font-family: "Inter", sans-serif;
line-height: 1.6;
overflow-x: hidden;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--background-primary);
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* =========================================
2. SKELETON LOADERS (PHASE 6)
========================================= */
@keyframes shimmer {
0% { background-position: -1000px 0; }
100% { background-position: 1000px 0; }
}
.skeleton {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.03) 25%,
rgba(255, 255, 255, 0.08) 50%,
rgba(255, 255, 255, 0.03) 75%
);
background-size: 1000px 100%;
animation: shimmer 2s infinite linear;
border-radius: 8px;
}
.skeleton-text {
height: 1em;
width: 100%;
margin-bottom: 0.5rem;
border-radius: 4px;
}
.skeleton-text.short { width: 50%; }
.skeleton-text.medium { width: 75%; }
.skeleton-title { height: 3rem; width: 60%; margin-bottom: 1.2rem; }
.skeleton-poster { width: 100%; height: 100%; position: absolute; inset: 0; }
.skeleton-ep-card { width: 100%; height: 90px; border-radius: 12px; margin-bottom: 12px; }
/* =========================================
3. APP STRUCTURE & HERO
========================================= */
.app-container {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.series-hero-section {
position: relative;
height: 65vh;
min-height: 550px;
background-position: top center;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 80px 5% 3rem;
transition: background-image 0.3s ease;
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
var(--background-primary) 5%,
rgba(10, 10, 10, 0.8) 45%,
rgba(10, 10, 10, 0.4) 75%,
rgba(0, 0, 0, 0.3) 100%
);
z-index: 1;
pointer-events: none;
}
.series-hero-content {
position: relative;
z-index: 2;
width: 100%;
display: flex;
gap: 3rem;
align-items: flex-end;
max-width: 1500px;
margin: 0 auto;
}
/* Poster */
.hero-poster-container {
flex-shrink: 0;
width: 240px;
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8);
border: 1px solid var(--border-color);
background: var(--background-tertiary);
transform: translateY(60px);
display: none;
z-index: 5;
position: relative;
}
.hero-poster-img {
width: 100%;
height: auto;
display: block;
aspect-ratio: 2/3;
object-fit: cover;
position: relative;
z-index: 2;
}
.hero-text-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 20px;
min-width: 0;
}
.series-title-text {
font-size: clamp(2.2rem, 5vw, 4.5rem);
font-weight: 800;
color: var(--text-primary);
text-shadow: 0 4px 30px rgba(0, 0, 0, 0.9);
line-height: 1.1;
margin-bottom: 1.2rem;
letter-spacing: -0.03em;
}
/* Metadata Pills */
.hero-metadata-capsules {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
margin-bottom: 2rem;
font-size: 0.9rem;
font-weight: 600;
}
.meta-capsule {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 16px;
border-radius: 50px;
color: #ddd;
backdrop-filter: blur(12px); /* Enhanced Glassmorphism */
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.rating-pill {
padding: 8px 14px;
border-radius: 50px;
font-weight: 800;
text-transform: uppercase;
font-size: 0.85rem;
background: #333;
color: #fff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.rating-r { background: linear-gradient(135deg, #e53935, #b71c1c); }
.rating-pg { background: linear-gradient(135deg, #fb8c00, #e65100); }
.rating-g { background: linear-gradient(135deg, #43a047, #1b5e20); }
.studio-text {
color: var(--brand-accent);
font-weight: 700;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
}
/* Hero Actions */
.hero-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px;
}
.hero-watch-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
background-color: var(--brand-accent);
color: #000;
padding: 0 32px;
height: 52px;
border-radius: 14px;
border: none;
font-size: 1.05rem;
font-weight: 800;
cursor: pointer;
box-shadow: 0 6px 20px rgba(255, 149, 0, 0.25);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.hero-watch-btn:hover {
background-color: var(--brand-accent-hover);
transform: translateY(-3px) scale(1.02);
box-shadow: 0 10px 30px rgba(255, 149, 0, 0.4);
}
/* New Locate Episode Button (Phase 2) */
.hero-locate-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
color: var(--text-primary);
font-size: 1.2rem;
cursor: pointer;
backdrop-filter: blur(12px);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.hero-locate-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: #fff;
transform: translateY(-3px) scale(1.05);
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.1);
color: var(--brand-accent);
}
/* RX watch button states */
.hero-watch-btn.rx-rated {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), background-color 0.15s ease;
}
.hero-watch-btn.rx-rated:hover {
cursor: not-allowed !important;
background-color: #ff9500;
box-shadow: 0 10px 30px rgba(192, 57, 43, 0.5);
transform: translateY(-3px) scale(1.02);
}
.hero-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-primary);
padding: 0 24px;
height: 52px;
border-radius: 14px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
backdrop-filter: blur(12px);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.hero-action-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px) scale(1.02);
}
.back-btn {
position: absolute;
top: 30px;
left: 30px;
z-index: 10;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.back-btn:hover {
transform: scale(1.1);
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
/* =========================================
4. MAIN CONTENT
========================================= */
.main-content {
position: relative;
padding: 2rem 5%;
background-color: var(--background-primary);
z-index: 3;
}
.desktop-center-wrapper {
max-width: 1500px;
margin: 0 auto;
}
.series-details-section {
margin-top: 2rem;
margin-bottom: 3rem;
}
.details-content-wrapper {
position: relative;
max-height: 140px;
overflow: hidden;
transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.details-content-wrapper::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80px;
background: linear-gradient(
to top,
var(--background-primary),
transparent
);
pointer-events: none;
transition: opacity 0.3s;
}
.details-content-wrapper.expanded {
max-height: 2000px;
}
.details-content-wrapper.expanded::after {
opacity: 0;
}
#series-synopsis {
font-size: 1.05rem;
color: var(--text-secondary);
margin-bottom: 1rem;
line-height: 1.8;
}
.series-genres {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 1.5rem;
}
.genre-tag {
font-size: 0.85rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
padding: 6px 14px;
border-radius: 8px;
color: var(--text-secondary);
transition: all 0.2s;
}
.genre-tag:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.show-more-container {
text-align: center;
margin-top: 1rem;
}
.show-more-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: color 0.2s;
}
.show-more-btn:hover {
color: var(--brand-accent);
}
/* Trailer */
.trailer-section {
margin-top: 3rem;
border-top: 1px solid var(--border-color);
padding-top: 2rem;
margin-bottom: 3rem;
}
.trailer-player-container {
background: #000;
border-radius: var(--border-radius);
overflow: hidden;
aspect-ratio: 16/9;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
}
/* ORANGE THEME #FF9500 */
#video .csPlayer {
--playerBg: #000;
--startBtnBg: #ff9500;
--startBtnIconColor: #fff;
--startBtnSize: 70px;
--sliderSeekTrackColor: #ff9500;
--sliderThumbColor: #ff9500;
--sliderLoadedTrackColor: rgba(255, 149, 0, 0.3);
--sliderBg: rgba(255, 255, 255, 0.2);
--playPauseIconColor: #ff9500;
--forwardIconColor: #ff9500;
--backwardIconColor: #ff9500;
--fullscreenBtnColor: #fff;
--settingsBtnColor: #fff;
--settingsBg: rgba(20, 20, 20, 0.95);
--settingsTextColor: #fff;
--settingsInputIconBg: #ff9500;
--settingsInputIconColor: #fff;
}
/* =========================================
5. EPISODES & SCROLLABLES
========================================= */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
gap: 1rem;
flex-wrap: wrap;
}
.section-title {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-primary);
margin-right: auto;
letter-spacing: -0.01em;
}
/* Episode List Controls */
.episode-controls {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.episode-controls .season-selector-container {
min-width: 0;
}
.episode-controls .season-selector {
width: 100%;
min-width: 0;
}
.layout-toggle-btn {
width: 44px;
height: 44px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
backdrop-filter: blur(5px);
}
.layout-toggle-btn:hover,
.layout-toggle-btn.active {
border-color: rgba(255, 255, 255, 0.3);
color: #fff;
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
/* Season Selector */
.season-selector-container {
position: relative;
min-width: 280px;
max-width: 390px;
}
.season-selector {
width: 100%;
padding: 12px 40px 12px 16px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
color: var(--text-primary);
border: 1px solid var(--border-color);
outline: none;
appearance: none;
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
backdrop-filter: blur(5px);
}
.season-selector:hover,
.season-selector:focus {
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.08);
}
.season-selector-container::after {
content: "\f078";
font-family: "Font Awesome 6 Free";
font-weight: 900;
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
pointer-events: none;
font-size: 0.8rem;
}
/* Episodes - Standard List View */
.episode-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 3rem;
}
.episode-item {
position: relative;
display: flex;
background: var(--background-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
min-height: 90px;
padding-right: 10px;
}
.episode-item:hover {
background: var(--background-tertiary);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
}
/* Episode Locate/Target Animation */
@keyframes target-pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 149, 0, 0.5); border-color: var(--brand-accent); }
70% { box-shadow: 0 0 0 15px rgba(255, 149, 0, 0); border-color: rgba(255,255,255,0.3); }
100% { box-shadow: 0 0 0 0 rgba(255, 149, 0, 0); border-color: var(--border-color); }
}
.episode-item.target-highlight {
animation: target-pulse 1.5s ease-out 2;
border-color: var(--brand-accent);
background: rgba(255, 149, 0, 0.05);
}
/* Progress Bar Styles */
.episode-progress-wrapper {
margin-top: 6px;
width: 100%;
max-width: 220px;
}
.ep-progress-track {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
margin-bottom: 4px;
}
.ep-progress-fill {
height: 100%;
background: var(--brand-accent);
border-radius: 2px;
}
.ep-progress-text {
font-size: 0.75rem;
color: var(--brand-accent);
font-weight: 600;
letter-spacing: 0.5px;
}
.episode-thumbnail {
width: 160px;
background: #000;
position: relative;
overflow: hidden;
flex-shrink: 0;
transition: width 0.3s;
}
.episode-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.8;
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.episode-item:hover .episode-thumbnail img {
transform: scale(1.08);
opacity: 1;
}
/* Text-only list fallback (Thumbnails completely hidden) */
.episode-list.thumbnails-hidden .episode-thumbnail {
display: none;
}
.episode-list.thumbnails-hidden .episode-item {
padding: 14px 20px;
align-items: center;
}
.episode-list.thumbnails-hidden .episode-info {
padding: 0;
}
.episode-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 20px;
min-width: 0;
}
.episode-title {
font-weight: 700;
font-size: 1rem;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
/* Unwatched Badge */
.episode-item.unwatched .episode-title::after {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background: var(--brand-accent);
border-radius: 50%;
margin-left: 8px;
vertical-align: middle;
box-shadow: 0 0 8px rgba(255, 149, 0, 0.6);
}
.episode-title-romanji {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
.episode-action-buttons {
display: flex;
align-items: center;
padding-right: 20px;
opacity: 0.4;
transition: all 0.3s;
}
.episode-item:hover .episode-action-buttons {
opacity: 1;
transform: scale(1.1);
color: var(--brand-accent);
}
.episode-item.watched .episode-title {
color: var(--text-muted);
}
.episode-item.watched::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--brand-accent);
z-index: 2;
}
/* =========================================
6. EPISODES - GRID/TILE VIEW (PHASE 3)
========================================= */
.episode-list.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
.episode-list.grid-view .episode-item {
flex-direction: column;
min-height: auto;
padding: 0;
border-radius: 14px;
}
.episode-list.grid-view .episode-thumbnail {
width: 100%;
aspect-ratio: 16/9;
}
.episode-list.grid-view .episode-info {
padding: 14px;
}
.episode-list.grid-view .episode-title {
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.episode-list.grid-view .episode-action-buttons {
position: absolute;
top: 10px;
right: 10px;
padding: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
width: 36px;
height: 36px;
border-radius: 50%;
justify-content: center;
opacity: 0;
color: #fff;
border: 1px solid rgba(255,255,255,0.1);
}
.episode-list.grid-view .episode-item:hover .episode-action-buttons {
opacity: 1;
background: var(--brand-accent);
color: #000;
border-color: var(--brand-accent);
}
.episode-list.grid-view .episode-item.watched::before {
width: 100%;
height: 4px;
bottom: auto;
left: 0;
top: 0;
}
/* Compact Grid View (Thumbnails Hidden) */
.episode-list.grid-view.thumbnails-hidden {
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 10px;
}
.episode-list.grid-view.thumbnails-hidden .episode-item {
flex-shrink: 0; /* Prevent items from shrinking */
width: 70px; /* Set a fixed width based on minmax from grid */
justify-content: center;
align-items: center;
padding: 12px 6px;
min-height: 44px;
text-align: center;
border-radius: 10px;
}
.episode-list.grid-view.thumbnails-hidden .episode-info {
padding: 0;
}
.episode-list.grid-view.thumbnails-hidden .episode-title {
font-size: 0.9rem;
margin: 0;
white-space: nowrap;
}
.episode-list.grid-view.thumbnails-hidden .episode-title {
display: none; /* Hide the main title */
}
.episode-list.grid-view.thumbnails-hidden .episode-title-romanji {
display: none; /* Hide the "Episode X" romanji text in compact mode */
}
/* Episode number badge shown only in no-thumbnail tile mode */
.episode-list.grid-view.thumbnails-hidden .ep-number-badge {
display: block;
font-size: 1rem;
font-weight: 800;
color: var(--text-primary);
margin: 0;
line-height: 1;
}
.ep-number-badge {
display: none; /* Hidden in all other modes */
}
.episode-list.grid-view.thumbnails-hidden .episode-progress-wrapper,
.episode-list.grid-view.thumbnails-hidden .episode-action-buttons {
display: none;
}
.episode-list.grid-view.thumbnails-hidden .episode-item.watched::before {
display: none;
}
.episode-list.grid-view.thumbnails-hidden .episode-item.watched {
opacity: 0.5;
background: transparent;
}
/* =========================================
7. THEMES SECTION
========================================= */
.theme-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 2rem;
}
.theme-item {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--background-secondary);
padding: 14px 20px;
border-radius: 14px;
border: 1px solid var(--border-color);
transition: all 0.3s;
}
.theme-item:hover {
border-color: rgba(255, 255, 255, 0.2);
background: var(--background-tertiary);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.theme-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
min-width: 0;
margin-right: 16px;
}
.theme-type {
font-size: 0.75rem;
color: var(--brand-accent);
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.theme-title {
font-size: 1rem;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theme-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.theme-btn {
width: 38px;
height: 38px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
font-size: 0.9rem;
backdrop-filter: blur(5px);
}
.theme-btn:hover {
background: #fff;
color: #000;
border-color: #fff;
transform: scale(1.1);
}
.theme-btn.play {
border-color: var(--brand-accent);
color: var(--brand-accent);
background: rgba(255, 149, 0, 0.1);
}
.theme-btn.play:hover {
background: var(--brand-accent);
color: #000;
border-color: var(--brand-accent);
box-shadow: 0 0 15px rgba(255, 149, 0, 0.4);
}
/* =========================================
8. MOVIE CARD
========================================= */
.movie-player-card {
background: var(--background-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
overflow: hidden;
position: relative;
aspect-ratio: 21 / 9;
max-height: 600px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-bottom: 3rem;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.movie-player-card:hover {
transform: scale(1.02);
border-color: rgba(255, 255, 255, 0.2);
}
.movie-backdrop {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.6;
transition: 0.5s;
}
.movie-player-card:hover .movie-backdrop {
opacity: 0.4;
transform: scale(1.05);
}
.movie-player-card::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.9) 0%,
rgba(0, 0, 0, 0.2) 60%,
rgba(0, 0, 0, 0.4) 100%
);
}
.movie-play-overlay {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
text-align: center;
}
.movie-play-btn {
width: 90px;
height: 90px;
border-radius: 50%;
background: rgba(255, 149, 0, 0.9);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
padding-left: 8px;
backdrop-filter: blur(10px);
box-shadow: 0 0 0 0 rgba(255, 149, 0, 0.7);
animation: pulse-orange 2s infinite;
transition: transform 0.3s;
}
@keyframes pulse-orange {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 149, 0, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 20px rgba(255, 149, 0, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 149, 0, 0); }
}
.movie-player-card:hover .movie-play-btn {
background: #fff;
color: var(--brand-accent);
animation: none;
transform: scale(1.1);
}
.movie-label {
font-size: 1.8rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
pointer-events: none;
}
/* =========================================
9. HORIZONTAL SCROLL SECTIONS
========================================= */
.horizontal-scroll-section {
margin-bottom: 4rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.horizontal-list {
display: flex;
gap: 1.2rem;
overflow-x: auto;
padding-bottom: 1.5rem;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding-right: 5%;
}
/* Characters */
.character-card {
flex: 0 0 160px;
background: var(--background-secondary);
border: 1px solid var(--border-color);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
transition: transform 0.3s;
}
.character-card:hover {
transform: translateY(-4px);
border-color: rgba(255,255,255,0.2);
}
.char-card-imgs { display: flex; height: 150px; }
.char-card-imgs img { width: 50%; height: 100%; object-fit: cover; }
.char-info { padding: 12px; text-align: center; font-size: 0.85rem; border-top: 1px solid var(--border-color); }
.char-name { font-weight: 700; display: block; line-height: 1.2; margin-bottom: 4px; color: #fff; }
.va-name { color: var(--text-secondary); font-size: 0.75rem; font-weight: 500; }
.char-switcher { display: flex; gap: 8px; }
.char-lang-btn {
background: rgba(255, 255, 255, 0.05);
color: var(--text-secondary);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 16px;
font-size: 0.85rem;
border-radius: 8px;
cursor: pointer;
font-weight: 700;
transition: all 0.3s;
backdrop-filter: blur(5px);
}
.char-lang-btn:hover { background: rgba(255, 255, 255, 0.15); color: #fff; }
.char-lang-btn.active { background: #fff; color: #000; border-color: #fff; }
/* TILE CARD */
.tile-card {
flex: 0 0 170px;
display: flex;
flex-direction: column;
gap: 12px;
text-decoration: none;
transition: transform 0.3s;
}
.tile-card:hover { transform: translateY(-6px); }
.tile-img-container {
width: 100%;
aspect-ratio: 2 / 3;
border-radius: 14px;
overflow: hidden;
background: var(--background-tertiary);
position: relative;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.tile-img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s; }
.tile-card:hover .tile-img { transform: scale(1.08); }
.tile-tag {
position: absolute;
top: 10px; left: 10px;
background: var(--brand-accent);
color: #000;
font-size: 0.7rem;
padding: 4px 10px;
border-radius: 6px;
font-weight: 800;
text-transform: uppercase;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
}
.tile-title {
font-size: 0.95rem;
font-weight: 600;
color: #ddd;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: color 0.3s;
}
.tile-card:hover .tile-title { color: #fff; }
/* Mobile Tabs */
.tabs-container { display: none; }
/* =========================================
10. MEDIA QUERIES
========================================= */
@media (min-width: 1024px) {
.series-hero-section { height: 70vh; padding-top: 120px; }
.hero-poster-container { display: block; margin-bottom: 100px; z-index: 5; }
.series-details-section { margin-top: 5rem; }
}
@media (max-width: 1023px) {
.series-hero-section { padding-top: 120px; height: auto; min-height: auto; padding-bottom: 2rem; }
.hero-poster-container {
display: block; width: 180px; transform: none;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
background: transparent;
margin: 0 auto 1.5rem auto;
}
.series-hero-content { flex-direction: column; align-items: center; gap: 1rem; text-align: center; }
.hero-text-content { width: 100%; align-items: center; }
.series-title-text { font-size: 2.2rem; margin-top: 0; text-align: center; line-height: 1.1; }
.hero-poster-img { border-radius: 14px; }
.hero-metadata-capsules { justify-content: center; gap: 8px; }
.meta-capsule { font-size: 0.8rem; padding: 6px 12px; }
.hero-actions { width: 100%; display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 10px; }
.hero-watch-btn { width: 100%; order: 1; margin-bottom: 0; height: 56px; font-size: 1.1rem; }
.hero-locate-btn { flex: none; order: 2; height: 50px; width: 50px; }
.hero-action-btn { flex: 1; order: 3; height: 50px; font-size: 0.9rem; }
.episode-controls {
width: 100%;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.episode-controls .season-selector-container {
flex: 1 1 170px;
max-width: 100%;
}
.tabs-container {
display: flex; gap: 1.5rem; border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem; overflow-x: auto; padding-left: 5%;
}
.tab-btn {
padding: 1rem 0.5rem; font-size: 1rem; font-weight: 600;
color: var(--text-secondary); background: none; border: none;
white-space: nowrap; position: relative; transition: color 0.3s;
}
.tab-btn.active { color: var(--text-primary); }
.tab-btn.active::after {
content: ""; position: absolute; bottom: 0; left: 0; right: 0;
height: 3px; background: var(--brand-accent); border-radius: 3px 3px 0 0;
}
.horizontal-scroll-section.mobile-tab-content { display: none; margin-bottom: 2rem; }
.horizontal-scroll-section.mobile-tab-content.active { display: block; }
.episode-list-section.mobile-tab-content { display: none; }
.episode-list-section.mobile-tab-content.active { display: block; }
.movie-player-card { aspect-ratio: 16/9; }
.movie-play-btn { width: 70px; height: 70px; font-size: 2rem; }
.episode-popup-content { width: 100%; height: 100%; max-width: none; aspect-ratio: unset; border-radius: 0; border: none; box-shadow: none; }
.popup-close { top: 20px; right: 20px; }
}
/* =========================================
11. POPUP / MODALS
========================================= */
.episode-popup-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 9999;
display: none; align-items: center; justify-content: center;
backdrop-filter: blur(15px); margin: 0; border-radius: 0;
}
.episode-popup-overlay.active { display: flex; }
.episode-popup-content {
width: 95%; max-width: 1400px; aspect-ratio: 16/9;
background: #000; position: relative; border-radius: 20px;
overflow: hidden; box-shadow: 0 30px 80px rgba(0, 0, 0, 0.8);
border: 1px solid rgba(255,255,255,0.1);
}
.popup-close {
position: absolute; top: 20px; right: 20px; z-index: 20; color: #fff;
width: 44px; height: 44px; border-radius: 50%;
background: rgba(0, 0, 0, 0.6); border: 1px solid rgba(255, 255, 255, 0.2);
cursor: pointer; display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; backdrop-filter: blur(8px); transition: all 0.3s;
}
.popup-close:hover { background: #fff; color: #000; transform: rotate(90deg) scale(1.1); }
iframe { width: 100%; height: 100%; border: none; }
/* Shrink player modal on 1080p / shorter screens */
@media (max-height: 900px) and (min-width: 1024px) {
.episode-popup-content {
width: 88%;
max-width: 1200px;
aspect-ratio: unset;
height: 82vh;
max-height: 720px;
}
}
/* =========================================
12. RX GATE MODAL (PHASE 1 MODERNIZATION)
========================================= */
.rx-gate-overlay {
position: fixed;
inset: 0;
z-index: 99999;
background: rgba(0, 0, 0, 0.4);
/* Heavy blur and dim */
backdrop-filter: blur(25px) brightness(0.3);
-webkit-backdrop-filter: blur(25px) brightness(0.3);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.rx-gate-overlay.visible {
opacity: 1;
pointer-events: all;
}
.rx-gate-card {
background: rgba(20, 20, 20, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
padding: 3.5rem 2.5rem;
max-width: 480px;
width: 100%;
text-align: center;
/* Subtle crimson glow */
box-shadow: 0 20px 60px rgba(192, 57, 43, 0.15), 0 0 100px rgba(0,0,0,0.8);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
transform: translateY(40px) scale(0.9);
transition: transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.4s ease;
opacity: 0;
}
.rx-gate-overlay.visible .rx-gate-card {
transform: translateY(0) scale(1);
opacity: 1;
}
.rx-gate-icon {
font-size: 3.5rem;
margin-bottom: 1.2rem;
display: block;
line-height: 1;
}
.rx-gate-badge {
display: inline-block;
background: linear-gradient(135deg, rgba(229, 57, 53, 0.2), rgba(183, 28, 28, 0.2));
border: 1px solid rgba(229, 57, 53, 0.3);
color: #ff5252;
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 1.5px;
text-transform: uppercase;
padding: 6px 16px;
border-radius: 50px;
margin-bottom: 1.6rem;
}
.rx-gate-title {
font-size: 1.6rem;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 0.8rem;
letter-spacing: -0.02em;
line-height: 1.2;
}
.rx-gate-subtitle {
font-size: 0.95rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 2.5rem;
}
.rx-gate-actions {
display: flex;
gap: 14px;
justify-content: center;
flex-wrap: wrap;
}
.rx-gate-btn-yes {
flex: 1;
min-width: 140px;
padding: 14px 24px;
border-radius: 14px;
border: 1px solid rgba(192, 57, 43, 0.3);
background: rgba(192, 57, 43, 0.1);
color: #e57373;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
font-family: inherit;
}
.rx-gate-btn-yes:hover {
background: rgba(192, 57, 43, 0.25);
border-color: #e53935;
color: #fff;
transform: translateY(-2px);
}
.rx-gate-btn-no {
flex: 1;
min-width: 140px;
padding: 14px 24px;
border-radius: 14px;
border: none;
background: var(--brand-accent);
color: #000;
font-size: 0.95rem;
font-weight: 800;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
font-family: inherit;
}
.rx-gate-btn-no:hover {
background: var(--brand-accent-hover);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(255,149,0,0.35);
}
.rx-gate-disclaimer {
margin-top: 2rem;
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.5;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Back Button -->
<button id="back-button" class="back-btn" style="display: none">
<i class="fas fa-arrow-left"></i>
</button>
<!-- Hero Section -->
<header class="series-hero-section" id="hero-section">
<div class="series-hero-content">
<!-- Poster with Skeleton -->
<div class="hero-poster-container" id="poster-container">
<div class="skeleton skeleton-poster" id="poster-skeleton"></div>
<img id="hero-poster-img" class="hero-poster-img" src="" alt="Poster" style="display: none;" />
</div>
<div class="hero-text-content">
<!-- Title Skeleton -->
<div id="title-skeleton" class="skeleton skeleton-title"></div>
<h1 class="series-title-text" id="series-title" style="display: none;"></h1>
<!-- Meta Skeletons -->
<div class="hero-metadata-capsules" id="meta-container">
<div class="skeleton skeleton-text" style="width: 300px; height: 35px; border-radius: 50px;"></div>
</div>
<!-- Hero Actions -->
<div class="hero-actions">
<!-- Main Play -->
<button id="hero-watch-btn" class="hero-watch-btn" style="display: none">
<i class="fas fa-play"></i> <span>Start Watching</span>
</button>
<!-- Locate Episode (Phase 2) -->
<button id="hero-locate-btn" class="hero-locate-btn" title="Jump to Current Episode" style="display: none;">
<i class="fas fa-crosshairs"></i>
</button>
<button class="hero-action-btn"><i class="fas fa-bookmark"></i> <span>Add to List</span></button>
<button class="hero-action-btn"><i class="fas fa-download"></i> <span>Export</span></button>
</div>
</div>
</div>
<div class="hero-overlay"></div>
</header>
<main class="main-content">
<div class="desktop-center-wrapper">
<!-- Synopsis Section -->
<section class="series-details-section">
<div id="details-content-wrapper" class="details-content-wrapper">
<div id="synopsis-skeleton">
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text medium"></div>
</div>
<p id="series-synopsis" style="display: none;"></p>
<div class="series-genres" id="genres-container"></div>
</div>
<div class="show-more-container">
<button id="show-more-btn" class="show-more-btn">
Show More <i class="fas fa-chevron-down"></i>
</button>
</div>
<!-- Trailer -->
<div id="trailer-section" class="trailer-section" style="display: none;">
<h3 class="section-title" style="margin-bottom: 1.5rem; font-size: 1.2rem; opacity: 0.8;">Official Trailer</h3>
<div id="trailer-player" class="trailer-player-container">
<div id="video"></div>
</div>
</div>
<!-- Tabs (Mobile Only) -->
<div class="tabs-container">
<button class="tab-btn active" data-tab="episodes">Episodes</button>
<button class="tab-btn" data-tab="themes">Music</button>
<button class="tab-btn" data-tab="related">Related</button>
<button class="tab-btn" data-tab="recommendations">Recs</button>
</div>
<div class="content-flow">
<!-- 1. Episodes List -->
<section id="tab-panel-episodes" class="episode-list-section mobile-tab-content active">
<div class="section-header">
<h2 class="section-title">Episodes</h2>
<div class="episode-controls">
<!-- Season Dropdown -->
<div class="season-selector-container" id="season-selector-wrapper" style="display: none">
<select id="season-selector" class="season-selector"></select>
</div>
<!-- Layout Toggle Buttons (Phase 3) -->
<button id="grid-toggle-btn" class="layout-toggle-btn" title="Toggle Grid/List View">
<i class="fas fa-th-large"></i>
</button>
<button id="thumbnail-toggle-btn" class="layout-toggle-btn" title="Show/Hide Thumbnails">
<i class="fas fa-image"></i>
</button>
</div>
</div>
<!-- Episode List Container (Grid-view or List-view handled by JS) -->
<div class="episode-list" id="episode-list-container">
<!-- Skeleton placeholders for episodes -->
<div class="skeleton skeleton-ep-card"></div>
<div class="skeleton skeleton-ep-card"></div>
<div class="skeleton skeleton-ep-card"></div>
<div class="skeleton skeleton-ep-card"></div>
</div>
<!-- Movie Mode Display -->
<div id="movie-player-card" class="movie-player-card" style="display: none">
<img id="movie-card-backdrop" class="movie-backdrop" src="" alt="Backdrop" />
<div class="movie-play-overlay">
<div class="movie-play-btn"><i class="fas fa-play"></i></div>
<span class="movie-label">Play Movie</span>
</div>
</div>
<div id="episodes-pagination" style="display: flex; justify-content: center; margin-top: 2rem"></div>
</section>
<!-- 2. Characters -->
<section class="characters-section horizontal-scroll-section" id="characters-section" style="display: none">
<div class="section-header">
<h3 class="section-title">Characters</h3>
<div class="char-switcher">
<button class="char-lang-btn active" data-lang="JAPANESE">JP</button>
<button class="char-lang-btn" data-lang="ENGLISH">EN</button>
</div>
</div>
<div class="characters-list horizontal-list" id="characters-list"></div>
</section>
<!-- 3. Themes / Music -->
<section id="tab-panel-themes" class="horizontal-scroll-section mobile-tab-content">
<h3 class="section-title" style="margin-bottom: 1.5rem">Music Themes</h3>
<div id="themes-list-container" class="theme-list">
<div class="skeleton skeleton-ep-card" style="height: 60px;"></div>
<div class="skeleton skeleton-ep-card" style="height: 60px;"></div>
</div>
</section>
<!-- 4. Related Series -->
<section id="tab-panel-related" class="horizontal-scroll-section mobile-tab-content">
<h3 class="section-title" style="margin-bottom: 1.5rem">Related Content</h3>
<div id="related-container" class="horizontal-list"></div>
</section>
<!-- 5. Recommendations -->
<section id="tab-panel-recommendations" class="horizontal-scroll-section mobile-tab-content">
<h3 class="section-title" style="margin-bottom: 1.5rem">You Might Also Like</h3>
<div id="recommendations-grid" class="horizontal-list"></div>
</section>
</div>
</div>
</div>
</main>
</div>
<!-- Video Popup -->
<div id="episode-popup-overlay" class="episode-popup-overlay">
<div class="episode-popup-content">
<button class="popup-close" title="Close Player"><i class="fas fa-times"></i></button>
<iframe id="episode-popup-iframe" src="" allowfullscreen></iframe>
</div>
</div>
<!-- RX Gate Modal (Phase 1 Modernized) -->
<div id="rx-gate-overlay" class="rx-gate-overlay">
<div class="rx-gate-card">
<div class="rx-gate-badge">Restricted Content</div>
<h2 class="rx-gate-title">Mature Audience Only</h2>
<p class="rx-gate-subtitle">This series contains adult themes. By continuing, you confirm you are of legal age and wish to proceed.</p>
<div class="rx-gate-actions">
<button class="rx-gate-btn-no" id="rx-gate-no">Take Me Back</button>
<button class="rx-gate-btn-yes" id="rx-gate-yes">Yes, I'm Sure</button>
</div>
<p class="rx-gate-disclaimer">I know, I really thought you would do better...</p>
</div>
</div>
<!-- JS Scripts are placed here as usual -->
<script src="https://www.youtube.com/iframe_api"></script>
<script>
// --- CONFIG & STATE ---
const urlParams = new URLSearchParams(window.location.search);
const currentMalId = urlParams.get("id");
const extensionIp = localStorage.getItem("extension_server_ip") || "localhost";
const serverUrl = ``; // Assumes same-origin or configured proxy
const THEMES_API = serverUrl + "/api/themes";
let animeDetails = {};
let episodesData = [];
let characterData = [];
let isMovie = false;
let isRxRated = false;
let rxGateAccepted = false;
// Layout State (Phase 3)
let currentLayout = localStorage.getItem("animex_layout") || "list-view"; // list-view or grid-view
let hideThumbnails = localStorage.getItem("hideThumbnails") === "true";
let currentEpRange = [1, 100];
// Watch History
let localWatchHistory = JSON.parse(localStorage.getItem("animex_watch_history") || "{}");
// --- INITIALIZATION ---
document.addEventListener("DOMContentLoaded", () => {
if (!currentMalId) return;
// Back button visibility
const isAnime = urlParams.get("anime") === "true";
initLayoutButtons();
initTabSystem();
initRxGate();
loadData();
// Show More Synopsis
document.getElementById("show-more-btn").onclick = () => {
document.getElementById("details-content-wrapper").classList.toggle("expanded");
};
// Close Popup
document.querySelector(".popup-close").onclick = () => {
closePlayerPopup();
};
});
// --- CORE DATA LOADING ---
async function loadData() {
toggleSkeletons(true);
try {
// 1. Fetch Anime Details (Jikan)
const detRes = await fetch(`/proxy?url=https://api.jikan.moe/v4/anime/${currentMalId}/full`);
const resJson = await detRes.json();
animeDetails = resJson.data;
if (!animeDetails) throw new Error("Anime not found");
renderDetails(animeDetails);
// 2. Load parallel data
await Promise.all([
fetchAndRenderEpisodes(),
fetchSeasons(),
fetchCharacters(),
fetchThemes(),
fetchRecs(),
fetchRelated()
]);
toggleSkeletons(false);
} catch (e) {
console.error("Data Load Error:", e);
document.getElementById("series-synopsis").textContent = "Error loading content. Please refresh.";
document.getElementById("synopsis-skeleton").style.display = "none";
document.getElementById("series-synopsis").style.display = "block";
}
}
function toggleSkeletons(show) {
const skeletons = document.querySelectorAll(".skeleton");
const content = [
"hero-poster-img", "series-title", "series-synopsis",
"hero-watch-btn", "hero-locate-btn"
];
skeletons.forEach(s => s.style.display = show ? "block" : "none");
content.forEach(id => {
const el = document.getElementById(id);
if (el) el.style.display = show ? "none" : (id.includes('btn') ? "inline-flex" : "block");
});
if (!show) {
document.getElementById("poster-skeleton").style.display = "none";
document.getElementById("synopsis-skeleton").style.display = "none";
}
}
// --- UI RENDERING ---
function renderDetails(anime) {
isMovie = anime.type === "Movie";
const heroSection = document.getElementById("hero-section");
// Background Image logic
heroSection.style.backgroundImage = `url('${anime.images.jpg.large_image_url}')`;
fetch(`/anime/${currentMalId}/banner`)
.then(r => { if (!r.ok) throw new Error("no banner"); return r.blob(); })
.then(blob => {
heroSection.style.backgroundImage = `url('${URL.createObjectURL(blob)}')`;
})
.catch(() => {
// Fallback: for movies, try the movie thumbnail endpoint as hero background
if (isMovie) {
fetch(`${serverUrl}/anime/${currentMalId}/movie/thumbnail`)
.then(r => r.json())
.then(d => {
const fallback = d.cover_url || d.thumbnail_url;
if (fallback) heroSection.style.backgroundImage = `url('${fallback}')`;
})
.catch(() => {});
}
});
document.getElementById("hero-poster-img").src = anime.images.jpg.image_url;
document.getElementById("series-title").textContent = anime.title_english || anime.title;
// Meta Capsules
const metaContainer = document.getElementById("meta-container");
metaContainer.innerHTML = "";
if (anime.rating) {
const rating = anime.rating.split(" ")[0];
const rClass = rating.includes("R") ? "rating-r" : (rating.includes("PG") ? "rating-pg" : "rating-g");
metaContainer.innerHTML += `<span class="rating-pill ${rClass}">${rating}</span>`;
if (anime.rating.toLowerCase().includes("rx")) {
isRxRated = true;
if (!rxGateAccepted) showRxGate();
}
}
metaContainer.innerHTML += `
<span class="meta-capsule"><i class="fas fa-calendar"></i> ${anime.year || 'N/A'}</span>
<span class="meta-capsule"><i class="fas fa-tv"></i> ${anime.type}</span>
<span class="meta-capsule">${anime.status}</span>
${anime.studios.length ? `<span class="studio-text">${anime.studios[0].name}</span>` : ''}
`;
document.getElementById("series-synopsis").textContent = anime.synopsis;
// Genres
const genreWrap = document.getElementById("genres-container");
genreWrap.innerHTML = "";
anime.genres.forEach(g => {
const span = document.createElement("span");
span.className = "genre-tag";
span.textContent = g.name;
genreWrap.appendChild(span);
});
// Trailer
if (anime.trailer?.youtube_id) {
document.getElementById("trailer-section").style.display = "block";
initYoutubePlayer(anime.trailer.youtube_id);
}
}
// --- EPISODE LOGIC (Phase 3 & 4) ---
async function fetchAndRenderEpisodes() {
let episodes = [];
try {
const mapRes = await fetch(`${serverUrl}/map/mal/${currentMalId}`);
if (mapRes.ok) {
const mapData = await mapRes.json();
if (mapData.kitsu_id) {
// Optimized paging for kitsu (Handles 1000+ eps)
let url = `/proxy?url=https://kitsu.io/api/edge/anime/${mapData.kitsu_id}/episodes?page[limit]=20`;
let count = 0;
while (url && count < 60) { // Safety cap for non-cached sessions
const r = await fetch(url);
const d = await r.json();
episodes = [...episodes, ...d.data];
url = d.links?.next;
count++;
}
}
}
} catch (e) {}
// Fallback if Kitsu/Map fails
if (episodes.length === 0) {
const epCount = animeDetails.episodes || 1;
for (let i = 1; i <= epCount; i++) {
episodes.push({ attributes: { number: i, canonicalTitle: `Episode ${i}`, thumbnail: null } });
}
// 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();
}
function formatTimestamp(seconds) {
seconds = Math.floor(seconds); // handle fractional seconds
if (seconds < 60) {
return `${seconds}s`;
}
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
if (seconds < 3600) { // less than an hour
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
const hrs = Math.floor(seconds / 3600);
const remainingMins = mins % 60;
return `${String(hrs).padStart(2, '0')}:${String(remainingMins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
function renderEpisodes() {
const list = document.getElementById("episode-list-container");
const movieCard = document.getElementById("movie-player-card");
if (isMovie) {
list.style.display = "none";
movieCard.style.display = "flex";
// Progress label
const hist = localWatchHistory[currentMalId]?.[1];
const movieLabel = document.querySelector(".movie-label");
movieLabel.innerHTML = "Play Movie";
if (hist?.timestamp > 0 && hist?.state !== "finished") {
// Use saved duration first, then parse from Jikan's duration string, then fallback to 90min
const duration = hist.duration || parseAnimeDuration(animeDetails?.duration) || 5400;
movieLabel.innerHTML = `Resume <span style="font-size:0.6em; color:var(--brand-accent)">(${formatTimestamp(hist.timestamp)})</span>`;
}
// Fetch backdrop thumbnail from server
fetch(`${serverUrl}/anime/${currentMalId}/movie/thumbnail`)
.then(r => r.json())
.then(d => {
document.getElementById("movie-card-backdrop").src = d.thumbnail_url || d.cover_url || "";
})
.catch(() => {
// Fallback to hero background image
const heroBg = document.getElementById("hero-section").style.backgroundImage;
if (heroBg) document.getElementById("movie-card-backdrop").src = heroBg.slice(5, -2);
});
movieCard.onclick = () => openPlayer(1);
updateWatchButton("Movie");
return;
}
list.className = `episode-list ${currentLayout} ${hideThumbnails ? 'thumbnails-hidden' : ''}`;
list.innerHTML = "";
const maxEp = Math.max(...episodesData.map(e => e.attributes.number));
// Handle Range Dropdown for Long Series (Phase 4)
if (maxEp > 100) {
ensureRangeDropdown(maxEp);
}
const filtered = episodesData.filter(e => e.attributes.number >= currentEpRange[0] && e.attributes.number <= currentEpRange[1]);
filtered.forEach(ep => {
const num = ep.attributes.number;
const hist = localWatchHistory[currentMalId]?.[num];
const isFinished = hist?.state === "finished";
const isUnwatched = !hist;
const item = document.createElement("div");
item.className = `episode-item ${isFinished ? 'watched' : ''} ${isUnwatched ? 'unwatched' : ''}`;
item.id = `ep-card-${num}`;
let progressHtml = "";
if (hist?.timestamp > 0 && !isFinished) {
const pct = (hist.timestamp / (hist.duration || 1440)) * 100;
progressHtml = `
<div class="episode-progress-wrapper">
<div class="ep-progress-track"><div class="ep-progress-fill" style="width:${pct}%"></div></div>
</div>`;
}
const thumbUrl = ep.attributes.thumbnail?.original ? `/proxy-image?url=${ep.attributes.thumbnail.original}` : 'https://placehold.co/320x180/111/333?text=No+Image';
item.innerHTML = `
<div class="episode-thumbnail"><img src="${thumbUrl}" loading="lazy"></div>
<div class="episode-info">
<div class="episode-title">${ep.attributes.canonicalTitle || 'Episode ' + num}</div>
<div class="episode-title-romanji">Episode ${num}</div>
<span class="ep-number-badge">${num}</span>
${progressHtml}
</div>
<div class="episode-action-buttons"><i class="fas ${isFinished ? 'fa-rotate-right' : 'fa-play'}"></i></div>
`;
item.onclick = () => openPlayer(num);
list.appendChild(item);
});
updateWatchButton("Anime");
}
// --- PHASE 2: LOCATE LOGIC ---
function setupLocateButton() {
const btn = document.getElementById("hero-locate-btn");
const hist = localWatchHistory[currentMalId] || {};
let lastEp = 0;
Object.keys(hist).forEach(k => { if(parseInt(k) > lastEp) lastEp = parseInt(k); });
if (lastEp === 0) {
btn.style.display = "none";
return;
}
btn.style.display = "inline-flex";
btn.onclick = () => {
let target = lastEp;
if (hist[target]?.state === "finished") target++;
// 1. Calculate and switch range if needed
const rangeStart = Math.floor((target - 1) / 100) * 100 + 1;
const rangeEnd = rangeStart + 99;
if (currentEpRange[0] !== rangeStart) {
currentEpRange = [rangeStart, rangeEnd];
const selector = document.getElementById("ep-range-select");
if (selector) selector.value = `${rangeStart}-${rangeEnd}`;
renderEpisodes();
}
// 2. Scroll and Highlight
setTimeout(() => {
const el = document.getElementById(`ep-card-${target}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add("target-highlight");
setTimeout(() => el.classList.remove("target-highlight"), 3000);
}
}, 100);
};
}
// --- PHASE 3: LAYOUT TOGGLES ---
function initLayoutButtons() {
const gridBtn = document.getElementById("grid-toggle-btn");
const thumbBtn = document.getElementById("thumbnail-toggle-btn");
const updateBtns = () => {
gridBtn.innerHTML = currentLayout === "grid-view" ? '<i class="fas fa-list"></i>' : '<i class="fas fa-th-large"></i>';
gridBtn.classList.toggle("active", currentLayout === "grid-view");
thumbBtn.classList.toggle("active", !hideThumbnails);
};
gridBtn.onclick = () => {
currentLayout = currentLayout === "list-view" ? "grid-view" : "list-view";
localStorage.setItem("animex_layout", currentLayout);
updateBtns();
renderEpisodes();
};
thumbBtn.onclick = () => {
hideThumbnails = !hideThumbnails;
localStorage.setItem("hideThumbnails", hideThumbnails);
updateBtns();
renderEpisodes();
};
updateBtns();
}
function ensureRangeDropdown(maxEp) {
let dropdown = document.getElementById("ep-range-select");
if (!dropdown) {
dropdown = document.createElement("select");
dropdown.id = "ep-range-select";
dropdown.className = "season-selector"; // Reuse styles
dropdown.style.marginBottom = "1.5rem";
dropdown.style.width = "auto";
document.getElementById("episode-list-container").before(dropdown);
dropdown.onchange = (e) => {
currentEpRange = e.target.value.split("-").map(Number);
renderEpisodes();
};
}
dropdown.innerHTML = "";
for (let i = 1; i <= maxEp; i += 100) {
const end = Math.min(i + 99, maxEp);
const opt = document.createElement("option");
opt.value = `${i}-${end}`;
opt.textContent = `Episodes ${i} - ${end}`;
if (currentEpRange[0] === i) opt.selected = true;
dropdown.appendChild(opt);
}
}
// --- WATCH HISTORY & PLAYER ---
function updateWatchButton(mode) {
const btn = document.getElementById("hero-watch-btn");
const hist = localWatchHistory[currentMalId] || {};
let lastEp = 0;
Object.keys(hist).forEach(k => { if(parseInt(k) > lastEp) lastEp = parseInt(k); });
let targetEp = 1;
let label = "Start Watching";
if (mode === "Movie") {
targetEp = 1;
label = lastEp === 1 ? "Resume Movie" : "Watch Movie";
} else {
targetEp = lastEp > 0 ? lastEp : 1;
if (hist[targetEp]?.state === "finished") targetEp++;
label = lastEp > 0 ? `Continue Ep ${targetEp}` : `Start Ep 1`;
}
btn.innerHTML = `<i class="fas fa-play"></i> <span>${label}</span>`;
btn.onclick = () => {
if (isRxRated && !rxGateAccepted) {
showRxGate();
document.getElementById("rx-gate-yes").onclick = () => {
rxGateAccepted = true;
dismissRxGate();
openPlayer(targetEp);
};
} else {
openPlayer(targetEp);
}
};
}
function openPlayer(ep) {
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() {
document.getElementById("episode-popup-overlay").classList.remove("active");
document.getElementById("episode-popup-iframe").src = "";
// Sync history on close
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 ---
function initRxGate() {
document.getElementById("rx-gate-no").onclick = () => window.history.back();
document.getElementById("rx-gate-yes").onclick = () => {
rxGateAccepted = true;
dismissRxGate();
};
}
function showRxGate() {
const overlay = document.getElementById("rx-gate-overlay");
overlay.style.display = "flex";
setTimeout(() => overlay.classList.add("visible"), 50);
}
function dismissRxGate() {
const overlay = document.getElementById("rx-gate-overlay");
overlay.classList.remove("visible");
setTimeout(() => overlay.style.display = "none", 400);
}
// --- THEMES & MUSIC ---
async function fetchThemes() {
const container = document.getElementById("themes-list-container");
try {
const res = await fetch(`${THEMES_API}/${currentMalId}`);
const themes = await res.json();
if (!themes.length) {
container.innerHTML = "<p class='text-muted'>No themes found.</p>";
return;
}
container.innerHTML = "";
themes.forEach(t => {
const div = document.createElement("div");
div.className = "theme-item";
div.innerHTML = `
<div class="theme-info">
<span class="theme-type">${t.slug}</span>
<span class="theme-title">${t.title}</span>
</div>
<div class="theme-actions">
<button class="theme-btn play" onclick="playTheme('${t.url}', '${t.title.replace(/'/g, "\\'")}')"><i class="fas fa-play"></i></button>
<button class="theme-btn" onclick="downloadTheme('${t.url}')"><i class="fas fa-download"></i></button>
</div>
`;
container.appendChild(div);
});
} catch (e) { container.innerHTML = ""; }
}
window.playTheme = (url, title) => {
const track = { url, title, anime: animeDetails.title, image: animeDetails.images.jpg.large_image_url };
localStorage.setItem('animex_music_queue', JSON.stringify([track]));
localStorage.setItem('animex_music_cmd', Date.now());
if (window.parent?.showToast) window.parent.showToast(`Playing: ${title}`, true);
};
window.downloadTheme = (url) => window.open(url, '_blank');
// --- OTHER DATA FETCHERS ---
async function fetchCharacters() {
try {
const res = await fetch(`${serverUrl}/anime/${currentMalId}/characters`);
const data = await res.json();
characterData = data.characters || [];
renderCharacters("JAPANESE");
document.querySelectorAll(".char-lang-btn").forEach(btn => {
btn.onclick = () => {
document.querySelectorAll(".char-lang-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
renderCharacters(btn.dataset.lang);
};
});
} catch (e) {}
}
function renderCharacters(lang) {
const list = document.getElementById("characters-list");
list.innerHTML = "";
characterData.forEach(c => {
const va = c.voice_actors.find(v => v.language === lang) || { name: "N/A", image: "" };
const div = document.createElement("div");
div.className = "character-card";
div.innerHTML = `
<div class="char-card-imgs"><img src="${c.character.image}"><img src="${va.image || 'https://placehold.co/100x100/111/333?text=No+VA'}"></div>
<div class="char-info"><span class="char-name">${c.character.name}</span><span class="va-name">${va.name}</span></div>
`;
list.appendChild(div);
});
document.getElementById("characters-section").style.display = characterData.length ? "block" : "none";
}
async function fetchRecs() {
const res = await fetch(`/proxy?url=https://api.jikan.moe/v4/anime/${currentMalId}/recommendations`);
const data = (await res.json()).data;
const grid = document.getElementById("recommendations-grid");
data?.slice(0, 15).forEach(r => {
const a = document.createElement("a");
a.className = "tile-card";
a.href = `series-info.html?id=${r.entry.mal_id}`;
a.innerHTML = `<div class="tile-img-container"><img src="${r.entry.images.jpg.image_url}" class="tile-img" loading="lazy"></div><div class="tile-title">${r.entry.title}</div>`;
grid.appendChild(a);
});
}
async function fetchRelated() {
const res = await fetch(`/proxy?url=https://api.jikan.moe/v4/anime/${currentMalId}/relations`);
const data = (await res.json()).data;
const container = document.getElementById("related-container");
data?.forEach(rel => {
rel.entry.forEach(entry => {
if (entry.type !== 'anime') return;
const a = document.createElement("a");
a.className = "tile-card";
a.href = `series-info.html?id=${entry.mal_id}`;
a.innerHTML = `<div class="tile-img-container"><img src="https://placehold.co/200x300/111/333?text=..." class="tile-img"><div class="tile-tag">${rel.relation}</div></div><div class="tile-title">${entry.name}</div>`;
container.appendChild(a);
fetch(`/proxy?url=https://api.jikan.moe/v4/anime/${entry.mal_id}`).then(r => r.json()).then(d => {
if (d.data?.images?.jpg?.image_url) a.querySelector("img").src = d.data.images.jpg.image_url;
});
});
});
}
async function fetchSeasons() {
try {
const res = await fetch(`${serverUrl}/anime/${currentMalId}/seasons`);
const data = await res.json();
const entries = data.season_groups || [];
if (entries.length > 1 || (entries[0]?.parts.length > 1)) {
const sel = document.getElementById("season-selector");
document.getElementById("season-selector-wrapper").style.display = "block";
sel.innerHTML = "";
entries.forEach(g => {
g.parts.forEach(p => {
const opt = document.createElement("option");
opt.value = p.mal_id;
opt.textContent = `${g.group_label} - ${p.short_label}`;
if (String(p.mal_id) === currentMalId) opt.selected = true;
sel.appendChild(opt);
});
});
sel.onchange = (e) => window.location.href = `series-info.html?id=${e.target.value}`;
}
} catch (e) {}
}
// --- HELPERS ---
function initTabSystem() {
document.querySelectorAll(".tab-btn").forEach(btn => {
btn.onclick = () => {
if (window.innerWidth >= 1024) return;
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
document.querySelectorAll(".mobile-tab-content").forEach(c => c.classList.remove("active"));
btn.classList.add("active");
document.getElementById(`tab-panel-${btn.dataset.tab}`).classList.add("active");
};
});
}
// Parses Jikan duration strings like "1 hr 54 min", "24 min", "2 hr" into seconds
function parseAnimeDuration(str) {
if (!str) return null;
let seconds = 0;
const hrs = str.match(/(\d+)\s*hr/);
const mins = str.match(/(\d+)\s*min/);
if (hrs) seconds += parseInt(hrs[1]) * 3600;
if (mins) seconds += parseInt(mins[1]) * 60;
return seconds > 0 ? seconds : null;
}
function initYoutubePlayer(id) {
if (window.csPlayer) {
csPlayer.init("video", { defaultId: id, thumbnail: true, theme: "default" }).catch(() => {});
}
}
</script>
</body>
</html>