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} +
+
${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 += ` -
-
${name} -
`; - } - }); - } 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 @@
- +
@@ -189,4 +256,4 @@ - + \ No newline at end of file diff --git a/views/books/books.html b/views/books/books.html index 78878de..e92ff26 100644 --- a/views/books/books.html +++ b/views/books/books.html @@ -8,6 +8,7 @@ + @@ -111,6 +112,71 @@
+
diff --git a/views/css/components/anilist-modal.css b/views/css/components/anilist-modal.css index 231a944..5312c7b 100644 --- a/views/css/components/anilist-modal.css +++ b/views/css/components/anilist-modal.css @@ -11,7 +11,7 @@ } .modal-content { - background: var(--bg-amoled); + background: var(--color-bg-amoled); border: 1px solid rgba(255,255,255,0.1); border-radius: var(--radius-lg); max-width: 900px; @@ -56,8 +56,8 @@ } .modal-close:hover { - background: var(--danger); - border-color: var(--danger); + background: var(--color-danger); + border-color: var(--color-danger); } .modal-title { @@ -65,7 +65,7 @@ font-weight: 800; padding: 1.5rem 2rem 0.5rem; margin-bottom: 0; - color: var(--text-primary); + color: var(--color-text-primary); border-bottom: 1px solid rgba(255,255,255,0.05); } @@ -77,7 +77,6 @@ flex-grow: 1; } -/* GRUPO PRINCIPAL DE CAMPOS */ .modal-fields-grid { display: grid; grid-template-columns: repeat(3, 1fr); @@ -91,7 +90,6 @@ gap: 0.5rem; } -/* Column Span Overrides */ .form-group.notes-group { grid-column: 1 / span 2; } @@ -104,19 +102,18 @@ grid-column: 1 / -1; } - .form-group label { font-size: 0.8rem; font-weight: 700; - color: var(--text-secondary); + color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.5px; } .form-input { - background: var(--bg-field); + background: var(--color-bg-field); border: 1px solid rgba(255,255,255,0.1); - color: var(--text-primary); + color: var(--color-text-primary); padding: 0.8rem 1rem; border-radius: 8px; font-family: inherit; @@ -126,8 +123,8 @@ .form-input:focus { outline: none; - border-color: var(--accent); - box-shadow: 0 0 10px var(--accent-glow); + border-color: var(--color-primary); + box-shadow: 0 0 10px var(--color-primary-glow); } .notes-textarea { @@ -140,7 +137,6 @@ gap: 1rem; } -/* CLAVE: Hace que la etiqueta de fecha esté encima del input */ .date-input-pair { flex: 1; min-width: 0; @@ -159,7 +155,7 @@ width: 18px; height: 18px; border: 1px solid rgba(255,255,255,0.2); - background: var(--bg-base); + background: var(--color-bg-base); border-radius: 4px; cursor: pointer; -webkit-appearance: none; @@ -170,8 +166,8 @@ } .form-checkbox:checked { - background: var(--accent); - border-color: var(--accent); + background: var(--color-primary); + border-color: var(--color-primary); } .form-checkbox:checked::after { @@ -184,7 +180,6 @@ font-size: 14px; } -/* ACCIONES (Barra inferior pegajosa) */ .modal-actions { display: flex; gap: 1rem; @@ -193,7 +188,7 @@ flex-shrink: 0; padding: 1rem 2rem; border-top: 1px solid rgba(255,255,255,0.05); - background: var(--bg-amoled); + background: var(--color-bg-amoled); position: sticky; bottom: 0; z-index: 10; @@ -211,7 +206,7 @@ } .btn-primary { - background: var(--accent); + background: var(--color-primary); color: white; } @@ -230,7 +225,7 @@ } .btn-danger { - background: var(--danger); + background: var(--color-danger); color: white; margin-right: auto; } @@ -248,7 +243,6 @@ opacity: 1; } -/* --- Media Queries (Responsive) --- */ @media (max-width: 900px) { .modal-content { max-width: 95%; @@ -271,3 +265,4 @@ padding-right: 1.5rem; } } + diff --git a/views/gallery/image.html b/views/gallery/image.html index ca700fb..9789c90 100644 --- a/views/gallery/image.html +++ b/views/gallery/image.html @@ -97,7 +97,7 @@
-

Loading similar images...

+

Loading similar images...