diff --git a/src/scripts/anime/anime.js b/src/scripts/anime/anime.js
index 50967b0..0bb111f 100644
--- a/src/scripts/anime/anime.js
+++ b/src/scripts/anime/anime.js
@@ -1,552 +1,335 @@
-const animeId = window.location.pathname.split('/').pop();
-let player;
-let totalEpisodes = 0;
-let currentPage = 1;
-const itemsPerPage = 12;
-let extensionName;
-let currentAnimeData = null;
-let isInList = false;
-let currentListEntry = null;
+const searchInput = document.getElementById('search-input');
+const searchResults = document.getElementById('search-results');
+let searchTimeout;
+let availableExtensions = [];
-const API_BASE = '/api';
+searchInput.addEventListener('input', (e) => {
+ const query = e.target.value;
+ clearTimeout(searchTimeout);
+ if (query.length < 2) {
+ searchResults.classList.remove('active');
+ searchResults.innerHTML = '';
+ searchInput.style.borderRadius = '99px';
+ return;
+ }
+ searchTimeout = setTimeout(() => {
+
+ fetchSearh(query);
+ }, 300);
+});
+
+document.addEventListener('click', (e) => {
+ if (!e.target.closest('.search-wrapper')) {
+ searchResults.classList.remove('active');
+ searchInput.style.borderRadius = '99px';
+ }
+});
+
+async function fetchSearh(query) {
+ try {
+ let apiUrl = `/api/search?q=${encodeURIComponent(query)}`;
+ let extensionName = null;
+ let finalQuery = query;
+
+ const parts = query.split(':');
+ if (parts.length >= 2) {
+ const potentialExtension = parts[0].trim().toLowerCase();
+
+ const foundExtension = availableExtensions.find(ext => ext.toLowerCase() === potentialExtension);
+
+ if (foundExtension) {
+ extensionName = foundExtension;
+
+ finalQuery = parts.slice(1).join(':').trim();
+
+ if (finalQuery.length === 0) {
+ renderSearchResults([]);
+ return;
+ }
+
+ apiUrl = `/api/search/${extensionName}?q=${encodeURIComponent(finalQuery)}`;
+ }
+ }
+
+ const res = await fetch(apiUrl);
+ const data = await res.json();
+
+ const resultsWithExtension = (data.results || []).map(anime => {
+ if (extensionName) {
+ return {
+ ...anime,
+ isExtensionResult: true,
+ extensionName: extensionName
+ };
+ }
+ return anime;
+ });
+
+ renderSearchResults(resultsWithExtension);
+ } catch (err) {
+ console.error("Search Error:", err);
+ }
+}
+
+async function loadContinueWatching() {
+ const token = localStorage.getItem('token');
+ if (!token) return;
+
+ try {
+ const res = await fetch('/api/list/filter?status=watching&entry_type=ANIME', {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!res.ok) return;
+
+ const data = await res.json();
+ const list = data.results || [];
+
+ renderContinueWatching('my-status', list);
+
+ } catch (err) {
+ console.error('Continue Watching Error:', err);
+ }
+}
+
+function renderContinueWatching(id, list) {
+ const container = document.getElementById(id);
+ if (!container) return;
+
+ container.innerHTML = '';
+
+ if (list.length === 0) {
+ container.innerHTML = `
No watching anime
`;
+ return;
+ }
+
+ list.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
+
+ list.forEach(item => {
+ const el = document.createElement('div');
+ el.className = 'card';
+
+ el.onclick = () => {
+ const ep = item.progress || 1;
+
+ if (item.source === 'anilist') {
+ window.location.href = `http://localhost:54322/watch/${item.entry_id}/${ep + 1}`;
+ } else {
+ window.location.href = `http://localhost:54322/watch/${item.entry_id}/${ep + 1}?${item.source}`;
+ }
+ };
+
+ const progressText = item.total_episodes
+ ? `${item.progress || 0}/${item.total_episodes}`
+ : `${item.progress || 0}`;
+
+ el.innerHTML = `
+
+

+
+
+
${item.title}
+
Ep ${progressText} - ${item.source}
+ `;
+
+ container.appendChild(el);
+ });
+}
+
+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';
+
+ let href;
+ if (anime.isExtensionResult) {
+ href = `/anime/${anime.extensionName}/${anime.id}`;
+ } else {
+ href = `/anime/${anime.id}`;
+ }
+
+ 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 getAuthToken() {
- return localStorage.getItem('token');
+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]); } }
+ });
}
-function getAuthHeaders() {
- const token = getAuthToken();
- return {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`
- };
-}
-
-function getSimpleAuthHeaders() {
- const token = getAuthToken();
- return {
- 'Authorization': `Bearer ${token}`
- };
-}
-
-// Lógica de chequeo de lista corregida para usar /list/entry/{id} y esperar 'found'
-async function checkIfInList() {
- const entryId = window.location.pathname.split('/').pop();
- const source = extensionName || 'anilist';
- const entryType = 'ANIME';
-
- const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
-
+async function fetchContent(isUpdate = false) {
try {
- const response = await fetch(fetchUrl, {
- headers: getSimpleAuthHeaders()
- });
-
- if (response.ok) {
- const data = await response.json();
-
- if (data.found && data.entry) { // Esperamos 'found'
- isInList = true;
- currentListEntry = data.entry;
- } else {
- isInList = false;
- currentListEntry = null;
- }
- updateAddToListButton();
+ 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);
}
- } catch (error) {
- console.error('Error checking single list entry:', error);
+ 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 updateAddToListButton() {
- const btn = document.getElementById('add-to-list-btn');
- if (!btn) return;
+function startHeroCycle() {
+ if(heroInterval) clearInterval(heroInterval);
+ heroInterval = setInterval(() => {
+ if(trendingAnimes.length > 0) {
+ currentHeroIndex = (currentHeroIndex + 1) % trendingAnimes.length;
+ updateHeroUI(trendingAnimes[currentHeroIndex]);
+ }
+ }, 10000);
+}
- if (isInList) {
- btn.innerHTML = `
-
- In Your List
- `;
- btn.style.background = 'rgba(34, 197, 94, 0.2)';
- btn.style.color = '#22c55e';
- btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
+function getTitle(anime) {
+ return anime.title.english || anime.title.romaji || "Unknown Title";
+}
+
+function updateHeroUI(anime) {
+ if(!anime) return;
+ const title = getTitle(anime);
+ const score = anime.averageScore ? anime.averageScore + '% Match' : 'N/A';
+ const year = anime.seasonYear || '';
+ const type = anime.format || 'TV';
+ const desc = anime.description || 'No description available.';
+ const poster = anime.coverImage ? anime.coverImage.extraLarge : '';
+ const banner = anime.bannerImage || poster;
+
+ document.getElementById('hero-title').innerText = title;
+ document.getElementById('hero-desc').innerHTML = desc;
+ document.getElementById('hero-score').innerText = score;
+ document.getElementById('hero-year').innerText = year;
+ document.getElementById('hero-type').innerText = type;
+ document.getElementById('hero-poster').src = poster;
+
+ const watchBtn = document.getElementById('watch-btn');
+ if(watchBtn) watchBtn.onclick = () => window.location.href = `/anime/${anime.id}`;
+
+ const bgImg = document.getElementById('hero-bg-media');
+ if(bgImg && bgImg.src !== banner) bgImg.src = banner;
+
+ updateHeroVideo(anime);
+
+ document.getElementById('hero-loading-ui').style.display = 'none';
+ document.getElementById('hero-real-ui').style.display = 'block';
+}
+
+function updateHeroVideo(anime) {
+ if (!player || !player.loadVideoById) return;
+ const videoContainer = document.getElementById('player');
+ if (anime.trailer && anime.trailer.site === 'youtube' && anime.trailer.id) {
+ if(player.getVideoData && player.getVideoData().video_id !== anime.trailer.id) {
+ player.loadVideoById(anime.trailer.id);
+ player.mute();
+ }
+ videoContainer.style.opacity = "1";
} else {
- btn.innerHTML = '+ Add to List';
- btn.style.background = null;
- btn.style.color = null;
- btn.style.borderColor = null;
+ videoContainer.style.opacity = "0";
+ player.stopVideo();
}
}
-// Función openAddToListModal actualizada con todos los campos extendidos
-function openAddToListModal() {
- // Referencias
- const modalTitle = document.getElementById('modal-title');
- const deleteBtn = document.getElementById('modal-delete-btn');
-
- // Mapeo de prefijos (usamos 'entry-' para el HTML del modal extendido)
- const statusEl = document.getElementById('entry-status');
- const progressEl = document.getElementById('entry-progress');
- const scoreEl = document.getElementById('entry-score');
- const startDateEl = document.getElementById('entry-start-date');
- const endDateEl = document.getElementById('entry-end-date');
- const repeatCountEl = document.getElementById('entry-repeat-count');
- const notesEl = document.getElementById('entry-notes');
- const privateEl = document.getElementById('entry-is-private');
-
-
- if (isInList && currentListEntry) {
- const statusReverseMap = {
- CURRENT: 'WATCHING'
- };
-
- statusEl.value = statusReverseMap[currentListEntry.status] || currentListEntry.status || 'PLANNING';
- progressEl.value = currentListEntry.progress || 0;
- scoreEl.value = currentListEntry.score || '';
-
- // Campos extendidos
- startDateEl.value = currentListEntry.start_date ? currentListEntry.start_date.split('T')[0] : '';
- endDateEl.value = currentListEntry.end_date ? currentListEntry.end_date.split('T')[0] : '';
- repeatCountEl.value = currentListEntry.repeat_count || 0;
- notesEl.value = currentListEntry.notes || '';
- privateEl.checked = currentListEntry.is_private === true || currentListEntry.is_private === 1;
-
- modalTitle.textContent = 'Edit List Entry';
- deleteBtn.style.display = 'block';
- } else {
- // Valores por defecto
- statusEl.value = 'PLANNING';
- progressEl.value = 0;
- scoreEl.value = '';
- startDateEl.value = '';
- endDateEl.value = '';
- repeatCountEl.value = 0;
- notesEl.value = '';
- privateEl.checked = false;
-
- modalTitle.textContent = 'Add to List';
- deleteBtn.style.display = 'none';
+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;
}
- progressEl.max = totalEpisodes || 999;
- document.getElementById('add-list-modal').classList.add('active');
+ 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);
+ });
}
-function closeAddToListModal() {
- document.getElementById('add-list-modal').classList.remove('active');
-}
-
-// Función saveToList actualizada con todos los campos extendidos
-async function saveToList() {
- const uiStatus = document.getElementById('entry-status').value;
-
- const anilistStatusMap = {
- WATCHING: 'CURRENT',
- COMPLETED: 'COMPLETED',
- PLANNING: 'PLANNING',
- PAUSED: 'PAUSED',
- DROPPED: 'DROPPED',
- REPEATING: 'REPEATING'
- };
-
- const status = anilistStatusMap[uiStatus];
- const progress = parseInt(document.getElementById('entry-progress').value) || 0;
- const scoreValue = document.getElementById('entry-score').value;
- const score = scoreValue ? parseFloat(scoreValue) : null;
-
- // Nuevos campos
- const start_date = document.getElementById('entry-start-date').value || null;
- const end_date = document.getElementById('entry-end-date').value || null;
- const repeat_count = parseInt(document.getElementById('entry-repeat-count').value) || 0;
- const notes = document.getElementById('entry-notes').value || null;
- const is_private = document.getElementById('entry-is-private').checked;
-
- try {
- const response = await fetch(`${API_BASE}/list/entry`, {
- method: 'POST',
- headers: getAuthHeaders(),
- body: JSON.stringify({
- entry_id: animeId,
- source: extensionName || 'anilist',
- entry_type: 'ANIME',
- status: status,
- progress: progress,
- score: score,
- // Campos extendidos
- start_date: start_date,
- end_date: end_date,
- repeat_count: repeat_count,
- notes: notes,
- is_private: is_private
- })
- });
-
- if (!response.ok) {
- throw new Error('Failed to save entry');
- }
-
- const data = await response.json();
-
- isInList = true;
- currentListEntry = data.entry;
- updateAddToListButton();
- closeAddToListModal();
- showNotification(isInList ? 'Updated successfully!' : 'Added to your list!', 'success');
- } catch (error) {
- console.error('Error saving to list:', error);
- showNotification('Failed to save. Please try again.', 'error');
- }
-}
-
-async function deleteFromList() {
- if (!confirm('Remove this anime from your list?')) return;
-
- const source = extensionName || 'anilist';
- const entryType = 'ANIME';
-
- try {
- const response = await fetch(`${API_BASE}/list/entry/${animeId}?source=${source}&entry_type=${entryType}`, {
- method: 'DELETE',
- headers: getSimpleAuthHeaders()
-
- });
-
- if (!response.ok) {
- throw new Error('Failed to delete entry');
- }
-
- isInList = false;
- currentListEntry = null;
- updateAddToListButton();
- closeAddToListModal();
- showNotification('Removed from your list', 'success');
- } catch (error) {
- console.error('Error deleting from list:', error);
- showNotification('Failed to remove. Please try again.', 'error');
- }
-}
-
-function showNotification(message, type = 'info') {
- const notification = document.createElement('div');
- notification.style.cssText = `
- position: fixed;
- top: 100px;
- right: 20px;
- background: ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : '#8b5cf6'};
- color: white;
- padding: 1rem 1.5rem;
- border-radius: 12px;
- box-shadow: 0 10px 30px rgba(0,0,0,0.3);
- z-index: 9999;
- font-weight: 600;
- animation: slideInRight 0.3s ease;
- `;
- notification.textContent = message;
- document.body.appendChild(notification);
-
- setTimeout(() => {
- notification.style.animation = 'slideOutRight 0.3s ease';
- setTimeout(() => notification.remove(), 300);
- }, 3000);
-}
-
-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 fetchUrl = extensionName
- ? `/api/anime/${animeId}?source=${extensionName}`
- : `/api/anime/${animeId}?source=anilist`;
- const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() });
- const data = await res.json();
-
- if (data.error) {
- document.getElementById('title').innerText = "Anime Not Found";
- return;
- }
-
- currentAnimeData = data;
-
- const title = data.title?.english || data.title?.romaji || data.title || "Unknown Title";
- document.title = `${title} | WaifuBoard`;
- document.getElementById('title').innerText = title;
-
- let posterUrl = '';
-
- if (extensionName) {
- posterUrl = data.image || '';
- } else {
- posterUrl = data.coverImage?.extraLarge || '';
- }
-
- if (posterUrl) {
- document.getElementById('poster').src = posterUrl;
- }
-
- const rawDesc = data.description || data.summary || "No description available.";
- handleDescription(rawDesc);
-
- 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 += `
- `;
- }
- });
- } else {
- charContainer.innerHTML = `
-
- No character data available
-
`;
- }
-
- document.getElementById('watch-btn').onclick = () => {
- window.location.href = `/watch/${animeId}/1`;
- };
-
- if (data.trailer && data.trailer.site === 'youtube') {
- window.onYouTubeIframeAPIReady = function() {
- player = new YT.Player('player', {
- height: '100%',
- width: '100%',
- videoId: data.trailer.id,
- playerVars: {
- autoplay: 1, controls: 0, mute: 1,
- loop: 1, playlist: data.trailer.id,
- showinfo: 0, modestbranding: 1, disablekb: 1
- },
- events: { onReady: (e) => e.target.playVideo() }
- });
- };
- } else {
- const banner = extensionName
- ? (data.image || '')
- : (data.bannerImage || data.coverImage?.extraLarge || '');
-
- if (banner) {
- document.querySelector('.video-background').innerHTML =
- `
`;
- }
- }
-
- if (extensionName) {
- totalEpisodes = data.episodes || 1;
- } else {
- if (data.nextAiringEpisode?.episode) {
- totalEpisodes = data.nextAiringEpisode.episode - 1;
- } else if (data.episodes) {
- totalEpisodes = data.episodes;
- } else {
- totalEpisodes = 12;
- }
- }
-
- totalEpisodes = Math.min(Math.max(totalEpisodes, 1), 5000);
- document.getElementById('episodes').innerText = totalEpisodes;
-
- renderEpisodes();
-
- await checkIfInList();
-
- } catch (err) {
- console.error('Error loading anime:', err);
- document.getElementById('title').innerText = "Error loading anime";
- }
-}
-
-function handleDescription(text) {
- const tmp = document.createElement("DIV");
- tmp.innerHTML = text;
- const cleanText = tmp.textContent || tmp.innerText || "";
-
- const sentences = cleanText.match(/[^\.!\?]+[\.!\?]+/g) || [cleanText];
-
- document.getElementById('full-description').innerHTML = text;
-
- if (sentences.length > 4) {
- const shortText = sentences.slice(0, 4).join(' ');
- document.getElementById('description-preview').innerText = shortText + '...';
- document.getElementById('read-more-btn').style.display = 'inline-flex';
- } else {
- document.getElementById('description-preview').innerHTML = text;
- document.getElementById('read-more-btn').style.display = 'none';
- }
-}
-
-function openModal() {
- document.getElementById('desc-modal').classList.add('active');
- document.body.style.overflow = 'hidden';
-}
-
-function closeModal() {
- document.getElementById('desc-modal').classList.remove('active');
- document.body.style.overflow = '';
-}
-
-document.getElementById('desc-modal').addEventListener('click', (e) => {
- if (e.target.id === 'desc-modal') closeModal();
-});
-
-function renderEpisodes() {
- const grid = document.getElementById('episodes-grid');
- grid.innerHTML = '';
-
- const start = (currentPage - 1) * itemsPerPage + 1;
- const end = Math.min(start + itemsPerPage - 1, totalEpisodes);
-
- for(let i = start; i <= end; i++) {
- createEpisodeButton(i, grid);
- }
- updatePaginationControls();
-}
-
-function createEpisodeButton(num, container) {
- const btn = document.createElement('div');
- btn.className = 'episode-btn';
- btn.innerText = `Ep ${num}`;
- btn.onclick = () =>
- window.location.href = `/watch/${animeId}/${num}` + (extensionName ? `?${extensionName}` : "");
-
- container.appendChild(btn);
-}
-
-function updatePaginationControls() {
- const totalPages = Math.ceil(totalEpisodes / itemsPerPage);
- document.getElementById('page-info').innerText = `Page ${currentPage} of ${totalPages}`;
- document.getElementById('prev-page').disabled = currentPage === 1;
- document.getElementById('next-page').disabled = currentPage === totalPages;
-
- document.getElementById('pagination-controls').style.display = 'flex';
-}
-
-function changePage(delta) {
- currentPage += delta;
- renderEpisodes();
-}
-
-const searchInput = document.getElementById('ep-search');
-searchInput.addEventListener('input', (e) => {
- const val = parseInt(e.target.value);
- const grid = document.getElementById('episodes-grid');
-
- if (val > 0 && val <= totalEpisodes) {
- grid.innerHTML = '';
- createEpisodeButton(val, grid);
- document.getElementById('pagination-controls').style.display = 'none';
- } else if (!e.target.value) {
- renderEpisodes();
- } else {
- grid.innerHTML = 'Episode not found
';
- document.getElementById('pagination-controls').style.display = 'none';
- }
-});
-
-document.addEventListener('DOMContentLoaded', () => {
- const modal = document.getElementById('add-list-modal');
- if (modal) {
- modal.addEventListener('click', (e) => {
- if (e.target.id === 'add-list-modal') {
- closeAddToListModal();
- }
- });
- }
-});
-
-const style = document.createElement('style');
-style.textContent = `
- @keyframes slideInRight {
- from { transform: translateX(400px); opacity: 0; }
- to { transform: translateX(0); opacity: 1; }
- }
- @keyframes slideOutRight {
- from { transform: translateX(0); opacity: 1; }
- to { transform: translateX(400px); opacity: 0; }
- }
-`;
-document.head.appendChild(style);
-
-loadAnime();
\ No newline at end of file
+fetchContent();
+loadContinueWatching();
+setInterval(() => fetchContent(true), 60000);
\ No newline at end of file
diff --git a/src/scripts/anime/animes.js b/src/scripts/anime/animes.js
index 6c7e3b0..4e164ab 100644
--- a/src/scripts/anime/animes.js
+++ b/src/scripts/anime/animes.js
@@ -3,6 +3,291 @@ const searchResults = document.getElementById('search-results');
let searchTimeout;
let availableExtensions = [];
+let currentAnimeData = null;
+
+let isInList = false;
+let currentListEntry = null;
+let totalEpisodes = 0;
+
+const API_BASE = '/api';
+
+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 getTitle(anime) {
+ return anime.title.english || anime.title.romaji || "Unknown Title";
+}
+
+function showNotification(message, type = 'info') {
+ const notification = document.createElement('div');
+ notification.style.cssText = `
+ position: fixed;
+ top: 100px;
+ right: 20px;
+ background: ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : '#8b5cf6'};
+ color: white;
+ padding: 1rem 1.5rem;
+ border-radius: 12px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
+ z-index: 9999;
+ font-weight: 600;
+ animation: slideInRight 0.3s ease;
+ `;
+ notification.textContent = message;
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.style.animation = 'slideOutRight 0.3s ease';
+ setTimeout(() => notification.remove(), 300);
+ }, 3000);
+}
+
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes slideInRight {
+ from { transform: translateX(400px); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+ }
+ @keyframes slideOutRight {
+ from { transform: translateX(0); opacity: 1; }
+ to { transform: translateX(400px); opacity: 0; }
+ }
+`;
+document.head.appendChild(style);
+
+async function checkIfInList(animeId) {
+ const source = 'anilist';
+ const entryType = 'ANIME';
+
+ const fetchUrl = `${API_BASE}/list/entry/${animeId}?source=${source}&entry_type=${entryType}`;
+
+ 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;
+ }
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('Error checking single list entry:', error);
+ return false;
+ }
+}
+
+function updateAddToListButton() {
+ const btn = document.querySelector('.hero-buttons .btn-blur');
+ if (!btn) return;
+
+ if (isInList) {
+ btn.innerHTML = `
+
+ In Your List
+ `;
+ btn.style.background = 'rgba(34, 197, 94, 0.2)';
+ btn.style.color = '#22c55e';
+ btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
+ } else {
+ btn.innerHTML = '+ Add to List';
+ btn.style.background = null;
+ btn.style.color = null;
+ btn.style.borderColor = null;
+ }
+}
+
+async function openAddToListModal() {
+ if (!getAuthToken()) {
+ showNotification('Please log in to manage your list.', 'error');
+ return;
+ }
+ if (!currentAnimeData) return;
+
+ await checkIfInList(currentAnimeData.id);
+
+ const modalTitle = document.getElementById('modal-title');
+ const deleteBtn = document.getElementById('modal-delete-btn');
+
+ const statusEl = document.getElementById('entry-status');
+ const progressEl = document.getElementById('entry-progress');
+ const scoreEl = document.getElementById('entry-score');
+ const startDateEl = document.getElementById('entry-start-date');
+ const endDateEl = document.getElementById('entry-end-date');
+ const repeatCountEl = document.getElementById('entry-repeat-count');
+ const notesEl = document.getElementById('entry-notes');
+ const privateEl = document.getElementById('entry-is-private');
+
+ if (isInList && currentListEntry) {
+ const statusReverseMap = { CURRENT: 'WATCHING' };
+
+ statusEl.value = statusReverseMap[currentListEntry.status] || currentListEntry.status || 'PLANNING';
+ progressEl.value = currentListEntry.progress || 0;
+ scoreEl.value = currentListEntry.score || '';
+
+ startDateEl.value = currentListEntry.start_date ? currentListEntry.start_date.split('T')[0] : '';
+ endDateEl.value = currentListEntry.end_date ? currentListEntry.end_date.split('T')[0] : '';
+ repeatCountEl.value = currentListEntry.repeat_count || 0;
+ notesEl.value = currentListEntry.notes || '';
+ privateEl.checked = currentListEntry.is_private === true || currentListEntry.is_private === 1;
+
+ modalTitle.textContent = 'Edit List Entry';
+ deleteBtn.style.display = 'block';
+ } else {
+ statusEl.value = 'PLANNING';
+ progressEl.value = 0;
+ scoreEl.value = '';
+ startDateEl.value = '';
+ endDateEl.value = '';
+ repeatCountEl.value = 0;
+ notesEl.value = '';
+ privateEl.checked = false;
+
+ modalTitle.textContent = 'Add to List';
+ deleteBtn.style.display = 'none';
+ }
+
+ totalEpisodes = currentAnimeData.episodes || 999;
+
+ progressEl.max = totalEpisodes;
+ document.getElementById('add-list-modal').classList.add('active');
+}
+
+function closeAddToListModal() {
+ document.getElementById('add-list-modal').classList.remove('active');
+}
+
+async function saveToList() {
+ const uiStatus = document.getElementById('entry-status').value;
+
+ const anilistStatusMap = {
+ WATCHING: 'CURRENT',
+ COMPLETED: 'COMPLETED',
+ PLANNING: 'PLANNING',
+ PAUSED: 'PAUSED',
+ DROPPED: 'DROPPED',
+ REPEATING: 'REPEATING'
+ };
+
+ const status = anilistStatusMap[uiStatus];
+ const progress = parseInt(document.getElementById('entry-progress').value) || 0;
+ const scoreValue = document.getElementById('entry-score').value;
+ const score = scoreValue ? parseFloat(scoreValue) : null;
+
+ const start_date = document.getElementById('entry-start-date').value || null;
+ const end_date = document.getElementById('entry-end-date').value || null;
+ const repeat_count = parseInt(document.getElementById('entry-repeat-count').value) || 0;
+ const notes = document.getElementById('entry-notes').value || null;
+ const is_private = document.getElementById('entry-is-private').checked;
+
+ const animeId = currentAnimeData ? currentAnimeData.id : null;
+ if (!animeId) return;
+
+ try {
+ const response = await fetch(`${API_BASE}/list/entry`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({
+ entry_id: animeId,
+ source: 'anilist',
+
+ entry_type: 'ANIME',
+ status: status,
+ progress: progress,
+ score: score,
+ start_date: start_date,
+ end_date: end_date,
+ repeat_count: repeat_count,
+ notes: notes,
+ is_private: is_private
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to save entry');
+ }
+
+ const data = await response.json();
+
+ isInList = true;
+ currentListEntry = data.entry;
+ updateAddToListButton();
+ closeAddToListModal();
+ showNotification(isInList ? 'Updated successfully!' : 'Added to your list!', 'success');
+ } catch (error) {
+ console.error('Error saving to list:', error);
+ showNotification('Failed to save. Please try again.', 'error');
+ }
+}
+
+async function deleteFromList() {
+ if (!confirm('Remove this anime from your list?')) return;
+
+ const animeId = currentAnimeData ? currentAnimeData.id : null;
+ if (!animeId) return;
+
+ const source = 'anilist';
+ const entryType = 'ANIME';
+
+ try {
+ const response = await fetch(`${API_BASE}/list/entry/${animeId}?source=${source}&entry_type=${entryType}`, {
+ method: 'DELETE',
+ headers: getSimpleAuthHeaders()
+
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete entry');
+ }
+
+ isInList = false;
+ currentListEntry = null;
+ updateAddToListButton();
+ closeAddToListModal();
+ showNotification('Removed from your list', 'success');
+ } catch (error) {
+ console.error('Error deleting from list:', error);
+ showNotification('Failed to remove. Please try again.', 'error');
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const modal = document.getElementById('add-list-modal');
+ if (modal) {
+ modal.addEventListener('click', (e) => {
+ if (e.target.id === 'add-list-modal') {
+ closeAddToListModal();
+ }
+ });
+ }
+});
+
searchInput.addEventListener('input', (e) => {
const query = e.target.value;
clearTimeout(searchTimeout);
@@ -106,7 +391,6 @@ function renderContinueWatching(id, list) {
return;
}
- // ✅ ordenar por fecha
list.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
list.forEach(item => {
@@ -123,7 +407,6 @@ function renderContinueWatching(id, list) {
}
};
-
const progressText = item.total_episodes
? `${item.progress || 0}/${item.total_episodes}`
: `${item.progress || 0}`;
@@ -253,12 +536,11 @@ function startHeroCycle() {
}, 10000);
}
-function getTitle(anime) {
- return anime.title.english || anime.title.romaji || "Unknown Title";
-}
-
-function updateHeroUI(anime) {
+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 || '';
@@ -277,6 +559,20 @@ function updateHeroUI(anime) {
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;
diff --git a/src/scripts/books/book.js b/src/scripts/books/book.js
index fe27285..d92ad83 100644
--- a/src/scripts/books/book.js
+++ b/src/scripts/books/book.js
@@ -55,7 +55,6 @@ function applyChapterFromUrlFilter() {
currentPage = 1;
}
-
function getBookEntryType(bookData) {
if (!bookData) return 'MANGA';
@@ -63,7 +62,6 @@ function getBookEntryType(bookData) {
return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL';
}
-// CORRECCIÓN: Usar el endpoint /list/entry/{id} y esperar 'found'
async function checkIfInList() {
if (!currentBookData) return;
@@ -72,7 +70,6 @@ async function checkIfInList() {
const entryType = getBookEntryType(currentBookData);
- // URL CORRECTA: /list/entry/{id}?source={source}&entry_type={entryType}
const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
try {
@@ -83,7 +80,6 @@ async function checkIfInList() {
if (response.ok) {
const data = await response.json();
- // LÓGICA CORRECTA: Comprobar data.found
if (data.found && data.entry) {
isInList = true;
@@ -94,7 +90,7 @@ async function checkIfInList() {
}
updateAddToListButton();
} else if (response.status === 404) {
- // Manejar 404 como 'no encontrado' si la API lo devuelve así
+
isInList = false;
currentListEntry = null;
updateAddToListButton();
@@ -128,37 +124,27 @@ function updateAddToListButton() {
}
}
-/**
- * REFACTORIZADO para usar la estructura del modal completo.
- * Asume que el HTML usa IDs como 'entry-status', 'entry-progress', 'entry-score', etc.
- */
function openAddToListModal() {
if (!currentBookData) return;
const totalUnits = currentBookData.chapters || currentBookData.volumes || 999;
const entryType = getBookEntryType(currentBookData);
- // Referencias a los elementos del nuevo modal (usando 'entry-' prefix)
const modalTitle = document.getElementById('modal-title');
const deleteBtn = document.getElementById('modal-delete-btn');
const progressLabel = document.getElementById('progress-label');
- // **VERIFICACIÓN CRÍTICA**
if (!modalTitle || !deleteBtn || !progressLabel) {
console.error("Error: Uno o más elementos críticos del modal (título, botón eliminar, o etiqueta de progreso) no se encontraron. Verifique los IDs en el HTML.");
return;
}
- // --- Población de Datos ---
-
if (isInList && currentListEntry) {
- // Datos comunes
+
document.getElementById('entry-status').value = currentListEntry.status || 'PLANNING';
document.getElementById('entry-progress').value = currentListEntry.progress || 0;
document.getElementById('entry-score').value = currentListEntry.score || '';
- // Nuevos datos
- // Usar formato ISO si viene como ISO, o limpiar si es necesario. Tu ejemplo JSON no tenía fechas.
document.getElementById('entry-start-date').value = currentListEntry.start_date ? currentListEntry.start_date.split('T')[0] : '';
document.getElementById('entry-end-date').value = currentListEntry.end_date ? currentListEntry.end_date.split('T')[0] : '';
document.getElementById('entry-repeat-count').value = currentListEntry.repeat_count || 0;
@@ -168,7 +154,7 @@ function openAddToListModal() {
modalTitle.textContent = 'Edit Library Entry';
deleteBtn.style.display = 'block';
} else {
- // Valores por defecto
+
document.getElementById('entry-status').value = 'PLANNING';
document.getElementById('entry-progress').value = 0;
document.getElementById('entry-score').value = '';
@@ -182,8 +168,6 @@ function openAddToListModal() {
deleteBtn.style.display = 'none';
}
- // --- Configuración de Etiquetas y Máximo ---
-
if (progressLabel) {
if (entryType === 'MANGA') {
progressLabel.textContent = 'Chapters Read';
@@ -200,11 +184,8 @@ function closeAddToListModal() {
document.getElementById('add-list-modal').classList.remove('active');
}
-/**
- * REFACTORIZADO para guardar TODOS los campos del modal.
- */
async function saveToList() {
- // Datos comunes
+
const uiStatus = document.getElementById('entry-status').value;
const anilistStatusMap = {
@@ -221,14 +202,12 @@ async function saveToList() {
const scoreValue = document.getElementById('entry-score').value;
const score = scoreValue ? parseFloat(scoreValue) : null;
- // Nuevos datos
const start_date = document.getElementById('entry-start-date').value || null;
const end_date = document.getElementById('entry-end-date').value || null;
const repeat_count = parseInt(document.getElementById('entry-repeat-count').value) || 0;
const notes = document.getElementById('entry-notes').value || null;
const is_private = document.getElementById('entry-is-private').checked;
-
if (!currentBookData) {
showNotification('Cannot save: Book data not loaded.', 'error');
return;
@@ -248,7 +227,7 @@ async function saveToList() {
status: status,
progress: progress,
score: score,
- // Nuevos campos
+
start_date: start_date,
end_date: end_date,
repeat_count: repeat_count,
@@ -264,7 +243,8 @@ async function saveToList() {
const data = await response.json();
isInList = true;
- currentListEntry = data.entry; // Usar la respuesta del servidor si está disponible
+ currentListEntry = data.entry;
+
updateAddToListButton();
closeAddToListModal();
showNotification(isInList ? 'Updated successfully!' : 'Added to your library!', 'success');
@@ -274,16 +254,15 @@ async function saveToList() {
}
}
-// CORRECCIÓN: Usar el endpoint /list/entry/{id} con los parámetros correctos.
async function deleteFromList() {
if (!confirm('Remove this book from your library?')) return;
const idToDelete = extensionName ? bookSlug : bookId;
const source = extensionName || 'anilist';
- const entryType = getBookEntryType(currentBookData); // Obtener el tipo de entrada
+ const entryType = getBookEntryType(currentBookData);
try {
- // URL CORRECTA para DELETE: /list/entry/{id}?source={source}&entry_type={entryType}
+
const response = await fetch(`${API_BASE}/list/entry/${idToDelete}?source=${source}&entry_type=${entryType}`, {
method: 'DELETE',
headers: getSimpleAuthHeaders()
@@ -611,7 +590,7 @@ function openReader(bookId, chapterId, provider) {
}
document.addEventListener('DOMContentLoaded', () => {
- // El ID del modal sigue siendo 'add-list-modal' para mantener la compatibilidad con el código original.
+
const modal = document.getElementById('add-list-modal');
if (modal) {
modal.addEventListener('click', (e) => {
diff --git a/src/scripts/books/books.js b/src/scripts/books/books.js
index c93fb3b..cfd2521 100644
--- a/src/scripts/books/books.js
+++ b/src/scripts/books/books.js
@@ -3,6 +3,14 @@ let currentHeroIndex = 0;
let heroInterval;
let availableExtensions = [];
+let currentBookData = null;
+
+let isInList = false;
+let currentListEntry = null;
+let totalUnits = 0;
+
+const API_BASE = '/api';
+
window.addEventListener('scroll', () => {
const nav = document.getElementById('navbar');
if (window.scrollY > 50) nav.classList.add('scrolled');
@@ -100,6 +108,7 @@ function renderSearchResults(results) {
item.className = 'search-item';
let href;
if (book.isExtensionResult) {
+
href = `/book/${book.extensionName}/${book.id}`;
} else {
href = `/book/${book.id}`;
@@ -173,8 +182,310 @@ function startHeroCycle() {
}, 8000);
}
+function getAuthToken() {
+ return localStorage.getItem('token');
+}
+
+function getAuthHeaders() {
+ const token = getAuthToken();
+ return {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ };
+}
+
+function getSimpleAuthHeaders() {
+ const token = localStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`
+ };
+}
+
+function getBookEntryType(bookData) {
+ if (!bookData) return 'MANGA';
+
+ const format = bookData.format?.toUpperCase() || 'MANGA';
+ return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL';
+}
+
+function showNotification(message, type = 'info') {
+ const notification = document.createElement('div');
+ notification.style.cssText = `
+ position: fixed;
+ top: 100px;
+ right: 20px;
+ background: ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : '#8b5cf6'};
+ color: white;
+ padding: 1rem 1.5rem;
+ border-radius: 12px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
+ z-index: 9999;
+ font-weight: 600;
+ animation: slideInRight 0.3s ease;
+ `;
+ notification.textContent = message;
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.style.animation = 'slideOutRight 0.3s ease';
+ setTimeout(() => notification.remove(), 300);
+ }, 3000);
+}
+
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes slideInRight {
+ from { transform: translateX(400px); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+ }
+ @keyframes slideOutRight {
+ from { transform: translateX(0); opacity: 1; }
+ to { transform: translateX(400px); opacity: 0; }
+ }
+`;
+document.head.appendChild(style);
+
+async function checkIfInList(bookId) {
+ if (!currentBookData) return false;
+
+ const entryId = bookId;
+ const source = 'anilist';
+ const entryType = getBookEntryType(currentBookData);
+
+ const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`;
+
+ 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;
+ }
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('Error checking single list entry:', error);
+ return false;
+ }
+}
+
+function updateAddToListButton() {
+ const btn = document.querySelector('.hero-buttons .btn-blur');
+ if (!btn) return;
+
+ if (isInList) {
+ btn.innerHTML = `
+
+ In Your Library
+ `;
+ btn.style.background = 'rgba(34, 197, 94, 0.2)';
+ btn.style.color = '#22c55e';
+ btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
+ btn.onclick = openAddToListModal;
+
+ } else {
+ btn.innerHTML = '+ Add to Library';
+ btn.style.background = null;
+ btn.style.color = null;
+ btn.style.borderColor = null;
+ btn.onclick = openAddToListModal;
+
+ }
+}
+
+async function openAddToListModal() {
+ if (!getAuthToken()) {
+ showNotification('Please log in to manage your library.', 'error');
+ return;
+ }
+ if (!currentBookData) return;
+
+ await checkIfInList(currentBookData.id);
+
+ const totalUnits = currentBookData.chapters || currentBookData.volumes || 999;
+ const entryType = getBookEntryType(currentBookData);
+
+ const modalTitle = document.getElementById('modal-title');
+ const deleteBtn = document.getElementById('modal-delete-btn');
+ const progressLabel = document.getElementById('progress-label');
+
+ let actualProgressLabel = progressLabel;
+ if (!actualProgressLabel) {
+
+ const progressInput = document.getElementById('entry-progress');
+ if (progressInput) actualProgressLabel = progressInput.previousElementSibling;
+ }
+
+ if (isInList && currentListEntry) {
+ document.getElementById('entry-status').value = currentListEntry.status || 'PLANNING';
+ document.getElementById('entry-progress').value = currentListEntry.progress || 0;
+ document.getElementById('entry-score').value = currentListEntry.score || '';
+
+ document.getElementById('entry-start-date').value = currentListEntry.start_date ? currentListEntry.start_date.split('T')[0] : '';
+ document.getElementById('entry-end-date').value = currentListEntry.end_date ? currentListEntry.end_date.split('T')[0] : '';
+ document.getElementById('entry-repeat-count').value = currentListEntry.repeat_count || 0;
+ document.getElementById('entry-notes').value = currentListEntry.notes || '';
+ document.getElementById('entry-is-private').checked = currentListEntry.is_private === true || currentListEntry.is_private === 1;
+
+ modalTitle.textContent = 'Edit Library Entry';
+ deleteBtn.style.display = 'block';
+ } else {
+ document.getElementById('entry-status').value = 'PLANNING';
+ document.getElementById('entry-progress').value = 0;
+ document.getElementById('entry-score').value = '';
+ document.getElementById('entry-start-date').value = '';
+ document.getElementById('entry-end-date').value = '';
+ document.getElementById('entry-repeat-count').value = 0;
+ document.getElementById('entry-notes').value = '';
+ document.getElementById('entry-is-private').checked = false;
+
+ modalTitle.textContent = 'Add to Library';
+ deleteBtn.style.display = 'none';
+ }
+
+ if (actualProgressLabel) {
+ if (entryType === 'MANGA') {
+ actualProgressLabel.textContent = 'Chapters Read';
+ } else {
+ actualProgressLabel.textContent = 'Volumes/Parts Read';
+ }
+ }
+
+ document.getElementById('entry-progress').max = totalUnits;
+ document.getElementById('add-list-modal').classList.add('active');
+}
+
+function closeAddToListModal() {
+ document.getElementById('add-list-modal').classList.remove('active');
+}
+
+async function saveToList() {
+ const uiStatus = document.getElementById('entry-status').value;
+
+ const anilistStatusMap = {
+ CURRENT: 'CURRENT',
+ COMPLETED: 'COMPLETED',
+ PLANNING: 'PLANNING',
+ PAUSED: 'PAUSED',
+ DROPPED: 'DROPPED',
+ REPEATING: 'REPEATING'
+ };
+
+ const status = anilistStatusMap[uiStatus] || uiStatus;
+
+ const progress = parseInt(document.getElementById('entry-progress').value) || 0;
+ const scoreValue = document.getElementById('entry-score').value;
+ const score = scoreValue ? parseFloat(scoreValue) : null;
+
+ const start_date = document.getElementById('entry-start-date').value || null;
+ const end_date = document.getElementById('entry-end-date').value || null;
+ const repeat_count = parseInt(document.getElementById('entry-repeat-count').value) || 0;
+ const notes = document.getElementById('entry-notes').value || null;
+ const is_private = document.getElementById('entry-is-private').checked;
+
+ if (!currentBookData) {
+ showNotification('Cannot save: Book data not loaded.', 'error');
+ return;
+ }
+
+ const entryType = getBookEntryType(currentBookData);
+ const idToSave = currentBookData.id;
+
+ try {
+ const response = await fetch(`${API_BASE}/list/entry`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({
+ entry_id: idToSave,
+ source: 'anilist',
+
+ entry_type: entryType,
+ status: status,
+ progress: progress,
+ score: score,
+ start_date: start_date,
+ end_date: end_date,
+ repeat_count: repeat_count,
+ notes: notes,
+ is_private: is_private
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to save entry');
+ }
+
+ const data = await response.json();
+
+ isInList = true;
+ currentListEntry = data.entry;
+ updateAddToListButton();
+ closeAddToListModal();
+ showNotification(isInList ? 'Updated successfully!' : 'Added to your library!', 'success');
+ } catch (error) {
+ console.error('Error saving to list:', error);
+ showNotification('Failed to save. Please try again.', 'error');
+ }
+}
+
+async function deleteFromList() {
+ if (!confirm('Remove this book from your library?')) return;
+
+ const idToDelete = currentBookData ? currentBookData.id : null;
+ if (!idToDelete) return;
+
+ const source = 'anilist';
+ const entryType = getBookEntryType(currentBookData);
+
+ try {
+ const response = await fetch(`${API_BASE}/list/entry/${idToDelete}?source=${source}&entry_type=${entryType}`, {
+ method: 'DELETE',
+ headers: getSimpleAuthHeaders()
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete entry');
+ }
+
+ isInList = false;
+ currentListEntry = null;
+ updateAddToListButton();
+ closeAddToListModal();
+ showNotification('Removed from your library', 'success');
+ } catch (error) {
+ console.error('Error deleting from list:', error);
+ showNotification('Failed to remove. Please try again.', 'error');
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const modal = document.getElementById('add-list-modal');
+ if (modal) {
+ modal.addEventListener('click', (e) => {
+ if (e.target.id === 'add-list-modal') {
+ closeAddToListModal();
+ }
+ });
+ }
+});
+
function updateHeroUI(book) {
if(!book) return;
+
+ currentBookData = book;
+ totalUnits = book.chapters || book.volumes || 999;
+
const title = book.title.english || book.title.romaji;
const desc = book.description || "No description available.";
const poster = (book.coverImage && (book.coverImage.extraLarge || book.coverImage.large)) || '';
@@ -194,9 +505,19 @@ function updateHeroUI(book) {
const readBtn = document.getElementById('read-btn');
if (readBtn) {
-
readBtn.onclick = () => window.location.href = `/book/${book.id}`;
}
+
+ const addToListBtn = document.querySelector('.hero-buttons .btn-blur');
+ if(addToListBtn) {
+
+ addToListBtn.onclick = openAddToListModal;
+
+ checkIfInList(book.id).then(() => {
+ updateAddToListButton();
+ });
+ }
+
}
async function loadContinueReading() {
@@ -234,7 +555,6 @@ function renderContinueReading(id, list) {
return;
}
- // ordenar por updated_at DESC
list.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
list.forEach(item => {
@@ -251,7 +571,6 @@ function renderContinueReading(id, list) {
}
};
-
const progressText = item.total_episodes
? `${item.progress || 0}/${item.total_episodes}`
: `${item.progress || 0}`;
@@ -270,7 +589,6 @@ function renderContinueReading(id, list) {
});
}
-
function renderList(id, list) {
const container = document.getElementById(id);
container.innerHTML = '';
diff --git a/src/scripts/books/reader.js b/src/scripts/books/reader.js
index ab11699..2daa005 100644
--- a/src/scripts/books/reader.js
+++ b/src/scripts/books/reader.js
@@ -203,7 +203,6 @@ function loadManga(pages) {
isLongStrip = true;
}
-
const useDouble = config.manga.mode === 'double' ||
(config.manga.mode === 'auto' && !isLongStrip && shouldUseDoublePage(pages));
@@ -478,7 +477,6 @@ prevBtn.addEventListener('click', () => {
loadChapter();
});
-
nextBtn.addEventListener('click', () => {
const newChapter = String(parseInt(chapter) + 1);
updateURL(newChapter);
@@ -636,8 +634,10 @@ function setupProgressTracking(data, source) {
entry_type: data.type === 'manga' ? 'MANGA' : 'NOVEL',
status: 'CURRENT',
progress: source === 'anilist'
- ? Math.floor(chapterNumber) // ✅ AniList solo enteros
- : chapterNumber // ✅ Local acepta decimales
+ ? Math.floor(chapterNumber)
+
+ : chapterNumber
+
};
try {
@@ -654,7 +654,6 @@ function setupProgressTracking(data, source) {
}
}
-
function checkProgress() {
const scrollTop = window.scrollY;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
@@ -673,12 +672,10 @@ function setupProgressTracking(data, source) {
}
}
- // remove previous listener just in case
window.removeEventListener('scroll', checkProgress);
window.addEventListener('scroll', checkProgress);
}
-
if (!bookId || !chapter || !provider) {
reader.innerHTML = `
diff --git a/views/anime/animes.html b/views/anime/animes.html
index 93f3fec..3db2b3c 100644
--- a/views/anime/animes.html
+++ b/views/anime/animes.html
@@ -7,6 +7,7 @@
+
@@ -41,7 +42,6 @@
-
+
+
+
+
Add to List
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+