diff --git a/src/scripts/anime/anime.js b/src/scripts/anime/anime.js
index 9536c5c..f605e3c 100644
--- a/src/scripts/anime/anime.js
+++ b/src/scripts/anime/anime.js
@@ -1,277 +1,301 @@
-const animeId = window.location.pathname.split('/').pop();
-let player;
+let animeData = null;
+let extensionName = null;
+let animeId = null;
-let totalEpisodes = 0;
-let currentPage = 1;
-const itemsPerPage = 12;
+const episodePagination = Object.create(PaginationManager);
+episodePagination.init(12, renderEpisodes);
-var tag = document.createElement('script');
-tag.src = "https://www.youtube.com/iframe_api";
-var firstScriptTag = document.getElementsByTagName('script')[0];
-firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
+YouTubePlayerUtils.init('player');
-let extensionName;
+document.addEventListener('DOMContentLoaded', () => {
+ loadAnime();
+ setupDescriptionModal();
+ setupEpisodeSearch();
+});
async function loadAnime() {
try {
- const path = window.location.pathname;
- const parts = path.split("/").filter(Boolean);
- let animeId;
- if (parts.length === 3) {
- extensionName = parts[1];
- animeId = parts[2];
- } else {
- animeId = parts[1];
+ const urlData = URLUtils.parseEntityPath('anime');
+ if (!urlData) {
+ showError("Invalid URL");
+ return;
}
+ extensionName = urlData.extensionName;
+ animeId = urlData.entityId;
+
const fetchUrl = extensionName
? `/api/anime/${animeId}?source=${extensionName}`
: `/api/anime/${animeId}?source=anilist`;
- const res = await fetch(fetchUrl);
+
+ const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() });
const data = await res.json();
if (data.error) {
- document.getElementById('title').innerText = "Anime Not Found";
+ showError("Anime Not Found");
return;
}
- const title = data.title?.english || data.title?.romaji || data.title || "Unknown Title";
- document.title = `${title} | WaifuBoard`;
- document.getElementById('title').innerText = title;
+ animeData = data;
- let posterUrl = '';
+ const metadata = MediaMetadataUtils.formatAnimeData(data, !!extensionName);
- if (extensionName) {
- posterUrl = data.image || '';
+ updatePageTitle(metadata.title);
+ updateMetadata(metadata);
+ updateDescription(data.description || data.summary);
+ updateCharacters(metadata.characters);
+ updateExtensionPill();
- } else {
- posterUrl = data.coverImage?.extraLarge || '';
- }
+ setupWatchButton();
- if (posterUrl) {
- document.getElementById('poster').src = posterUrl;
- }
+ const hasTrailer = YouTubePlayerUtils.playTrailer(
+ metadata.trailer,
+ 'player',
+ metadata.banner
+ );
- const rawDesc = data.description || data.summary || "No description available.";
- handleDescription(rawDesc);
+ setupEpisodes(metadata.episodes);
- const score = extensionName ? (data.score ? data.score * 10 : '?') : data.averageScore;
- document.getElementById('score').innerText = (score || '?') + '% Score';
-
- document.getElementById('year').innerText =
- extensionName ? (data.year || '????') : (data.seasonYear || data.startDate?.year || '????');
-
- document.getElementById('genres').innerText =
- data.genres?.length > 0 ? data.genres.slice(0, 3).join(' • ') : '';
-
- document.getElementById('format').innerText = data.format || 'TV';
-
- document.getElementById('status').innerText = data.status || 'Unknown';
-
- const extensionPill = document.getElementById('extension-pill');
- if (extensionName && extensionPill) {
- extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`;
- extensionPill.style.display = 'inline-flex';
- } else if (extensionPill) {
- extensionPill.style.display = 'none';
- }
-
- let seasonText = '';
- if (extensionName) {
- seasonText = data.season || 'Unknown';
- } else {
- if (data.season && data.seasonYear) {
- seasonText = `${data.season} ${data.seasonYear}`;
- } else if (data.startDate?.year) {
- const months = ['', 'Winter', 'Winter', 'Spring', 'Spring', 'Spring', 'Summer', 'Summer', 'Summer', 'Fall', 'Fall', 'Fall', 'Winter'];
- const month = data.startDate.month || 1;
- const estimatedSeason = months[month] || '';
- seasonText = `${estimatedSeason} ${data.startDate.year}`.trim();
- }
- }
- document.getElementById('season').innerText = seasonText || 'Unknown';
-
- const studio = extensionName
- ? data.studio || "Unknown"
- : (data.studios?.nodes?.[0]?.name ||
- data.studios?.edges?.[0]?.node?.name ||
- 'Unknown Studio');
-
- document.getElementById('studio').innerText = studio;
-
- const charContainer = document.getElementById('char-list');
- charContainer.innerHTML = '';
-
- let characters = [];
-
- if (extensionName) {
- characters = data.characters || [];
- } else {
- if (data.characters?.nodes?.length > 0) {
- characters = data.characters.nodes.slice(0, 5);
- } else if (data.characters?.edges?.length > 0) {
- characters = data.characters.edges
- .filter(edge => edge?.node?.name?.full)
- .slice(0, 5)
- .map(edge => edge.node);
- }
- }
-
- if (characters.length > 0) {
- characters.slice(0, 5).forEach(char => {
- const name = char?.name?.full || char?.name;
- if (name) {
- charContainer.innerHTML += `
-
-

+ const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0;
+ const score = item.score ? item.score.toFixed(1) : null;
+ const repeatCount = item.repeat_count || 0;
+
+ const entryType = (item.entry_type).toUpperCase();
+ let unitLabel = 'units';
+ if (entryType === 'ANIME') {
+ unitLabel = 'episodes';
+ } else if (entryType === 'MANGA') {
+ unitLabel = 'chapters';
+ } else if (entryType === 'NOVEL') {
+ unitLabel = 'chapters/volumes';
+ }
+
+ const statusLabels = {
+ 'WATCHING': entryType === 'ANIME' ? 'Watching' : 'Reading',
+ 'COMPLETED': 'Completed',
+ 'PLANNING': 'Planning',
+ 'PAUSED': 'Paused',
+ 'DROPPED': 'Dropped'
+ };
+
+ const extraInfo = [];
+ if (repeatCount > 0) {
+ extraInfo.push(`
🔁 ${repeatCount}`);
+ }
+ if (item.is_private) {
+ extraInfo.push('
🔒 Private');
+ }
+
+ const entryDataString = JSON.stringify(item).replace(/'/g, ''');
+
+ div.innerHTML = `
+
+
+
+
+
-
-
${item.title}
-
Ep ${progressText} - ${item.source}
+
+
+
+
+ ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel} ${score ? `⭐ ${score}` : ''}
+
+
+
+
+
`;
- container.appendChild(el);
- });
-}
+ const editBtn = div.querySelector('.edit-icon-btn');
+ editBtn.addEventListener('click', (e) => {
+ try {
+ const entryData = JSON.parse(e.currentTarget.dataset.entry);
-function renderSearchResults(results) {
- searchResults.innerHTML = '';
- if (results.length === 0) {
- searchResults.innerHTML = '
No results found
';
- } else {
- results.forEach(anime => {
- const title = getTitle(anime);
- const img = anime.coverImage.medium || anime.coverImage.large;
- const rating = anime.averageScore ? `${anime.averageScore}%` : 'N/A';
- const year = anime.seasonYear || '';
- const format = anime.format || 'TV';
+ window.ListModalManager.isInList = true;
+ window.ListModalManager.currentEntry = entryData;
+ window.ListModalManager.currentData = entryData;
- let href;
- if (anime.isExtensionResult) {
- href = `/anime/${anime.extensionName}/${anime.id}`;
- } else {
- href = `/anime/${anime.id}`;
+ window.ListModalManager.open(entryData, entryData.source);
+ } catch (error) {
+ console.error('Error parsing entry data for modal:', error);
+
+ if (window.NotificationUtils) {
+ window.NotificationUtils.error('Could not open modal. Check HTML form IDs.');
}
-
- const item = document.createElement('a');
- item.className = 'search-item';
- item.href = href;
- item.innerHTML = `
-

-
-
${title}
-
- ${rating}
- • ${year}
- • ${format}
-
-
- `;
- searchResults.appendChild(item);
- });
- }
- searchResults.classList.add('active');
- searchInput.style.borderRadius = '12px 12px 0 0';
-}
-
-function scrollCarousel(id, direction) {
- const container = document.getElementById(id);
- if(container) {
- const scrollAmount = container.clientWidth * 0.75;
- container.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
- }
-}
-
-let trendingAnimes = [];
-let currentHeroIndex = 0;
-let player;
-let heroInterval;
-
-var tag = document.createElement('script');
-tag.src = "https://www.youtube.com/iframe_api";
-var firstScriptTag = document.getElementsByTagName('script')[0];
-firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
-
-function onYouTubeIframeAPIReady() {
- player = new YT.Player('player', {
- height: '100%', width: '100%',
- playerVars: { 'autoplay': 1, 'controls': 0, 'mute': 1, 'loop': 1, 'showinfo': 0, 'modestbranding': 1 },
- events: { 'onReady': (e) => { e.target.mute(); if(trendingAnimes.length) updateHeroVideo(trendingAnimes[currentHeroIndex]); } }
+ }
});
-}
-async function fetchContent(isUpdate = false) {
- try {
-
- const resExt = await fetch('/api/extensions/anime');
- const dataExt = await resExt.json();
- if (dataExt.extensions) {
- availableExtensions = dataExt.extensions;
- console.log("Extensiones de anime disponibles cargadas:", availableExtensions);
- }
-
- const trendingRes = await fetch('/api/trending');
- const trendingData = await trendingRes.json();
-
- if (trendingData.results && trendingData.results.length > 0) {
- trendingAnimes = trendingData.results;
- if (!isUpdate) {
- updateHeroUI(trendingAnimes[0]);
- startHeroCycle();
- }
- renderList('trending', trendingAnimes);
- } else if (!isUpdate) {
- setTimeout(() => fetchContent(false), 2000);
- }
-
- const topRes = await fetch('/api/top-airing');
- const topData = await topRes.json();
- if (topData.results && topData.results.length > 0) {
- renderList('top-airing', topData.results);
- }
-
- } catch (e) {
- console.error("Fetch Error:", e);
- if(!isUpdate) setTimeout(() => fetchContent(false), 5000);
- }
-}
-
-function startHeroCycle() {
- if(heroInterval) clearInterval(heroInterval);
- heroInterval = setInterval(() => {
- if(trendingAnimes.length > 0) {
- currentHeroIndex = (currentHeroIndex + 1) % trendingAnimes.length;
- updateHeroUI(trendingAnimes[currentHeroIndex]);
- }
- }, 10000);
-}
-
-async function updateHeroUI(anime) {
- if(!anime) return;
-
- currentAnimeData = anime;
-
- const title = getTitle(anime);
- const score = anime.averageScore ? anime.averageScore + '% Match' : 'N/A';
- const year = anime.seasonYear || '';
- const type = anime.format || 'TV';
- const desc = anime.description || 'No description available.';
- const poster = anime.coverImage ? anime.coverImage.extraLarge : '';
- const banner = anime.bannerImage || poster;
-
- document.getElementById('hero-title').innerText = title;
- document.getElementById('hero-desc').innerHTML = desc;
- document.getElementById('hero-score').innerText = score;
- document.getElementById('hero-year').innerText = year;
- document.getElementById('hero-type').innerText = type;
- document.getElementById('hero-poster').src = poster;
-
- const watchBtn = document.getElementById('watch-btn');
- if(watchBtn) watchBtn.onclick = () => window.location.href = `/anime/${anime.id}`;
-
- const addToListBtn = document.querySelector('.hero-buttons .btn-blur');
- if(addToListBtn) {
-
- addToListBtn.onclick = openAddToListModal;
-
- const success = await checkIfInList(anime.id);
- if (success) {
- updateAddToListButton();
- } else {
- updateAddToListButton();
-
- }
- }
-
- const bgImg = document.getElementById('hero-bg-media');
- if(bgImg && bgImg.src !== banner) bgImg.src = banner;
-
- updateHeroVideo(anime);
-
- document.getElementById('hero-loading-ui').style.display = 'none';
- document.getElementById('hero-real-ui').style.display = 'block';
-}
-
-function updateHeroVideo(anime) {
- if (!player || !player.loadVideoById) return;
- const videoContainer = document.getElementById('player');
- if (anime.trailer && anime.trailer.site === 'youtube' && anime.trailer.id) {
- if(player.getVideoData && player.getVideoData().video_id !== anime.trailer.id) {
- player.loadVideoById(anime.trailer.id);
- player.mute();
- }
- videoContainer.style.opacity = "1";
- } else {
- videoContainer.style.opacity = "0";
- player.stopVideo();
- }
-}
-
-function renderList(id, list) {
- const container = document.getElementById(id);
- const firstId = list.length > 0 ? list[0].id : null;
- const currentFirstId = container.firstElementChild?.dataset?.id;
- if (currentFirstId && parseInt(currentFirstId) === firstId && container.children.length === list.length) {
- return;
- }
-
- container.innerHTML = '';
- list.forEach(anime => {
- const title = getTitle(anime);
- const cover = anime.coverImage ? anime.coverImage.large : '';
- const ep = anime.nextAiringEpisode ? 'Ep ' + anime.nextAiringEpisode.episode : (anime.episodes ? anime.episodes + ' Eps' : 'TV');
- const score = anime.averageScore || '--';
-
- const el = document.createElement('div');
- el.className = 'card';
- el.dataset.id = anime.id;
-
- el.onclick = () => window.location.href = `/anime/${anime.id}`;
- el.innerHTML = `
-

-
-
${title}
-
${score}% • ${ep}
-
- `;
- container.appendChild(el);
- });
-}
-
-fetchContent();
-loadContinueWatching();
-setInterval(() => fetchContent(true), 60000);
\ No newline at end of file
+ return div;
+}
\ No newline at end of file
diff --git a/src/scripts/books/book.js b/src/scripts/books/book.js
index d92ad83..42b132b 100644
--- a/src/scripts/books/book.js
+++ b/src/scripts/books/book.js
@@ -1,110 +1,141 @@
-const bookId = window.location.pathname.split('/').pop();
-let allChapters = [];
-let filteredChapters = [];
-let currentPage = 1;
-const itemsPerPage = 12;
+let bookData = null;
let extensionName = null;
+let bookId = null;
let bookSlug = null;
-let currentBookData = null;
-let isInList = false;
-let currentListEntry = null;
+let allChapters = [];
+let filteredChapters = [];
-const API_BASE = '/api';
+const chapterPagination = Object.create(PaginationManager);
+chapterPagination.init(12, () => renderChapterTable());
-function getBookUrl(id, source = 'anilist') {
- return `/api/book/${id}?source=${source}`;
-}
-
-function getChaptersUrl(id, source = 'anilist') {
- return `/api/book/${id}/chapters?source=${source}`;
-}
-
-function getAuthToken() {
- return localStorage.getItem('token');
-}
-
-function getAuthHeaders() {
- const token = getAuthToken();
- return {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`
- };
-}
-
-function getSimpleAuthHeaders() {
- const token = getAuthToken();
- return {
- 'Authorization': `Bearer ${token}`
- };
-}
-
-function applyChapterFromUrlFilter() {
- const params = new URLSearchParams(window.location.search);
- const chapterParam = params.get('chapter');
-
- if (!chapterParam) return;
-
- const chapterNumber = parseFloat(chapterParam);
- if (isNaN(chapterNumber)) return;
-
- filteredChapters = allChapters.filter(
- ch => parseFloat(ch.number) === chapterNumber
- );
-
- currentPage = 1;
-}
-
-function getBookEntryType(bookData) {
- if (!bookData) return 'MANGA';
-
- const format = bookData.format?.toUpperCase() || 'MANGA';
- return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL';
-}
-
-async function checkIfInList() {
- if (!currentBookData) return;
-
- const entryId = extensionName ? bookSlug : bookId;
- const source = extensionName || 'anilist';
-
- const entryType = getBookEntryType(currentBookData);
-
- const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
+document.addEventListener('DOMContentLoaded', () => {
+ init();
+ setupModalClickOutside();
+});
+async function init() {
try {
- const response = await fetch(fetchUrl, {
- headers: getSimpleAuthHeaders()
- });
- if (response.ok) {
- const data = await response.json();
-
- if (data.found && data.entry) {
-
- isInList = true;
- currentListEntry = data.entry;
- } else {
- isInList = false;
- currentListEntry = null;
- }
- updateAddToListButton();
- } else if (response.status === 404) {
-
- isInList = false;
- currentListEntry = null;
- updateAddToListButton();
+ const urlData = URLUtils.parseEntityPath('book');
+ if (!urlData) {
+ showError("Book Not Found");
+ return;
}
- } catch (error) {
- console.error('Error checking single list entry:', error);
+
+ extensionName = urlData.extensionName;
+ bookId = urlData.entityId;
+ bookSlug = urlData.slug;
+
+ await loadBookMetadata();
+
+ await loadChapters();
+
+ await setupAddToListButton();
+
+ } catch (err) {
+ console.error("Metadata Error:", err);
+ showError("Error loading book");
}
}
-function updateAddToListButton() {
+async function loadBookMetadata() {
+ const source = extensionName || 'anilist';
+ const fetchUrl = `/api/book/${bookId}?source=${source}`;
+
+ const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() });
+ const data = await res.json();
+
+ if (data.error || !data) {
+ showError("Book Not Found");
+ return;
+ }
+
+ bookData = data;
+
+ const metadata = MediaMetadataUtils.formatBookData(data, !!extensionName);
+
+ updatePageTitle(metadata.title);
+ updateMetadata(metadata);
+ updateExtensionPill();
+}
+
+function updatePageTitle(title) {
+ document.title = `${title} | WaifuBoard Books`;
+ const titleEl = document.getElementById('title');
+ if (titleEl) titleEl.innerText = title;
+}
+
+function updateMetadata(metadata) {
+
+ const descEl = document.getElementById('description');
+ if (descEl) descEl.innerHTML = metadata.description;
+
+ const scoreEl = document.getElementById('score');
+ if (scoreEl) {
+ scoreEl.innerText = extensionName
+ ? `${metadata.score}`
+ : `${metadata.score}% Score`;
+ }
+
+ const pubEl = document.getElementById('published-date');
+ if (pubEl) pubEl.innerText = metadata.year;
+
+ const statusEl = document.getElementById('status');
+ if (statusEl) statusEl.innerText = metadata.status;
+
+ const formatEl = document.getElementById('format');
+ if (formatEl) formatEl.innerText = metadata.format;
+
+ const chaptersEl = document.getElementById('chapters');
+ if (chaptersEl) chaptersEl.innerText = metadata.chapters;
+
+ const genresEl = document.getElementById('genres');
+ if (genresEl) genresEl.innerText = metadata.genres;
+
+ const posterEl = document.getElementById('poster');
+ if (posterEl) posterEl.src = metadata.poster;
+
+ const heroBgEl = document.getElementById('hero-bg');
+ if (heroBgEl) heroBgEl.src = metadata.banner;
+}
+
+function updateExtensionPill() {
+ const pill = document.getElementById('extension-pill');
+ if (!pill) return;
+
+ if (extensionName) {
+ pill.textContent = extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase();
+ pill.style.display = 'inline-flex';
+ } else {
+ pill.style.display = 'none';
+ }
+}
+
+async function setupAddToListButton() {
+ const btn = document.getElementById('add-to-list-btn');
+ if (!btn || !bookData) return;
+
+ ListModalManager.currentData = bookData;
+ const entryType = ListModalManager.getEntryType(bookData);
+ const idForCheck = extensionName ? bookSlug : bookId;
+
+ await ListModalManager.checkIfInList(
+ idForCheck,
+ extensionName || 'anilist',
+ entryType
+ );
+
+ updateCustomAddButton();
+
+ btn.onclick = () => ListModalManager.open(bookData, extensionName || 'anilist');
+}
+
+function updateCustomAddButton() {
const btn = document.getElementById('add-to-list-btn');
if (!btn) return;
- if (isInList) {
+ if (ListModalManager.isInList) {
btn.innerHTML = `
-