1516 lines
50 KiB
HTML
1516 lines
50 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>Animex Library</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&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
|
/>
|
|
<link rel="stylesheet" href="Resources/manga.css" />
|
|
|
|
<style>
|
|
:root {
|
|
--bg-primary: #121212;
|
|
--bg-semi: #121212BF;
|
|
--bg-secondary: #18181b;
|
|
--surface-color: #27272a;
|
|
--border-color: #3f3f46;
|
|
--text-primary: #f4f4f5;
|
|
--text-secondary: #a1a1aa;
|
|
--accent-color: #ff9500;
|
|
--accent-color-rgb: 255, 149, 0;
|
|
--accent-hover: #ffae45;
|
|
--font-family: "Inter", sans-serif;
|
|
--radius-md: 12px;
|
|
--radius-sm: 8px;
|
|
}
|
|
|
|
/* --- Global & Base Styles --- */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
html {
|
|
scroll-behavior: smooth;
|
|
}
|
|
body {
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
font-family: var(--font-family);
|
|
overflow-x: hidden;
|
|
padding-bottom: 120px; /* Safe area for nav bar */
|
|
}
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* --- Main App Container & Views --- */
|
|
.app-view {
|
|
min-height: 100vh;
|
|
padding: 24px;
|
|
animation: fadeIn 0.4s ease-in-out;
|
|
}
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* --- Header & Navigation --- */
|
|
.view-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.view-title {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
}
|
|
.back-btn,
|
|
.action-btn {
|
|
background: var(--surface-color);
|
|
border: 1px solid var(--border-color);
|
|
color: var(--text-primary);
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
padding: 10px 16px;
|
|
border-radius: var(--radius-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: background-color 0.2s, border-color 0.2s;
|
|
}
|
|
.back-btn:hover,
|
|
.action-btn:hover {
|
|
background-color: #3f3f46;
|
|
border-color: #52525b;
|
|
}
|
|
.view-header .action-btn {
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* --- Section Styles --- */
|
|
.section {
|
|
margin-bottom: 40px;
|
|
}
|
|
.section-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin-bottom: 16px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
/* --- Grid for Series/Lists --- */
|
|
.content-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.grid-item {
|
|
cursor: pointer;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
position: relative;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
.grid-item:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
.grid-item-poster {
|
|
width: 100%;
|
|
aspect-ratio: 2 / 3;
|
|
border-radius: var(--radius-sm);
|
|
object-fit: cover;
|
|
background-color: var(--surface-color);
|
|
display: block;
|
|
border: 1px solid var(--border-color);
|
|
margin-bottom: 8px;
|
|
}
|
|
.grid-item-title {
|
|
font-size: 0.9rem;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.list-grid-item-title {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* --- Details View --- */
|
|
/* Most styles are now inherited from manga.css */
|
|
#details-view {
|
|
padding: 0; /* Remove default padding to allow hero to be full-width */
|
|
}
|
|
.hero-title-container {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
margin-bottom: 20px;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 5;
|
|
padding: 0 5%;
|
|
}
|
|
#details-view .manga-title-en,
|
|
#details-view .manga-title-jp {
|
|
text-align: center;
|
|
max-width: 900px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
display: block;
|
|
line-height: 1.1;
|
|
word-break: break-word;
|
|
overflow-wrap: anywhere;
|
|
transition: font-size 0.2s;
|
|
}
|
|
#details-view .manga-title-jp {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* Responsive title scaling */
|
|
.auto-scale-title {
|
|
font-size: 2.2rem;
|
|
font-weight: 700;
|
|
max-height: 3.6em;
|
|
overflow: hidden;
|
|
display: block;
|
|
transition: font-size 0.2s, max-height 0.2s;
|
|
}
|
|
.auto-scale-title.shrink-1 {
|
|
font-size: 2rem;
|
|
}
|
|
.auto-scale-title.shrink-2 {
|
|
font-size: 1.7rem;
|
|
}
|
|
.auto-scale-title.shrink-3 {
|
|
font-size: 1.4rem;
|
|
}
|
|
.auto-scale-title.shrink-4 {
|
|
font-size: 1.1rem;
|
|
}
|
|
.auto-scale-title.shrink-5 {
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.auto-scale-title {
|
|
font-size: 1.3rem;
|
|
}
|
|
.auto-scale-title.shrink-1 {
|
|
font-size: 1.1rem;
|
|
}
|
|
.auto-scale-title.shrink-2 {
|
|
font-size: 1rem;
|
|
}
|
|
.auto-scale-title.shrink-3 {
|
|
font-size: 0.9rem;
|
|
}
|
|
.auto-scale-title.shrink-4 {
|
|
font-size: 0.8rem;
|
|
}
|
|
.auto-scale-title.shrink-5 {
|
|
font-size: 0.7rem;
|
|
}
|
|
}
|
|
#content-sheet {
|
|
padding-top: 6rem;
|
|
}
|
|
#details-view .item-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-radius: var(--radius-sm);
|
|
overflow: hidden;
|
|
gap: 10px;
|
|
}
|
|
|
|
#details-view .chapter-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
background-color: var(--surface-color);
|
|
border-bottom: 1px solid var(--border-color);
|
|
transition: background-color 0.2s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#details-view .chapter-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
#details-view .chapter-item:hover {
|
|
background-color: #3f3f46;
|
|
transform: none;
|
|
border: none;
|
|
}
|
|
|
|
#details-view .chapter-item .chapter-details {
|
|
flex-grow: 1;
|
|
overflow: hidden;
|
|
padding: 0; /* Override manga.css */
|
|
}
|
|
|
|
#details-view .chapter-item .chapter-title {
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin: 0 0 4px 0;
|
|
padding: 0;
|
|
font-size: 0.95rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
#details-view .chapter-item .chapter-meta {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
#details-view .chapter-item .chapter-actions {
|
|
margin-left: 16px;
|
|
color: var(--text-secondary);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
/* --- Integrated Player View --- */
|
|
#player-view {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: #000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 100;
|
|
}
|
|
#player-view .player-header {
|
|
padding: 12px 16px;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
#player-view .player-title {
|
|
font-size: 1rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
#player-view .player-main {
|
|
flex-grow: 1;
|
|
position: relative;
|
|
}
|
|
#player-view .video-wrapper {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
#player-view video {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
#player-view .episode-shelf {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
max-height: 50%;
|
|
background: rgba(18, 18, 18, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
border-top: 1px solid var(--border-color);
|
|
transform: translateY(100%);
|
|
transition: transform 0.3s ease;
|
|
overflow-y: auto;
|
|
padding: 16px 16px 90px;
|
|
}
|
|
#player-view .episode-shelf.visible {
|
|
transform: translateY(0);
|
|
}
|
|
#player-view .shelf-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
#shelf-item-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
#shelf-item-list .list-episode {
|
|
background: var(--surface-color);
|
|
border: 1px solid var(--border-color);
|
|
padding: 12px 16px;
|
|
border-radius: var(--radius-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
#shelf-item-list .list-episode:hover {
|
|
background-color: #3f3f46;
|
|
}
|
|
#shelf-item-list .list-episode-num {
|
|
font-weight: 700;
|
|
color: var(--text-secondary);
|
|
}
|
|
#shelf-item-list .list-episode-title {
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* --- Integrated PDF Reader View --- */
|
|
#pdf-reader-view {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: var(--bg-primary);
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 100;
|
|
}
|
|
#pdf-reader-main {
|
|
flex-grow: 1;
|
|
overflow: auto;
|
|
text-align: center;
|
|
}
|
|
#pdf-canvas {
|
|
max-width: 100%;
|
|
height: auto;
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
|
}
|
|
.pdf-controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 12px;
|
|
background: var(--bg-secondary);
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.app-view {
|
|
padding: 16px;
|
|
}
|
|
.view-title {
|
|
font-size: 1.5rem;
|
|
}
|
|
.content-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
}
|
|
/* Fix for title overlapping poster on mobile */
|
|
.cover-art-container {
|
|
transform: translateY(15px);
|
|
}
|
|
.cover-art-container.loaded {
|
|
transform: translateY(20px);
|
|
}
|
|
.hero-title-container {
|
|
bottom: 0;
|
|
margin-bottom: 10px;
|
|
}
|
|
#content-sheet {
|
|
margin-top: -40px;
|
|
}
|
|
}
|
|
|
|
.hero-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(
|
|
to top,
|
|
var(--bg-primary) 10%,
|
|
var(--bg-semi) 40%,
|
|
transparent 100%
|
|
);
|
|
z-index: 1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- ======== VIEW 1: MAIN LIBRARY ======== -->
|
|
<div id="main-view" class="app-view">
|
|
<div class="view-header">
|
|
<h1 class="view-title">Library</h1>
|
|
<button id="import-folder-btn" class="action-btn">
|
|
<i class="fas fa-folder-plus"></i>
|
|
</button>
|
|
<input
|
|
type="file"
|
|
id="legacy-folder-input"
|
|
webkitdirectory
|
|
directory
|
|
multiple
|
|
class="hidden"
|
|
/>
|
|
</div>
|
|
|
|
<div id="lists-section" class="section">
|
|
<h2 class="section-title">My Lists</h2>
|
|
<div id="lists-grid" class="content-grid"></div>
|
|
</div>
|
|
|
|
<div id="anime-section" class="section hidden">
|
|
<h2 class="section-title">Anime</h2>
|
|
<div id="anime-grid" class="content-grid"></div>
|
|
</div>
|
|
|
|
<div id="manga-section" class="section hidden">
|
|
<h2 class="section-title">Manga</h2>
|
|
<div id="manga-grid" class="content-grid"></div>
|
|
</div>
|
|
|
|
<p
|
|
id="library-placeholder"
|
|
style="text-align: center; color: var(--text-secondary); padding: 40px"
|
|
>
|
|
Your library is empty. Click "Import Folder" to scan for series.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- ======== VIEW 2: SERIES DETAILS (REDESIGNED) ======== -->
|
|
<div id="details-view" class="hidden">
|
|
<header id="hero-section">
|
|
<div class="hero-overlay"></div>
|
|
<button class="back-btn" data-target-view="main-view">
|
|
<i class="fas fa-arrow-left"></i>
|
|
</button>
|
|
<h1 id="vertical-title-jp" class="vertical-title-jp"></h1>
|
|
<div class="cover-art-container">
|
|
<img id="manga-cover" src="" alt="Cover Art" />
|
|
</div>
|
|
<div class="hero-title-container">
|
|
<h2 id="details-title-en" class="manga-title-en"></h2>
|
|
<p id="details-title-jp" class="manga-title-jp"></p>
|
|
</div>
|
|
</header>
|
|
<main id="content-sheet">
|
|
<div class="synopsis-container">
|
|
<h3>Synopsis</h3>
|
|
<p id="details-synopsis"></p>
|
|
</div>
|
|
|
|
<div class="tabs-section">
|
|
<div class="tabs-container">
|
|
<button id="items-tab-btn" class="tab-btn active">
|
|
Episodes / Chapters
|
|
</button>
|
|
</div>
|
|
<div id="items-panel" class="tab-panel active">
|
|
<div
|
|
class="view-header"
|
|
style="
|
|
margin-bottom: 1rem;
|
|
padding: 0;
|
|
justify-content: space-between;
|
|
"
|
|
>
|
|
<h2
|
|
id="details-item-list-title"
|
|
class="section-title"
|
|
style="margin: 0; border: 0; font-size: 1.5rem"
|
|
></h2>
|
|
<button id="map-files-btn" class="action-btn hidden">
|
|
<i class="fas fa-link"></i>
|
|
</button>
|
|
</div>
|
|
<ul id="details-item-list" class="item-list chapter-list"></ul>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- ======== VIEW 3: INTEGRATED PLAYER (ANIME) ======== -->
|
|
<div id="player-view" class="app-view hidden">
|
|
<header class="player-header">
|
|
<button class="back-btn" data-target-view="details-view">
|
|
<i class="fas fa-arrow-left"></i>
|
|
</button>
|
|
<div class="player-title">
|
|
<span id="player-series-title"></span> -
|
|
<span id="player-episode-title"></span>
|
|
</div>
|
|
<button
|
|
id="toggle-shelf-btn"
|
|
class="action-btn"
|
|
style="margin-left: auto"
|
|
>
|
|
<i class="fas fa-list-ul"></i>
|
|
</button>
|
|
</header>
|
|
<main class="player-main">
|
|
<div class="video-wrapper">
|
|
<video id="video-player" playsinline controls></video>
|
|
</div>
|
|
<div id="episode-shelf" class="episode-shelf">
|
|
<div class="shelf-header">
|
|
<h3 class="section-title" style="margin: 0; border: 0">Episodes</h3>
|
|
<button id="next-episode-btn" class="action-btn">
|
|
<i class="fas fa-step-forward"></i>
|
|
</button>
|
|
</div>
|
|
<div id="shelf-item-list" class="item-list"></div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- ======== VIEW 4: PDF READER (MANGA) ======== -->
|
|
<div
|
|
id="pdf-reader-view"
|
|
class="hidden"
|
|
style="
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 100;
|
|
background-color: var(--bg-primary);
|
|
"
|
|
>
|
|
<iframe
|
|
id="pdf-iframe"
|
|
src="about:blank"
|
|
style="border: none; width: 100%; height: 100%"
|
|
></iframe>
|
|
</div>
|
|
|
|
<!-- ======== VIEW 5: LIST DETAILS ======== -->
|
|
<div id="list-view" class="app-view hidden">
|
|
<div class="view-header">
|
|
<button class="back-btn" data-target-view="main-view">
|
|
<i class="fas fa-arrow-left"></i> Back
|
|
</button>
|
|
<h1 id="list-title" class="view-title">List Name</h1>
|
|
</div>
|
|
<div id="list-content-grid" class="content-grid"></div>
|
|
<p
|
|
id="list-placeholder"
|
|
class="hidden"
|
|
style="text-align: center; color: var(--text-secondary); padding: 40px"
|
|
>
|
|
This list is empty or contains items not in your library.
|
|
</p>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// --- STATE MANAGEMENT ---
|
|
let libraryData = { anime: {}, manga: {} };
|
|
let userLists = {};
|
|
let currentSeriesId = null;
|
|
let activeObjectUrls = new Set();
|
|
const LISTS_STORAGE_KEY = "animex_lists_v1";
|
|
|
|
// --- DOM ELEMENTS ---
|
|
const views = {
|
|
main: document.getElementById("main-view"),
|
|
details: document.getElementById("details-view"),
|
|
player: document.getElementById("player-view"),
|
|
pdfReader: document.getElementById("pdf-reader-view"),
|
|
list: document.getElementById("list-view"),
|
|
};
|
|
const legacyFolderInput = document.getElementById(
|
|
"legacy-folder-input"
|
|
);
|
|
|
|
// --- VIEW CONTROLLER ---
|
|
function cleanupResources() {
|
|
activeObjectUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
activeObjectUrls.clear();
|
|
const iframe = document.getElementById("pdf-iframe");
|
|
if (iframe) {
|
|
iframe.src = "about:blank";
|
|
}
|
|
}
|
|
|
|
async function showView(viewName, ...args) {
|
|
cleanupResources();
|
|
Object.values(views).forEach((v) => v.classList.add("hidden"));
|
|
views[viewName].classList.remove("hidden");
|
|
|
|
switch (viewName) {
|
|
case "details":
|
|
renderDetailsView(...args);
|
|
break;
|
|
case "player":
|
|
renderPlayerView(...args);
|
|
break;
|
|
case "pdfReader":
|
|
renderPdfReaderView(...args);
|
|
break;
|
|
case "list":
|
|
await renderListView(...args);
|
|
break;
|
|
case "main":
|
|
default:
|
|
renderMainView();
|
|
break;
|
|
}
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
// --- FILE IMPORT & PROCESSING ---
|
|
document
|
|
.getElementById("import-folder-btn")
|
|
.addEventListener("click", async () => {
|
|
if ("showDirectoryPicker" in window) {
|
|
try {
|
|
const dirHandle = await window.showDirectoryPicker();
|
|
await processDirectoryHandle(dirHandle);
|
|
} catch (err) {
|
|
if (err.name !== "AbortError") {
|
|
window.parent.showToast(
|
|
"Error importing directory: " + err.message,
|
|
"error"
|
|
);
|
|
console.error("Error importing directory:", err);
|
|
}
|
|
}
|
|
} else {
|
|
legacyFolderInput.value = "";
|
|
legacyFolderInput.click();
|
|
}
|
|
});
|
|
|
|
legacyFolderInput.addEventListener("change", (event) => {
|
|
const files = Array.from(event.target.files);
|
|
if (files.length > 0) {
|
|
processFiles(files);
|
|
} else {
|
|
window.parent.showToast(
|
|
"No files selected. Please select a folder to import.",
|
|
"info"
|
|
);
|
|
}
|
|
});
|
|
|
|
async function processDirectoryHandle(dirHandle) {
|
|
const allFiles = [];
|
|
async function getFilesRecursively(directoryHandle, path) {
|
|
for await (const entry of directoryHandle.values()) {
|
|
const newPath = path ? `${path}/${entry.name}` : entry.name;
|
|
if (entry.kind === "file") {
|
|
const file = await entry.getFile();
|
|
Object.defineProperty(file, "webkitRelativePath", {
|
|
value: newPath,
|
|
configurable: true,
|
|
});
|
|
allFiles.push(file);
|
|
} else if (entry.kind === "directory") {
|
|
await getFilesRecursively(entry, newPath);
|
|
}
|
|
}
|
|
}
|
|
await getFilesRecursively(dirHandle, "");
|
|
if (allFiles.length === 0) {
|
|
window.parent.showToast(
|
|
"No files found in the selected directory.",
|
|
"info"
|
|
);
|
|
return;
|
|
}
|
|
processFiles(allFiles);
|
|
}
|
|
|
|
async function processFiles(files) {
|
|
if (!files || files.length === 0) {
|
|
window.parent.showToast("No files found to import.", "info");
|
|
return;
|
|
}
|
|
const newLibraryData = { anime: {}, manga: {} };
|
|
const directoryContents = new Map();
|
|
|
|
for (const file of files) {
|
|
const path = file.webkitRelativePath || file.name;
|
|
const i = path.lastIndexOf("/");
|
|
const dirPath = i === -1 ? "" : path.substring(0, i);
|
|
if (!directoryContents.has(dirPath)) {
|
|
directoryContents.set(dirPath, []);
|
|
}
|
|
directoryContents.get(dirPath).push(file);
|
|
}
|
|
|
|
let seriesImported = 0;
|
|
for (const [dirPath, seriesFiles] of directoryContents.entries()) {
|
|
const metaFile = seriesFiles.find(
|
|
(f) => f.name.toLowerCase() === "meta.json"
|
|
);
|
|
if (!metaFile) continue;
|
|
|
|
try {
|
|
const meta = JSON.parse(await metaFile.text());
|
|
const seriesId =
|
|
dirPath.split("/").pop() ||
|
|
meta.title
|
|
.replace(/[^\w\s-]/g, "")
|
|
.trim()
|
|
.replace(/\s+/g, "-")
|
|
.toLowerCase();
|
|
if (!seriesId) continue;
|
|
|
|
const mediaExtensions = /\.(mp4|mkv|webm|avi|pdf)$/i;
|
|
const posterMatch = /^poster\./;
|
|
|
|
meta.posterFile =
|
|
seriesFiles.find((f) =>
|
|
posterMatch.test(f.name.toLowerCase())
|
|
) || null;
|
|
meta.rawFiles = seriesFiles.filter((f) =>
|
|
mediaExtensions.test(f.name)
|
|
);
|
|
meta.isMapped = false;
|
|
|
|
if (meta.type && newLibraryData[meta.type]) {
|
|
newLibraryData[meta.type][seriesId] = meta;
|
|
seriesImported++;
|
|
}
|
|
} catch (e) {
|
|
window.parent.showToast(
|
|
`Could not process series at path: ${dirPath}
|
|
${e.message}`,
|
|
"error"
|
|
);
|
|
console.warn(`Could not process series at path: ${dirPath}`, e);
|
|
}
|
|
}
|
|
|
|
if (seriesImported === 0) {
|
|
window.parent.showToast(
|
|
"No valid series found. Make sure each series folder contains a meta.json file.",
|
|
"info"
|
|
);
|
|
} else {
|
|
window.parent.showToast(
|
|
`${seriesImported} series imported successfully.`,
|
|
"success"
|
|
);
|
|
}
|
|
libraryData = newLibraryData;
|
|
showView("main");
|
|
}
|
|
|
|
async function mapSeriesFiles(seriesId, type) {
|
|
const series = libraryData[type]?.[seriesId];
|
|
if (!series || series.isMapped) return;
|
|
|
|
console.log(`Mapping files for ${series.title}...`);
|
|
|
|
const itemsKey = type === "anime" ? "episodes" : "chapters";
|
|
const seriesItems = series[itemsKey] || [];
|
|
const rawFiles = series.rawFiles || [];
|
|
if (seriesItems.length === 0 || rawFiles.length === 0) {
|
|
series.isMapped = true;
|
|
return;
|
|
}
|
|
|
|
function extractItemNumber(filename) {
|
|
const cleanedName = filename
|
|
.toLowerCase()
|
|
.replace(/\[.*?\]/g, " ")
|
|
.replace(/\(.*?\)/g, " ");
|
|
let match = cleanedName.match(/s\d+[e](\d+)/);
|
|
if (match) return parseInt(match[1]);
|
|
match = cleanedName.match(/(?:e|ep|episode)[\s._-]*(\d+)/);
|
|
if (match) return parseInt(match[1]);
|
|
if (type === "manga") {
|
|
match = cleanedName.match(/(?:c|ch|chapter)[\s._-]*(\d+)/);
|
|
if (match) return parseInt(match[1]);
|
|
}
|
|
match = cleanedName.match(/[\s._-](\d{1,4})[\s._-]/);
|
|
if (match) {
|
|
const num = parseInt(match[1]);
|
|
if (
|
|
![480, 720, 1080, 2160].includes(num) &&
|
|
(num < 1900 || num > 2050)
|
|
) {
|
|
return num;
|
|
}
|
|
}
|
|
const allNumbers = cleanedName.match(/\d+/g) || [];
|
|
for (const numStr of allNumbers) {
|
|
const num = parseInt(numStr);
|
|
if (
|
|
![480, 720, 1080, 2160].includes(num) &&
|
|
(num < 1900 || num > 2050)
|
|
) {
|
|
return num;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const fileMap = new Map();
|
|
rawFiles.forEach((file) => {
|
|
const itemNum = extractItemNumber(file.name);
|
|
if (itemNum !== null && !fileMap.has(itemNum)) {
|
|
fileMap.set(itemNum, file);
|
|
}
|
|
});
|
|
|
|
let filesMappedCount = 0;
|
|
seriesItems.forEach((item) => {
|
|
const itemNum = item.episode || item.chapter;
|
|
if (fileMap.has(itemNum)) {
|
|
item.file = fileMap.get(itemNum);
|
|
filesMappedCount++;
|
|
}
|
|
});
|
|
|
|
series.isMapped = true;
|
|
console.log(
|
|
`Mapping complete for ${series.title}. Mapped ${filesMappedCount} of ${seriesItems.length} items.`
|
|
);
|
|
|
|
if (currentSeriesId && currentSeriesId.seriesId === seriesId) {
|
|
renderDetailsView(seriesId, type);
|
|
}
|
|
}
|
|
|
|
// --- RENDERING LOGIC ---
|
|
function renderMainView() {
|
|
loadLists();
|
|
const animeGrid = document.getElementById("anime-grid");
|
|
const mangaGrid = document.getElementById("manga-grid");
|
|
const listsGrid = document.getElementById("lists-grid");
|
|
const animeSection = document.getElementById("anime-section");
|
|
const mangaSection = document.getElementById("manga-section");
|
|
const listsSection = document.getElementById("lists-section");
|
|
const placeholder = document.getElementById("library-placeholder");
|
|
|
|
animeGrid.innerHTML = "";
|
|
mangaGrid.innerHTML = "";
|
|
listsGrid.innerHTML = "";
|
|
|
|
const hasAnime = Object.keys(libraryData.anime).length > 0;
|
|
const hasManga = Object.keys(libraryData.manga).length > 0;
|
|
const hasLists = Object.keys(userLists).length > 0;
|
|
|
|
placeholder.classList.toggle("hidden", hasAnime || hasManga);
|
|
animeSection.classList.toggle("hidden", !hasAnime);
|
|
mangaSection.classList.toggle("hidden", !hasManga);
|
|
listsSection.classList.toggle("hidden", !hasLists);
|
|
|
|
for (const [id, series] of Object.entries(libraryData.anime)) {
|
|
animeGrid.innerHTML += createGridItem(id, series);
|
|
}
|
|
for (const [id, series] of Object.entries(libraryData.manga)) {
|
|
mangaGrid.innerHTML += createGridItem(id, series);
|
|
}
|
|
for (const listName of Object.keys(userLists)) {
|
|
listsGrid.innerHTML += createGridItem(listName, null, true);
|
|
}
|
|
}
|
|
|
|
function createGridItem(id, series, isList = false) {
|
|
let title, posterUrl;
|
|
const malIdAttr =
|
|
!isList && series && series.mal_id
|
|
? `data-mal-id="${series.mal_id}"`
|
|
: "";
|
|
|
|
if (isList) {
|
|
title = id;
|
|
posterUrl = "https://placehold.co/300x450/18181B/A1A1AA?text=LIST";
|
|
} else {
|
|
title = series.title;
|
|
if (series.posterFile) {
|
|
if (typeof series.posterFile === "string") {
|
|
posterUrl = series.posterFile;
|
|
} else {
|
|
posterUrl = URL.createObjectURL(series.posterFile);
|
|
activeObjectUrls.add(posterUrl);
|
|
}
|
|
} else {
|
|
posterUrl =
|
|
"https://placehold.co/300x450/27272A/A1A1AA?text=No+Poster";
|
|
}
|
|
}
|
|
const dataType = isList ? "list" : series.type;
|
|
const mangaTag =
|
|
!isList && series && series.type === "manga"
|
|
? `<div style="position:absolute;top:8px;left:8px;z-index:2;"><span style="background:#ff9500;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>`
|
|
: "";
|
|
|
|
return `
|
|
<div class="grid-item" data-id="${id}" data-type="${dataType}" ${malIdAttr} style="position:relative;">
|
|
<img src="${posterUrl}" alt="${title}" class="grid-item-poster">
|
|
<p class="grid-item-title ${
|
|
isList ? "list-grid-item-title" : ""
|
|
}">${title}</p>
|
|
</div>`;
|
|
}
|
|
|
|
function renderDetailsView(seriesId, type) {
|
|
currentSeriesId = { seriesId, type };
|
|
const series = libraryData[type]?.[seriesId];
|
|
if (!series) return showView("main");
|
|
|
|
const posterUrl = series.posterFile
|
|
? typeof series.posterFile === "string"
|
|
? series.posterFile
|
|
: URL.createObjectURL(series.posterFile)
|
|
: "https://placehold.co/300x450/27272A/A1A1AA?text=No+Poster";
|
|
|
|
if (series.posterFile && typeof series.posterFile !== "string") {
|
|
activeObjectUrls.add(posterUrl);
|
|
}
|
|
|
|
const heroSection = document.getElementById("hero-section");
|
|
const verticalTitleJp = document.getElementById("vertical-title-jp");
|
|
const coverArtContainer = heroSection.querySelector(
|
|
".cover-art-container"
|
|
);
|
|
|
|
heroSection.style.backgroundImage = `url(${posterUrl})`;
|
|
document.getElementById("manga-cover").src = posterUrl;
|
|
const titleEl = document.getElementById("details-title-en");
|
|
titleEl.textContent = series.title;
|
|
titleEl.classList.add("auto-scale-title");
|
|
// Responsive font scaling and layout adjustment
|
|
setTimeout(() => {
|
|
// 1. Adjust font size to fit
|
|
titleEl.classList.remove(
|
|
"shrink-1",
|
|
"shrink-2",
|
|
"shrink-3",
|
|
"shrink-4",
|
|
"shrink-5"
|
|
);
|
|
let shrink = 0;
|
|
while (titleEl.scrollHeight > titleEl.offsetHeight && shrink < 5) {
|
|
shrink++;
|
|
titleEl.classList.add("shrink-" + shrink);
|
|
}
|
|
|
|
// 2. Adjust layout to prevent title overlapping poster
|
|
const heroSection = document.getElementById("hero-section");
|
|
const titleContainer = document.querySelector(
|
|
".hero-title-container"
|
|
);
|
|
const contentSheet = document.getElementById("content-sheet");
|
|
|
|
if (heroSection && titleContainer && contentSheet) {
|
|
// Use padding on the hero section to create space for the title container.
|
|
// This prevents it from overlapping the poster art and also pushes the
|
|
// main content sheet down appropriately.
|
|
const titleHeight = titleContainer.offsetHeight;
|
|
heroSection.style.paddingBottom = `${titleHeight + 20}px`; // title height + 20px gap
|
|
|
|
// Clear any previous dynamic padding on the content sheet as it's no longer needed.
|
|
contentSheet.style.paddingTop = "";
|
|
}
|
|
}, 50);
|
|
const originalTitle = series.original_title || "";
|
|
document.getElementById("details-title-jp").textContent =
|
|
originalTitle;
|
|
verticalTitleJp.textContent = originalTitle.slice(0, 10);
|
|
document.getElementById("details-synopsis").textContent =
|
|
series.synopsis || "No synopsis available.";
|
|
|
|
setTimeout(() => {
|
|
verticalTitleJp.classList.add("loaded");
|
|
coverArtContainer.classList.add("loaded");
|
|
}, 100);
|
|
|
|
const itemsKey = type === "anime" ? "episodes" : "chapters";
|
|
const itemListEl = document.getElementById("details-item-list");
|
|
const itemListTitle = document.getElementById(
|
|
"details-item-list-title"
|
|
);
|
|
const mapBtn = document.getElementById("map-files-btn");
|
|
|
|
itemListEl.innerHTML = "";
|
|
mapBtn.classList.add("hidden");
|
|
|
|
if (series.isMapped) {
|
|
itemListTitle.textContent =
|
|
itemsKey.charAt(0).toUpperCase() + itemsKey.slice(1);
|
|
const items = series[itemsKey] || [];
|
|
if (items.length === 0) {
|
|
itemListEl.innerHTML = `<p style="color: var(--text-secondary); padding: 20px;">No ${itemsKey} found in metadata.</p>`;
|
|
return;
|
|
}
|
|
items
|
|
.sort(
|
|
(a, b) => (a.episode || a.chapter) - (b.episode || b.chapter)
|
|
)
|
|
.forEach((item) => {
|
|
if (!item.file) return;
|
|
const itemNum = item.episode || item.chapter;
|
|
const li = document.createElement("li");
|
|
li.className = "chapter-item";
|
|
li.dataset.itemNum = itemNum;
|
|
const iconClass = type === "anime" ? "fa-play" : "fa-book-open";
|
|
li.innerHTML = `
|
|
<div class="chapter-details">
|
|
<p class="chapter-title">${
|
|
item.title || "No Title"
|
|
}</p>
|
|
<p class="chapter-meta">${
|
|
type === "anime" ? "Episode" : "Chapter"
|
|
} ${itemNum}</p>
|
|
</div>
|
|
<div class="chapter-actions">
|
|
<i class="fas ${iconClass}"></i>
|
|
</div>
|
|
`;
|
|
itemListEl.appendChild(li);
|
|
});
|
|
} else {
|
|
itemListTitle.textContent = "Unmapped Local Files";
|
|
mapBtn.classList.remove("hidden");
|
|
mapBtn.disabled = false;
|
|
mapBtn.innerHTML = '<i class="fas fa-link"></i> Map Files';
|
|
mapBtn.onclick = () => {
|
|
mapBtn.innerHTML =
|
|
'<i class="fas fa-spinner fa-spin"></i> Mapping...';
|
|
mapBtn.disabled = true;
|
|
setTimeout(() => mapSeriesFiles(seriesId, type), 100);
|
|
};
|
|
|
|
if (series.rawFiles && series.rawFiles.length > 0) {
|
|
series.rawFiles.forEach((file, index) => {
|
|
const li = document.createElement("li");
|
|
li.className = "chapter-item";
|
|
li.dataset.rawFileIndex = index;
|
|
const iconClass = type === "anime" ? "fa-play" : "fa-book-open";
|
|
li.innerHTML = `
|
|
<div class="chapter-details">
|
|
<p class="chapter-title">${file.name}</p>
|
|
</div>
|
|
<div class="chapter-actions"><i class="fas ${iconClass}"></i></div>
|
|
`;
|
|
itemListEl.appendChild(li);
|
|
});
|
|
} else {
|
|
itemListEl.innerHTML = `<p style="color: var(--text-secondary); padding: 20px;">No local files found for this series.</p>`;
|
|
}
|
|
setTimeout(() => mapSeriesFiles(seriesId, type), 500);
|
|
}
|
|
}
|
|
|
|
async function renderListView(listName) {
|
|
document.getElementById("list-title").textContent = listName;
|
|
const grid = document.getElementById("list-content-grid");
|
|
const placeholder = document.getElementById("list-placeholder");
|
|
grid.innerHTML = "";
|
|
|
|
const listContent = userLists[listName] || [];
|
|
let itemsFound = 0;
|
|
|
|
const renderPromises = listContent.map(
|
|
async ([seriesMalId, items]) => {
|
|
const stringMalId = String(seriesMalId);
|
|
|
|
const foundAnime = Object.entries(libraryData.anime).find(
|
|
([id, data]) => String(data.mal_id) === stringMalId
|
|
);
|
|
const foundManga = Object.entries(libraryData.manga).find(
|
|
([id, data]) => String(data.mal_id) === stringMalId
|
|
);
|
|
|
|
const [seriesId, seriesData] = foundAnime ||
|
|
foundManga || [null, null];
|
|
|
|
if (seriesData) {
|
|
itemsFound++;
|
|
return createGridItem(seriesId, seriesData);
|
|
}
|
|
|
|
if (navigator.onLine) {
|
|
try {
|
|
let resp = await fetch(
|
|
`https://api.jikan.moe/v4/anime/${stringMalId}`
|
|
);
|
|
let type = "anime";
|
|
if (!resp.ok) {
|
|
resp = await fetch(
|
|
`https://api.jikan.moe/v4/manga/${stringMalId}`
|
|
);
|
|
type = "manga";
|
|
if (!resp.ok)
|
|
throw new Error(`Series ${stringMalId} not found.`);
|
|
}
|
|
|
|
const data = (await resp.json()).data;
|
|
if (data) {
|
|
const jikanSeries = {
|
|
title: data.title_english || data.title || "No Title",
|
|
synopsis: data.synopsis || "No synopsis available.",
|
|
posterFile: data.images.jpg.image_url,
|
|
type: type,
|
|
mal_id: data.mal_id,
|
|
};
|
|
itemsFound++;
|
|
return createGridItem(stringMalId, jikanSeries);
|
|
}
|
|
} catch (err) {
|
|
console.warn(
|
|
`[renderListView] Could not fetch series from Jikan for mal_id: ${stringMalId}`,
|
|
err
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
);
|
|
|
|
const gridItemsHtml = (await Promise.all(renderPromises))
|
|
.filter(Boolean)
|
|
.join("");
|
|
grid.innerHTML = gridItemsHtml;
|
|
|
|
placeholder.classList.toggle("hidden", itemsFound > 0);
|
|
}
|
|
|
|
// --- PLAYER & READER LOGIC ---
|
|
const videoPlayer = document.getElementById("video-player");
|
|
const shelfList = document.getElementById("shelf-item-list");
|
|
|
|
function renderPlayerView(
|
|
seriesId,
|
|
type,
|
|
itemNum,
|
|
rawFileIndex = null
|
|
) {
|
|
const series = libraryData[type][seriesId];
|
|
const itemsKey = "episodes";
|
|
let currentItem, currentFile;
|
|
|
|
if (rawFileIndex !== null && series.rawFiles) {
|
|
currentFile = series.rawFiles[parseInt(rawFileIndex)];
|
|
currentItem = {
|
|
episode: "Raw File",
|
|
title: currentFile.name,
|
|
file: currentFile,
|
|
};
|
|
} else {
|
|
currentItem = series[itemsKey]?.find((ep) => ep.episode == itemNum);
|
|
currentFile = currentItem?.file;
|
|
}
|
|
|
|
if (!series || !currentFile)
|
|
return showView("details", seriesId, type);
|
|
|
|
document.getElementById("player-series-title").textContent =
|
|
series.title;
|
|
document.getElementById("player-episode-title").textContent = `E${
|
|
currentItem.episode
|
|
} - ${currentItem.title || ""}`;
|
|
|
|
const url = URL.createObjectURL(currentFile);
|
|
activeObjectUrls.add(url);
|
|
videoPlayer.src = url;
|
|
videoPlayer.play();
|
|
|
|
shelfList.innerHTML = "";
|
|
if (series.isMapped && series[itemsKey]) {
|
|
series[itemsKey]
|
|
.sort((a, b) => a.episode - b.episode)
|
|
.forEach((item) => {
|
|
if (!item.file) return;
|
|
shelfList.innerHTML += `
|
|
<div class="list-episode" data-item-num="${
|
|
item.episode
|
|
}" style="${
|
|
item.episode == itemNum
|
|
? "background:var(--accent-color); color:black;"
|
|
: ""
|
|
}">
|
|
<span class="list-episode-num">${
|
|
item.episode
|
|
}</span>
|
|
<span class="list-episode-title">${
|
|
item.title || "No Title"
|
|
}</span>
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
const nextEpBtn = document.getElementById("next-episode-btn");
|
|
nextEpBtn.classList.add("hidden");
|
|
if (series.isMapped && itemNum) {
|
|
const sortedEpisodes = series[itemsKey]
|
|
.filter((ep) => ep.file)
|
|
.sort((a, b) => a.episode - b.episode);
|
|
const currentIndex = sortedEpisodes.findIndex(
|
|
(ep) => ep.episode == itemNum
|
|
);
|
|
const nextItem = sortedEpisodes[currentIndex + 1];
|
|
if (nextItem) {
|
|
nextEpBtn.classList.remove("hidden");
|
|
nextEpBtn.dataset.itemNum = nextItem.episode;
|
|
}
|
|
}
|
|
}
|
|
|
|
function openPdfReader(seriesId, type, itemNum, rawFileIndex = null) {
|
|
const series = libraryData[type]?.[seriesId];
|
|
if (!series) {
|
|
window.parent.showToast("Series not found.", "error");
|
|
return;
|
|
}
|
|
|
|
let targetFile = null;
|
|
let currentItem = null;
|
|
let nextItemNum = null;
|
|
|
|
if (rawFileIndex != null && series.rawFiles) {
|
|
targetFile = series.rawFiles[parseInt(rawFileIndex)];
|
|
currentItem = {
|
|
chapter: "Raw File",
|
|
title: targetFile.name,
|
|
file: targetFile,
|
|
};
|
|
} else if (itemNum != null) {
|
|
const itemsKey = "chapters";
|
|
const sortedItems = (series[itemsKey] || [])
|
|
.filter(it => it.file)
|
|
.sort((a, b) => (a.chapter) - (b.chapter));
|
|
|
|
const currentIndex = sortedItems.findIndex(
|
|
(it) => String(it.chapter) === String(itemNum)
|
|
);
|
|
|
|
if (currentIndex === -1) {
|
|
window.parent.showToast("Chapter not found.", "error");
|
|
return;
|
|
}
|
|
|
|
currentItem = sortedItems[currentIndex];
|
|
targetFile = currentItem?.file || null;
|
|
|
|
const nextItem = sortedItems[currentIndex + 1];
|
|
if (nextItem) {
|
|
nextItemNum = nextItem.chapter;
|
|
}
|
|
}
|
|
|
|
if (!targetFile) {
|
|
window.parent.showToast("File not found for this item.", "error");
|
|
return;
|
|
}
|
|
|
|
const canUsePopup = window.parent && typeof window.parent.openPopup === "function";
|
|
if (canUsePopup) {
|
|
const chapterTitleText =
|
|
currentItem.chapter === "Raw File"
|
|
? currentItem.title
|
|
: `Chapter ${currentItem.chapter} - ${currentItem.title || "No Title"}`;
|
|
|
|
let popupUrl = `pdf.html?embedded=true&seriesTitle=${encodeURIComponent(
|
|
series.title
|
|
)}&chapterTitle=${encodeURIComponent(chapterTitleText)}&seriesId=${encodeURIComponent(seriesId)}&type=${type}&itemNum=${itemNum}`;
|
|
|
|
if (nextItemNum !== null) {
|
|
popupUrl += `&nextItemNum=${nextItemNum}`;
|
|
}
|
|
|
|
const popup = window.parent.openPopup(popupUrl);
|
|
|
|
const messageHandler = (event) => {
|
|
const iframe = window.parent.document.querySelector('#popup-iframe');
|
|
if (iframe && event.source === iframe.contentWindow && event.data && event.data.type === 'pdf-reader-ready') {
|
|
event.source.postMessage({
|
|
type: 'load-pdf',
|
|
fileData: targetFile,
|
|
seriesTitle: series.title,
|
|
chapterTitle: chapterTitleText
|
|
}, '*');
|
|
window.parent.removeEventListener('message', messageHandler);
|
|
}
|
|
};
|
|
window.parent.addEventListener('message', messageHandler);
|
|
|
|
const closeChecker = setInterval(() => {
|
|
if (popup && popup.closed) {
|
|
clearInterval(closeChecker);
|
|
window.parent.removeEventListener('message', messageHandler);
|
|
}
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
function loadLists() {
|
|
try {
|
|
const stored = localStorage.getItem(LISTS_STORAGE_KEY);
|
|
userLists = stored ? JSON.parse(stored) : {};
|
|
} catch (e) {
|
|
userLists = {};
|
|
}
|
|
}
|
|
|
|
// --- EVENT LISTENERS ---
|
|
window.addEventListener("message", (event) => {
|
|
if (event.data && event.data.type === "pdf-reader-back") {
|
|
if (currentSeriesId) {
|
|
showView(
|
|
"details",
|
|
currentSeriesId.seriesId,
|
|
currentSeriesId.type
|
|
);
|
|
} else {
|
|
showView("main");
|
|
}
|
|
} else if (event.data && event.data.type === "pdf-reader-next") {
|
|
const { seriesId, type, itemNum } = event.data;
|
|
if (seriesId && type && itemNum) {
|
|
openPdfReader(seriesId, type, itemNum);
|
|
}
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener("click", (e) => {
|
|
const backBtn = e.target.closest(".back-btn");
|
|
if (backBtn) {
|
|
if (videoPlayer && !videoPlayer.paused) videoPlayer.pause();
|
|
const targetView = backBtn.dataset.targetView;
|
|
if (targetView === "details" && currentSeriesId) {
|
|
showView(
|
|
targetView,
|
|
currentSeriesId.seriesId,
|
|
currentSeriesId.type
|
|
);
|
|
} else {
|
|
showView("main");
|
|
}
|
|
return;
|
|
}
|
|
|
|
const gridItem = e.target.closest(".grid-item");
|
|
if (gridItem) {
|
|
const listView = e.target.closest("#list-view");
|
|
if (listView) {
|
|
const malId = gridItem.dataset.malId || gridItem.dataset.id;
|
|
if (
|
|
malId &&
|
|
window.parent &&
|
|
typeof window.parent.openPopup === "function"
|
|
) {
|
|
const url = `series-info.html?id=${malId}`;
|
|
window.parent.openPopup(url);
|
|
}
|
|
return;
|
|
}
|
|
const id = gridItem.dataset.id;
|
|
const type = gridItem.dataset.type;
|
|
if (type === "anime" || type === "manga")
|
|
showView("details", id, type);
|
|
else if (type === "list") showView("list", id);
|
|
return;
|
|
}
|
|
|
|
const detailsItem = e.target.closest(
|
|
"#details-item-list .chapter-item"
|
|
);
|
|
if (detailsItem && currentSeriesId) {
|
|
const { seriesId, type } = currentSeriesId;
|
|
const itemNum = detailsItem.dataset.itemNum;
|
|
const rawFileIndex = detailsItem.dataset.rawFileIndex;
|
|
|
|
if (type === 'manga') {
|
|
openPdfReader(seriesId, type, itemNum, rawFileIndex);
|
|
return;
|
|
}
|
|
|
|
// --- Logic for Anime / Video Player ---
|
|
const series = libraryData[type]?.[seriesId];
|
|
let targetFile = null;
|
|
|
|
if (rawFileIndex != null && series && series.rawFiles) {
|
|
targetFile = series.rawFiles[parseInt(rawFileIndex)];
|
|
} else if (itemNum != null && series) {
|
|
const itemsKey = "episodes";
|
|
const currentItem = (series[itemsKey] || []).find(
|
|
(it) => String(it.episode) === String(itemNum)
|
|
);
|
|
targetFile = currentItem?.file || null;
|
|
}
|
|
|
|
if (!targetFile) {
|
|
window.parent.showToast("File not found for this item.", "error");
|
|
return;
|
|
}
|
|
|
|
const canUsePopup = window.parent && typeof window.parent.openPopup === "function";
|
|
if (canUsePopup) {
|
|
const blobUrl = URL.createObjectURL(targetFile);
|
|
activeObjectUrls.add(blobUrl);
|
|
const popupUrl = `video_player.html?full=true&video=${encodeURIComponent(blobUrl)}`;
|
|
window.parent.openPopup(popupUrl);
|
|
} else {
|
|
if (itemNum) {
|
|
showView("player", seriesId, type, itemNum);
|
|
} else if (rawFileIndex) {
|
|
showView("player", seriesId, type, null, rawFileIndex);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
const shelfItem = e.target.closest("#shelf-item-list .list-episode");
|
|
if (shelfItem && currentSeriesId) {
|
|
const itemNum = shelfItem.dataset.itemNum;
|
|
const { seriesId, type } = currentSeriesId;
|
|
renderPlayerView(seriesId, type, itemNum);
|
|
}
|
|
});
|
|
|
|
document
|
|
.getElementById("toggle-shelf-btn")
|
|
.addEventListener("click", () => {
|
|
document
|
|
.getElementById("episode-shelf")
|
|
.classList.toggle("visible");
|
|
});
|
|
|
|
document
|
|
.getElementById("next-episode-btn")
|
|
.addEventListener("click", (e) => {
|
|
if (!currentSeriesId) return;
|
|
const itemNum = e.currentTarget.dataset.itemNum;
|
|
renderPlayerView(
|
|
currentSeriesId.seriesId,
|
|
currentSeriesId.type,
|
|
itemNum
|
|
);
|
|
});
|
|
|
|
// --- INITIALIZATION ---
|
|
showView("main");
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|