diff --git a/src/scripts/anime/anime.js b/src/scripts/anime/anime.js index 9536c5c..f605e3c 100644 --- a/src/scripts/anime/anime.js +++ b/src/scripts/anime/anime.js @@ -1,277 +1,301 @@ -const animeId = window.location.pathname.split('/').pop(); -let player; +let animeData = null; +let extensionName = null; +let animeId = null; -let totalEpisodes = 0; -let currentPage = 1; -const itemsPerPage = 12; +const episodePagination = Object.create(PaginationManager); +episodePagination.init(12, renderEpisodes); -var tag = document.createElement('script'); -tag.src = "https://www.youtube.com/iframe_api"; -var firstScriptTag = document.getElementsByTagName('script')[0]; -firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); +YouTubePlayerUtils.init('player'); -let extensionName; +document.addEventListener('DOMContentLoaded', () => { + loadAnime(); + setupDescriptionModal(); + setupEpisodeSearch(); +}); async function loadAnime() { try { - const path = window.location.pathname; - const parts = path.split("/").filter(Boolean); - let animeId; - if (parts.length === 3) { - extensionName = parts[1]; - animeId = parts[2]; - } else { - animeId = parts[1]; + const urlData = URLUtils.parseEntityPath('anime'); + if (!urlData) { + showError("Invalid URL"); + return; } + extensionName = urlData.extensionName; + animeId = urlData.entityId; + const fetchUrl = extensionName ? `/api/anime/${animeId}?source=${extensionName}` : `/api/anime/${animeId}?source=anilist`; - const res = await fetch(fetchUrl); + + const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() }); const data = await res.json(); if (data.error) { - document.getElementById('title').innerText = "Anime Not Found"; + showError("Anime Not Found"); return; } - const title = data.title?.english || data.title?.romaji || data.title || "Unknown Title"; - document.title = `${title} | WaifuBoard`; - document.getElementById('title').innerText = title; + animeData = data; - let posterUrl = ''; + const metadata = MediaMetadataUtils.formatAnimeData(data, !!extensionName); - if (extensionName) { - posterUrl = data.image || ''; + updatePageTitle(metadata.title); + updateMetadata(metadata); + updateDescription(data.description || data.summary); + updateCharacters(metadata.characters); + updateExtensionPill(); - } else { - posterUrl = data.coverImage?.extraLarge || ''; - } + setupWatchButton(); - if (posterUrl) { - document.getElementById('poster').src = posterUrl; - } + const hasTrailer = YouTubePlayerUtils.playTrailer( + metadata.trailer, + 'player', + metadata.banner + ); - const rawDesc = data.description || data.summary || "No description available."; - handleDescription(rawDesc); + setupEpisodes(metadata.episodes); - const score = extensionName ? (data.score ? data.score * 10 : '?') : data.averageScore; - document.getElementById('score').innerText = (score || '?') + '% Score'; - - document.getElementById('year').innerText = - extensionName ? (data.year || '????') : (data.seasonYear || data.startDate?.year || '????'); - - document.getElementById('genres').innerText = - data.genres?.length > 0 ? data.genres.slice(0, 3).join(' • ') : ''; - - document.getElementById('format').innerText = data.format || 'TV'; - - document.getElementById('status').innerText = data.status || 'Unknown'; - - const extensionPill = document.getElementById('extension-pill'); - if (extensionName && extensionPill) { - extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`; - extensionPill.style.display = 'inline-flex'; - } else if (extensionPill) { - extensionPill.style.display = 'none'; - } - - let seasonText = ''; - if (extensionName) { - seasonText = data.season || 'Unknown'; - } else { - if (data.season && data.seasonYear) { - seasonText = `${data.season} ${data.seasonYear}`; - } else if (data.startDate?.year) { - const months = ['', 'Winter', 'Winter', 'Spring', 'Spring', 'Spring', 'Summer', 'Summer', 'Summer', 'Fall', 'Fall', 'Fall', 'Winter']; - const month = data.startDate.month || 1; - const estimatedSeason = months[month] || ''; - seasonText = `${estimatedSeason} ${data.startDate.year}`.trim(); - } - } - document.getElementById('season').innerText = seasonText || 'Unknown'; - - const studio = extensionName - ? data.studio || "Unknown" - : (data.studios?.nodes?.[0]?.name || - data.studios?.edges?.[0]?.node?.name || - 'Unknown Studio'); - - document.getElementById('studio').innerText = studio; - - const charContainer = document.getElementById('char-list'); - charContainer.innerHTML = ''; - - let characters = []; - - if (extensionName) { - characters = data.characters || []; - } else { - if (data.characters?.nodes?.length > 0) { - characters = data.characters.nodes.slice(0, 5); - } else if (data.characters?.edges?.length > 0) { - characters = data.characters.edges - .filter(edge => edge?.node?.name?.full) - .slice(0, 5) - .map(edge => edge.node); - } - } - - if (characters.length > 0) { - characters.slice(0, 5).forEach(char => { - const name = char?.name?.full || char?.name; - if (name) { - charContainer.innerHTML += ` -
-
${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 setupAddToListButton(); } catch (err) { console.error('Error loading anime:', err); - document.getElementById('title').innerText = "Error loading anime"; + showError("Error loading anime"); } } -function handleDescription(text) { - const tmp = document.createElement("DIV"); - tmp.innerHTML = text; - const cleanText = tmp.textContent || tmp.innerText || ""; +function updatePageTitle(title) { + document.title = `${title} | WaifuBoard`; + document.getElementById('title').innerText = title; +} - const sentences = cleanText.match(/[^\.!\?]+[\.!\?]+/g) || [cleanText]; +function updateMetadata(metadata) { - document.getElementById('full-description').innerHTML = text; + if (metadata.poster) { + document.getElementById('poster').src = metadata.poster; + } - 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'; + document.getElementById('score').innerText = `${metadata.score}% Score`; + + document.getElementById('year').innerText = metadata.year; + + document.getElementById('genres').innerText = metadata.genres; + + document.getElementById('format').innerText = metadata.format; + + document.getElementById('status').innerText = metadata.status; + + document.getElementById('season').innerText = metadata.season; + + document.getElementById('studio').innerText = metadata.studio; + + document.getElementById('episodes').innerText = metadata.episodes; +} + +function updateDescription(rawDescription) { + const desc = MediaMetadataUtils.truncateDescription(rawDescription, 4); + + document.getElementById('description-preview').innerHTML = desc.short; + document.getElementById('full-description').innerHTML = desc.full; + + const readMoreBtn = document.getElementById('read-more-btn'); + if (desc.isTruncated) { + readMoreBtn.style.display = 'inline-flex'; } else { - document.getElementById('description-preview').innerHTML = text; - document.getElementById('read-more-btn').style.display = 'none'; + readMoreBtn.style.display = 'none'; } } -function openModal() { - document.getElementById('desc-modal').classList.add('active'); - document.body.style.overflow = 'hidden'; +function updateCharacters(characters) { + const container = document.getElementById('char-list'); + container.innerHTML = ''; + + if (characters.length > 0) { + characters.forEach(char => { + container.innerHTML += ` +
+
${char.name} +
`; + }); + } else { + container.innerHTML = ` +
+ No character data available +
`; + } } -function closeModal() { - document.getElementById('desc-modal').classList.remove('active'); - document.body.style.overflow = ''; +function updateExtensionPill() { + const pill = document.getElementById('extension-pill'); + if (!pill) return; + + if (extensionName) { + pill.textContent = extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase(); + pill.style.display = 'inline-flex'; + } else { + pill.style.display = 'none'; + } } -document.getElementById('desc-modal').addEventListener('click', (e) => { - if (e.target.id === 'desc-modal') closeModal(); -}); +function setupWatchButton() { + const watchBtn = document.getElementById('watch-btn'); + if (watchBtn) { + watchBtn.onclick = () => { + const url = URLUtils.buildWatchUrl(animeId, 1, extensionName); + window.location.href = url; + }; + } +} + +async function setupAddToListButton() { + const btn = document.getElementById('add-to-list-btn'); + if (!btn || !animeData) return; + + ListModalManager.currentData = animeData; + const entryType = ListModalManager.getEntryType(animeData); + + await ListModalManager.checkIfInList(animeId, extensionName || 'anilist', entryType); + + const tempBtn = document.querySelector('.hero-buttons .btn-blur'); + if (tempBtn) { + ListModalManager.updateButton('.hero-buttons .btn-blur'); + } else { + + updateCustomAddButton(); + } + + btn.onclick = () => ListModalManager.open(animeData, extensionName || 'anilist'); +} + +function updateCustomAddButton() { + const btn = document.getElementById('add-to-list-btn'); + if (!btn) return; + + if (ListModalManager.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; + } +} + +function setupEpisodes(totalEpisodes) { + + const limitedTotal = Math.min(Math.max(totalEpisodes, 1), 5000); + + episodePagination.setTotalItems(limitedTotal); + renderEpisodes(); +} function renderEpisodes() { const grid = document.getElementById('episodes-grid'); + if (!grid) return; + grid.innerHTML = ''; - const start = (currentPage - 1) * itemsPerPage + 1; - const end = Math.min(start + itemsPerPage - 1, totalEpisodes); + const range = episodePagination.getPageRange(); + const start = range.start + 1; - for(let i = start; i <= end; i++) { + const end = range.end; + + for (let i = start; i <= end; i++) { createEpisodeButton(i, grid); } - updatePaginationControls(); + + episodePagination.renderControls( + 'pagination-controls', + 'page-info', + 'prev-page', + 'next-page' + ); } 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}` : ""); - + btn.onclick = () => { + const url = URLUtils.buildWatchUrl(animeId, num, extensionName); + window.location.href = url; + }; 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; +function setupDescriptionModal() { + const modal = document.getElementById('desc-modal'); + if (!modal) return; - document.getElementById('pagination-controls').style.display = 'flex'; + modal.addEventListener('click', (e) => { + if (e.target.id === 'desc-modal') { + closeDescriptionModal(); + } + }); } -function changePage(delta) { - currentPage += delta; - renderEpisodes(); +function openDescriptionModal() { + document.getElementById('desc-modal').classList.add('active'); + document.body.style.overflow = 'hidden'; } -const searchInput = document.getElementById('ep-search'); -searchInput.addEventListener('input', (e) => { - const val = parseInt(e.target.value); - const grid = document.getElementById('episodes-grid'); +function closeDescriptionModal() { + document.getElementById('desc-modal').classList.remove('active'); + document.body.style.overflow = ''; +} - 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'; - } -}); +function setupEpisodeSearch() { + const searchInput = document.getElementById('ep-search'); + if (!searchInput) return; -loadAnime(); \ No newline at end of file + searchInput.addEventListener('input', (e) => { + const val = parseInt(e.target.value); + const grid = document.getElementById('episodes-grid'); + const totalEpisodes = episodePagination.totalItems; + + 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'; + } + }); +} + +function showError(message) { + document.getElementById('title').innerText = message; +} + +function saveToList() { + if (!animeId) return; + ListModalManager.save(animeId, extensionName || 'anilist'); +} + +function deleteFromList() { + if (!animeId) return; + ListModalManager.delete(animeId, extensionName || 'anilist'); +} + +function closeAddToListModal() { + ListModalManager.close(); +} + +window.openDescriptionModal = openDescriptionModal; +window.closeDescriptionModal = closeDescriptionModal; +window.changePage = (delta) => { + if (delta > 0) episodePagination.nextPage(); + else episodePagination.prevPage(); +}; \ No newline at end of file diff --git a/src/scripts/anime/animes.js b/src/scripts/anime/animes.js index 4e164ab..4ae562c 100644 --- a/src/scripts/anime/animes.js +++ b/src/scripts/anime/animes.js @@ -1,633 +1,320 @@ -const searchInput = document.getElementById('search-input'); -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'; +let currentList = []; +let filteredList = []; -function getAuthToken() { - return localStorage.getItem('token'); +document.addEventListener('DOMContentLoaded', async () => { + await loadList(); + setupEventListeners(); +}); + +function getEntryLink(item) { + const isAnime = item.entry_type?.toUpperCase() === 'ANIME'; + const baseRoute = isAnime ? '/anime' : '/book'; + const source = item.source || 'anilist'; + + if (source === 'anilist') { + return `${baseRoute}/${item.entry_id}`; + } else { + return `${baseRoute}/${source}/${item.entry_id}`; + } } -function getAuthHeaders() { - const token = getAuthToken(); - return { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }; -} +async function populateSourceFilter() { + const select = document.getElementById('source-filter'); + if (!select) return; -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; + select.innerHTML = ` + + `; - 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() - }); - + const response = await fetch(`${API_BASE}/extensions`); if (response.ok) { const data = await response.json(); + const extensions = data.extensions || []; - if (data.found && data.entry) { - isInList = true; - currentListEntry = data.entry; - } else { - isInList = false; - currentListEntry = null; - } - return true; + extensions.forEach(extName => { + if (extName.toLowerCase() !== 'anilist' && extName.toLowerCase() !== 'local') { + const option = document.createElement('option'); + option.value = extName; + option.textContent = extName.charAt(0).toUpperCase() + extName.slice(1); + select.appendChild(option); + } + }); } - return false; } catch (error) { - console.error('Error checking single list entry:', error); - return false; + console.error('Error loading extensions:', error); } } -function updateAddToListButton() { - const btn = document.querySelector('.hero-buttons .btn-blur'); - if (!btn) return; +function setupEventListeners() { - 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; - } + document.querySelectorAll('.view-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const view = btn.dataset.view; + const container = document.getElementById('list-container'); + if (view === 'list') { + container.classList.add('list-view'); + } else { + container.classList.remove('list-view'); + } + }); + }); + + document.getElementById('status-filter').addEventListener('change', applyFilters); + document.getElementById('source-filter').addEventListener('change', applyFilters); + document.getElementById('type-filter').addEventListener('change', applyFilters); + document.getElementById('sort-filter').addEventListener('change', applyFilters); + + document.querySelector('.search-input').addEventListener('input', (e) => { + const query = e.target.value.toLowerCase(); + if (query) { + filteredList = currentList.filter(item => + item.title?.toLowerCase().includes(query) + ); + } else { + filteredList = [...currentList]; + } + applyFilters(); + }); + + document.getElementById('modal-save-btn')?.addEventListener('click', async () => { + + const entryToSave = window.ListModalManager.currentEntry || window.ListModalManager.currentData; + + if (!entryToSave) return; + + await window.ListModalManager.save(entryToSave.entry_id, entryToSave.source); + + await loadList(); + }); + + document.getElementById('modal-delete-btn')?.addEventListener('click', async () => { + const entryToDelete = window.ListModalManager.currentEntry || window.ListModalManager.currentData; + + if (!entryToDelete) return; + + await window.ListModalManager.delete(entryToDelete.entry_id, entryToDelete.source); + + await loadList(); + }); + + document.getElementById('add-list-modal')?.addEventListener('click', (e) => { + if (e.target.id === 'add-list-modal') { + window.ListModalManager.close(); + } + }); } -async function openAddToListModal() { - if (!getAuthToken()) { - showNotification('Please log in to manage your list.', 'error'); - return; - } - if (!currentAnimeData) return; +async function loadList() { + const loadingState = document.getElementById('loading-state'); + const emptyState = document.getElementById('empty-state'); + const container = document.getElementById('list-container'); - 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; + await populateSourceFilter(); try { - const response = await fetch(`${API_BASE}/list/entry`, { - method: 'POST', - headers: getAuthHeaders(), - body: JSON.stringify({ - entry_id: animeId, - source: 'anilist', + loadingState.style.display = 'flex'; + emptyState.style.display = 'none'; + container.innerHTML = ''; - 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 - }) + const response = await fetch(`${API_BASE}/list`, { + headers: window.AuthUtils.getSimpleAuthHeaders() }); if (!response.ok) { - throw new Error('Failed to save entry'); + throw new Error('Failed to load list'); } const data = await response.json(); + currentList = data.results || []; + filteredList = [...currentList]; - 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'); - } -} + loadingState.style.display = 'none'; -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'); + if (currentList.length === 0) { + emptyState.style.display = 'flex'; + } else { + updateStats(); + applyFilters(); } - - 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); - 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)}`; - } + console.error('Error loading list:', error); + loadingState.style.display = 'none'; + if (window.NotificationUtils) { + window.NotificationUtils.error('Failed to load your list. Please try again.'); + } else { + alert('Failed to load your list. Please try again.'); } - - 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; +function updateStats() { + const total = currentList.length; + const watching = currentList.filter(item => item.status === 'WATCHING').length; + const completed = currentList.filter(item => item.status === 'COMPLETED').length; + const planning = currentList.filter(item => item.status === 'PLANNING').length; - 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); - } + document.getElementById('total-count').textContent = total; + document.getElementById('watching-count').textContent = watching; + document.getElementById('completed-count').textContent = completed; + document.getElementById('planned-count').textContent = planning; } -function renderContinueWatching(id, list) { - const container = document.getElementById(id); - if (!container) return; +function applyFilters() { + const statusFilter = document.getElementById('status-filter').value; + const sourceFilter = document.getElementById('source-filter').value; + const typeFilter = document.getElementById('type-filter').value; + const sortFilter = document.getElementById('sort-filter').value; + let filtered = [...filteredList]; + + if (statusFilter !== 'all') { + filtered = filtered.filter(item => item.status === statusFilter); + } + + if (sourceFilter !== 'all') { + filtered = filtered.filter(item => item.source === sourceFilter); + } + + if (typeFilter !== 'all') { + filtered = filtered.filter(item => item.entry_type === typeFilter); + } + + switch (sortFilter) { + case 'title': + filtered.sort((a, b) => (a.title || '').localeCompare(b.title || '')); + break; + case 'score': + filtered.sort((a, b) => (b.score || 0) - (a.score || 0)); + break; + case 'progress': + filtered.sort((a, b) => (b.progress || 0) - (a.progress || 0)); + break; + case 'updated': + default: + filtered.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + break; + } + + renderList(filtered); +} + +function renderList(items) { + const container = document.getElementById('list-container'); container.innerHTML = ''; - if (list.length === 0) { - container.innerHTML = `
No watching anime
`; + if (items.length === 0) { + container.innerHTML = '

No entries match your filters

'; return; } - list.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + items.forEach(item => { + const element = createListItem(item); + container.appendChild(element); + }); +} - list.forEach(item => { - const el = document.createElement('div'); - el.className = 'card'; +function createListItem(item) { + const div = document.createElement('div'); + div.className = 'list-item'; - el.onclick = () => { - const ep = item.progress || 1; + const itemLink = getEntryLink(item); - 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 posterUrl = item.poster || '/public/assets/placeholder.png'; + const progress = item.progress || 0; - const progressText = item.total_episodes - ? `${item.progress || 0}/${item.total_episodes}` - : `${item.progress || 0}`; + const totalUnits = item.entry_type === 'ANIME' ? + item.total_episodes || 0 : + item.total_chapters || 0; - el.innerHTML = ` -
- + const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0; + const score = item.score ? item.score.toFixed(1) : null; + const repeatCount = item.repeat_count || 0; + + const entryType = (item.entry_type).toUpperCase(); + let unitLabel = 'units'; + if (entryType === 'ANIME') { + unitLabel = 'episodes'; + } else if (entryType === 'MANGA') { + unitLabel = 'chapters'; + } else if (entryType === 'NOVEL') { + unitLabel = 'chapters/volumes'; + } + + const statusLabels = { + 'WATCHING': entryType === 'ANIME' ? 'Watching' : 'Reading', + 'COMPLETED': 'Completed', + 'PLANNING': 'Planning', + 'PAUSED': 'Paused', + 'DROPPED': 'Dropped' + }; + + const extraInfo = []; + if (repeatCount > 0) { + extraInfo.push(`🔁 ${repeatCount}`); + } + if (item.is_private) { + extraInfo.push('🔒 Private'); + } + + const entryDataString = JSON.stringify(item).replace(/'/g, '''); + + div.innerHTML = ` + + ${item.title || 'Entry'} + +
+
+ +

${item.title || 'Unknown Title'}

+
+
+ ${statusLabels[item.status] || item.status} + ${entryType} + ${item.source.toUpperCase()} + ${extraInfo.join('')} +
-
-

${item.title}

-

Ep ${progressText} - ${item.source}

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

${title}

-

${score}% • ${ep}

-
- `; - container.appendChild(el); - }); -} - -fetchContent(); -loadContinueWatching(); -setInterval(() => fetchContent(true), 60000); \ No newline at end of file + return div; +} \ No newline at end of file diff --git a/src/scripts/books/book.js b/src/scripts/books/book.js index d92ad83..42b132b 100644 --- a/src/scripts/books/book.js +++ b/src/scripts/books/book.js @@ -1,110 +1,141 @@ -const bookId = window.location.pathname.split('/').pop(); -let allChapters = []; -let filteredChapters = []; -let currentPage = 1; -const itemsPerPage = 12; +let bookData = null; let extensionName = null; +let bookId = null; let bookSlug = null; -let currentBookData = null; -let isInList = false; -let currentListEntry = null; +let allChapters = []; +let filteredChapters = []; -const API_BASE = '/api'; +const chapterPagination = Object.create(PaginationManager); +chapterPagination.init(12, () => renderChapterTable()); -function getBookUrl(id, source = 'anilist') { - return `/api/book/${id}?source=${source}`; -} - -function getChaptersUrl(id, source = 'anilist') { - return `/api/book/${id}/chapters?source=${source}`; -} - -function getAuthToken() { - return localStorage.getItem('token'); -} - -function getAuthHeaders() { - const token = getAuthToken(); - return { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }; -} - -function getSimpleAuthHeaders() { - const token = getAuthToken(); - return { - 'Authorization': `Bearer ${token}` - }; -} - -function applyChapterFromUrlFilter() { - const params = new URLSearchParams(window.location.search); - const chapterParam = params.get('chapter'); - - if (!chapterParam) return; - - const chapterNumber = parseFloat(chapterParam); - if (isNaN(chapterNumber)) return; - - filteredChapters = allChapters.filter( - ch => parseFloat(ch.number) === chapterNumber - ); - - currentPage = 1; -} - -function getBookEntryType(bookData) { - if (!bookData) return 'MANGA'; - - const format = bookData.format?.toUpperCase() || 'MANGA'; - return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL'; -} - -async function checkIfInList() { - if (!currentBookData) return; - - const entryId = extensionName ? bookSlug : bookId; - const source = extensionName || 'anilist'; - - const entryType = getBookEntryType(currentBookData); - - const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`; +document.addEventListener('DOMContentLoaded', () => { + init(); + setupModalClickOutside(); +}); +async function init() { try { - const response = await fetch(fetchUrl, { - headers: getSimpleAuthHeaders() - }); - if (response.ok) { - const data = await response.json(); - - if (data.found && data.entry) { - - isInList = true; - currentListEntry = data.entry; - } else { - isInList = false; - currentListEntry = null; - } - updateAddToListButton(); - } else if (response.status === 404) { - - isInList = false; - currentListEntry = null; - updateAddToListButton(); + const urlData = URLUtils.parseEntityPath('book'); + if (!urlData) { + showError("Book Not Found"); + return; } - } catch (error) { - console.error('Error checking single list entry:', error); + + extensionName = urlData.extensionName; + bookId = urlData.entityId; + bookSlug = urlData.slug; + + await loadBookMetadata(); + + await loadChapters(); + + await setupAddToListButton(); + + } catch (err) { + console.error("Metadata Error:", err); + showError("Error loading book"); } } -function updateAddToListButton() { +async function loadBookMetadata() { + const source = extensionName || 'anilist'; + const fetchUrl = `/api/book/${bookId}?source=${source}`; + + const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() }); + const data = await res.json(); + + if (data.error || !data) { + showError("Book Not Found"); + return; + } + + bookData = data; + + const metadata = MediaMetadataUtils.formatBookData(data, !!extensionName); + + updatePageTitle(metadata.title); + updateMetadata(metadata); + updateExtensionPill(); +} + +function updatePageTitle(title) { + document.title = `${title} | WaifuBoard Books`; + const titleEl = document.getElementById('title'); + if (titleEl) titleEl.innerText = title; +} + +function updateMetadata(metadata) { + + const descEl = document.getElementById('description'); + if (descEl) descEl.innerHTML = metadata.description; + + const scoreEl = document.getElementById('score'); + if (scoreEl) { + scoreEl.innerText = extensionName + ? `${metadata.score}` + : `${metadata.score}% Score`; + } + + const pubEl = document.getElementById('published-date'); + if (pubEl) pubEl.innerText = metadata.year; + + const statusEl = document.getElementById('status'); + if (statusEl) statusEl.innerText = metadata.status; + + const formatEl = document.getElementById('format'); + if (formatEl) formatEl.innerText = metadata.format; + + const chaptersEl = document.getElementById('chapters'); + if (chaptersEl) chaptersEl.innerText = metadata.chapters; + + const genresEl = document.getElementById('genres'); + if (genresEl) genresEl.innerText = metadata.genres; + + const posterEl = document.getElementById('poster'); + if (posterEl) posterEl.src = metadata.poster; + + const heroBgEl = document.getElementById('hero-bg'); + if (heroBgEl) heroBgEl.src = metadata.banner; +} + +function updateExtensionPill() { + const pill = document.getElementById('extension-pill'); + if (!pill) return; + + if (extensionName) { + pill.textContent = extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase(); + pill.style.display = 'inline-flex'; + } else { + pill.style.display = 'none'; + } +} + +async function setupAddToListButton() { + const btn = document.getElementById('add-to-list-btn'); + if (!btn || !bookData) return; + + ListModalManager.currentData = bookData; + const entryType = ListModalManager.getEntryType(bookData); + const idForCheck = extensionName ? bookSlug : bookId; + + await ListModalManager.checkIfInList( + idForCheck, + extensionName || 'anilist', + entryType + ); + + updateCustomAddButton(); + + btn.onclick = () => ListModalManager.open(bookData, extensionName || 'anilist'); +} + +function updateCustomAddButton() { const btn = document.getElementById('add-to-list-btn'); if (!btn) return; - if (isInList) { + if (ListModalManager.isInList) { btn.innerHTML = ` @@ -113,339 +144,32 @@ function updateAddToListButton() { `; 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; + btn.style.borderColor = 'rgba(34, 197, 94, 0.3)'; } else { btn.innerHTML = '+ Add to Library'; btn.style.background = null; btn.style.color = null; btn.style.borderColor = null; - btn.onclick = openAddToListModal; } } -function openAddToListModal() { - if (!currentBookData) return; - - 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'); - - 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; - } - - 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 (progressLabel) { - if (entryType === 'MANGA') { - progressLabel.textContent = 'Chapters Read'; - } else { - progressLabel.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 = { - 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; - - if (!currentBookData) { - showNotification('Cannot save: Book data not loaded.', 'error'); - return; - } - - const entryType = getBookEntryType(currentBookData); - const idToSave = extensionName ? bookSlug : bookId; - - try { - const response = await fetch(`${API_BASE}/list/entry`, { - method: 'POST', - headers: getAuthHeaders(), - body: JSON.stringify({ - entry_id: idToSave, - source: extensionName || '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 = extensionName ? bookSlug : bookId; - const source = extensionName || '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'); - } -} - -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 init() { - try { - const path = window.location.pathname; - const parts = path.split("/").filter(Boolean); - let currentBookId; - - if (parts.length === 3) { - extensionName = parts[1]; - bookSlug = parts[2]; - - currentBookId = bookSlug; - } else { - currentBookId = parts[1]; - - } - - const idForFetch = currentBookId; - - const fetchUrl = getBookUrl( - idForFetch, - extensionName || 'anilist' - ); - - const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() }); - const data = await res.json(); - - if (data.error || !data) { - const titleEl = document.getElementById('title'); - if (titleEl) titleEl.innerText = "Book Not Found"; - return; - } - - currentBookData = data; - - let title, description, score, year, status, format, chapters, poster, banner, genres; - - if (extensionName) { - - title = data.title || data.name || "Unknown"; - description = data.summary || "No description available."; - score = data.score ? Math.round(data.score) : '?'; - - year = data.published || '????'; - - status = data.status || 'Unknown'; - format = data.format || 'LN'; - chapters = data.chapters || '?'; - poster = data.image || ''; - - banner = poster; - - genres = Array.isArray(data.genres) ? data.genres.slice(0, 3).join(' • ') : ''; - - } else { - - title = data.title.english || data.title.romaji || "Unknown"; - description = data.description || "No description available."; - score = data.averageScore || '?'; - - year = (data.startDate && data.startDate.year) ? data.startDate.year : '????'; - status = data.status || 'Unknown'; - format = data.format || 'MANGA'; - chapters = data.chapters || '?'; - poster = data.coverImage.extraLarge || data.coverImage.large || ''; - banner = data.bannerImage || poster; - genres = data.genres ? data.genres.slice(0, 3).join(' • ') : ''; - - } - - document.title = `${title} | WaifuBoard Books`; - - const titleEl = document.getElementById('title'); - if (titleEl) titleEl.innerText = title; - - 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'; - } - - const descEl = document.getElementById('description'); - if (descEl) descEl.innerHTML = description; - - const scoreEl = document.getElementById('score'); - if (scoreEl) scoreEl.innerText = score + (extensionName ? '' : '% Score'); - - const pubEl = document.getElementById('published-date'); - if (pubEl) pubEl.innerText = year; - - const statusEl = document.getElementById('status'); - if (statusEl) statusEl.innerText = status; - - const formatEl = document.getElementById('format'); - if (formatEl) formatEl.innerText = format; - - const chaptersEl = document.getElementById('chapters'); - if (chaptersEl) chaptersEl.innerText = chapters; - - const genresEl = document.getElementById('genres'); - if(genresEl) { - genresEl.innerText = genres; - } - - const posterEl = document.getElementById('poster'); - if (posterEl) posterEl.src = poster; - - const heroBgEl = document.getElementById('hero-bg'); - if (heroBgEl) heroBgEl.src = banner; - - loadChapters(idForFetch); - - await checkIfInList(); - - } catch (err) { - console.error("Metadata Error:", err); - } -} - -async function loadChapters(idForFetch) { +async function loadChapters() { const tbody = document.getElementById('chapters-body'); if (!tbody) return; tbody.innerHTML = '
Searching extensions for chapters...
'; try { + const source = extensionName || 'anilist'; + const fetchUrl = `/api/book/${bookId}/chapters?source=${source}`; - const fetchUrl = getChaptersUrl( - idForFetch, - extensionName || 'anilist' - ); - - const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() }); + const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() }); const data = await res.json(); allChapters = data.chapters || []; filteredChapters = [...allChapters]; - applyChapterFromUrlFilter(); + + applyChapterFilter(); const totalEl = document.getElementById('total-chapters'); @@ -457,15 +181,12 @@ async function loadChapters(idForFetch) { if (totalEl) totalEl.innerText = `${allChapters.length} Found`; - populateProviderFilter(); + setupProviderFilter(); - const readBtn = document.getElementById('read-start-btn'); - if (readBtn && filteredChapters.length > 0) { + setupReadButton(); - readBtn.onclick = () => openReader(idForFetch, filteredChapters[0].id, filteredChapters[0].provider); - } - - renderTable(idForFetch); + chapterPagination.setTotalItems(filteredChapters.length); + renderChapterTable(); } catch (err) { tbody.innerHTML = '
Error loading chapters.
'; @@ -473,52 +194,72 @@ async function loadChapters(idForFetch) { } } -function populateProviderFilter() { +function applyChapterFilter() { + const chapterParam = URLUtils.getQueryParam('chapter'); + if (!chapterParam) return; + + const chapterNumber = parseFloat(chapterParam); + if (isNaN(chapterNumber)) return; + + filteredChapters = allChapters.filter( + ch => parseFloat(ch.number) === chapterNumber + ); + + chapterPagination.reset(); +} + +function setupProviderFilter() { const select = document.getElementById('provider-filter'); if (!select) return; const providers = [...new Set(allChapters.map(ch => ch.provider))]; - if (providers.length > 0) { - select.style.display = 'inline-block'; + if (providers.length === 0) return; - select.innerHTML = ''; + select.style.display = 'inline-block'; + select.innerHTML = ''; - providers.forEach(prov => { - const opt = document.createElement('option'); - opt.value = prov; - opt.innerText = prov; - select.appendChild(opt); - }); + providers.forEach(prov => { + const opt = document.createElement('option'); + opt.value = prov; + opt.innerText = prov; + select.appendChild(opt); + }); - if (extensionName) { + if (extensionName) { + const extensionProvider = providers.find( + p => p.toLowerCase() === extensionName.toLowerCase() + ); - const extensionProvider = providers.find(p => p.toLowerCase() === extensionName.toLowerCase()); + if (extensionProvider) { + select.value = extensionProvider; + filteredChapters = allChapters.filter(ch => ch.provider === extensionProvider); + } + } - if (extensionProvider) { - - select.value = extensionProvider; - - filteredChapters = allChapters.filter(ch => ch.provider === extensionProvider); - - } + select.onchange = (e) => { + const selected = e.target.value; + if (selected === 'all') { + filteredChapters = [...allChapters]; + } else { + filteredChapters = allChapters.filter(ch => ch.provider === selected); } - select.onchange = (e) => { - const selected = e.target.value; - if (selected === 'all') { - filteredChapters = [...allChapters]; - } else { - filteredChapters = allChapters.filter(ch => ch.provider === selected); - } - currentPage = 1; - const idForFetch = extensionName ? bookSlug : bookId; - renderTable(idForFetch); - }; - } + chapterPagination.reset(); + chapterPagination.setTotalItems(filteredChapters.length); + renderChapterTable(); + }; } -function renderTable(idForFetch) { +function setupReadButton() { + const readBtn = document.getElementById('read-start-btn'); + if (!readBtn || filteredChapters.length === 0) return; + + const firstChapter = filteredChapters[0]; + readBtn.onclick = () => openReader(0, firstChapter.provider); +} + +function renderChapterTable() { const tbody = document.getElementById('chapters-body'); if (!tbody) return; @@ -526,25 +267,25 @@ function renderTable(idForFetch) { if (filteredChapters.length === 0) { tbody.innerHTML = '
No chapters match this filter.
'; - updatePagination(); + chapterPagination.renderControls( + 'pagination', + 'page-info', + 'prev-page', + 'next-page' + ); return; } - const start = (currentPage - 1) * itemsPerPage; - const end = start + itemsPerPage; - const pageItems = filteredChapters.slice(start, end); - - pageItems.forEach((ch, idx) => { - const realIndex = start + idx; + const pageItems = chapterPagination.getCurrentPageItems(filteredChapters); + pageItems.forEach((ch) => { const row = document.createElement('tr'); - row.innerHTML = ` ${ch.number} ${ch.title || 'Chapter ' + ch.number}
${ch.provider} - @@ -552,66 +293,49 @@ function renderTable(idForFetch) { tbody.appendChild(row); }); - updatePagination(); + chapterPagination.renderControls( + 'pagination', + 'page-info', + 'prev-page', + 'next-page' + ); } -function updatePagination() { - const totalPages = Math.ceil(filteredChapters.length / itemsPerPage); - const pagination = document.getElementById('pagination'); - - if (!pagination) return; - - if (totalPages <= 1) { - pagination.style.display = 'none'; - return; - } - - pagination.style.display = 'flex'; - document.getElementById('page-info').innerText = `Page ${currentPage} of ${totalPages}`; - - const prevBtn = document.getElementById('prev-page'); - const nextBtn = document.getElementById('next-page'); - - prevBtn.disabled = currentPage === 1; - nextBtn.disabled = currentPage >= totalPages; - - const idForFetch = extensionName ? bookSlug : bookId; - - prevBtn.onclick = () => { currentPage--; renderTable(idForFetch); }; - nextBtn.onclick = () => { currentPage++; renderTable(idForFetch); }; +function openReader(chapterId, provider) { + window.location.href = URLUtils.buildReadUrl(bookId, chapterId, provider, extensionName); } -function openReader(bookId, chapterId, provider) { - const c = encodeURIComponent(chapterId); - const p = encodeURIComponent(provider); - let extension = "?source=anilist"; - if (extensionName) extension = "?source=" + extensionName; - window.location.href = `/read/${p}/${c}/${bookId}${extension}`; -} - -document.addEventListener('DOMContentLoaded', () => { - +function setupModalClickOutside() { const modal = document.getElementById('add-list-modal'); - if (modal) { - modal.addEventListener('click', (e) => { - if (e.target.id === 'add-list-modal') { - closeAddToListModal(); - } - }); - } -}); + if (!modal) return; -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); + modal.addEventListener('click', (e) => { + if (e.target.id === 'add-list-modal') { + ListModalManager.close(); + } + }); +} -init(); \ No newline at end of file +function showError(message) { + const titleEl = document.getElementById('title'); + if (titleEl) titleEl.innerText = message; +} + +function saveToList() { + const idToSave = extensionName ? bookSlug : bookId; + ListModalManager.save(idToSave, extensionName || 'anilist'); +} + +function deleteFromList() { + const idToDelete = extensionName ? bookSlug : bookId; + ListModalManager.delete(idToDelete, extensionName || 'anilist'); +} + +function closeAddToListModal() { + ListModalManager.close(); +} + +window.openReader = openReader; +window.saveToList = saveToList; +window.deleteFromList = deleteFromList; +window.closeAddToListModal = closeAddToListModal; \ No newline at end of file diff --git a/src/scripts/books/books.js b/src/scripts/books/books.js index cfd2521..95de71a 100644 --- a/src/scripts/books/books.js +++ b/src/scripts/books/books.js @@ -1,15 +1,12 @@ let trendingBooks = []; let currentHeroIndex = 0; let heroInterval; -let availableExtensions = []; -let currentBookData = null; - -let isInList = false; -let currentListEntry = null; -let totalUnits = 0; - -const API_BASE = '/api'; +document.addEventListener('DOMContentLoaded', () => { + SearchManager.init('#search-input', '#search-results', 'book'); + ContinueWatchingManager.load('my-status-books', 'reading', 'MANGA'); + init(); +}); window.addEventListener('scroll', () => { const nav = document.getElementById('navbar'); @@ -17,124 +14,6 @@ window.addEventListener('scroll', () => { else nav.classList.remove('scrolled'); }); -const searchInput = document.getElementById('search-input'); -const searchResults = document.getElementById('search-results'); -let searchTimeout; - -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(() => { - fetchBookSearch(query); - }, 300); -}); - -document.addEventListener('click', (e) => { - if (!e.target.closest('.search-wrapper')) { - searchResults.classList.remove('active'); - searchInput.style.borderRadius = '99px'; - } -}); - -async function fetchBookSearch(query) { - try { - let apiUrl = `/api/search/books?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/books/${extensionName}?q=${encodeURIComponent(finalQuery)}`; - } - } - - const res = await fetch(apiUrl); - const data = await res.json(); - - const resultsWithExtension = data.results.map(book => { - if (extensionName) { - return { ...book, - isExtensionResult: true, - extensionName: extensionName - - }; - } - return book; - }); - - renderSearchResults(resultsWithExtension || []); - } catch (err) { - console.error("Search Error:", err); - renderSearchResults([]); - } -} - -function renderSearchResults(results) { - searchResults.innerHTML = ''; - - if (!results || results.length === 0) { - searchResults.innerHTML = '
No results found
'; - } else { - results.forEach(book => { - const title = book.title.english || book.title.romaji || "Unknown"; - const img = (book.coverImage && (book.coverImage.medium || book.coverImage.large)) || ''; - const rating = Number.isInteger(book.averageScore) ? `${book.averageScore}%` : book.averageScore || 'N/A'; - const year = book.seasonYear || (book.startDate ? book.startDate.year : '') || '????'; - const format = book.format || 'MANGA'; - - const item = document.createElement('a'); - item.className = 'search-item'; - let href; - if (book.isExtensionResult) { - - href = `/book/${book.extensionName}/${book.id}`; - } else { - href = `/book/${book.id}`; - } - 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) { @@ -145,14 +24,6 @@ function scrollCarousel(id, direction) { async function init() { try { - - const resExt = await fetch('/api/extensions/book'); - const dataExt = await resExt.json(); - if (dataExt.extensions) { - availableExtensions = dataExt.extensions; - console.log("Extensiones disponibles cargadas:", availableExtensions); - } - const res = await fetch('/api/books/trending'); const data = await res.json(); @@ -182,310 +53,9 @@ 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) { +async 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)) || ''; @@ -510,83 +80,14 @@ function updateHeroUI(book) { const addToListBtn = document.querySelector('.hero-buttons .btn-blur'); if(addToListBtn) { + ListModalManager.currentData = book; + const entryType = ListModalManager.getEntryType(book); - addToListBtn.onclick = openAddToListModal; + await ListModalManager.checkIfInList(book.id, 'anilist', entryType); + ListModalManager.updateButton(); - checkIfInList(book.id).then(() => { - updateAddToListButton(); - }); + addToListBtn.onclick = () => ListModalManager.open(book, 'anilist'); } - -} - -async function loadContinueReading() { - const token = localStorage.getItem('token'); - if (!token) return; - - try { - const res = await fetch('/api/list/filter?status=reading&entry_type=MANGA', { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }); - - if (!res.ok) return; - - const data = await res.json(); - const list = data.results || []; - - renderContinueReading('my-status-books', list); - - } catch (err) { - console.error('Continue Reading Error:', err); - } -} - -function renderContinueReading(id, list) { - const container = document.getElementById(id); - if (!container) return; - - container.innerHTML = ''; - - if (list.length === 0) { - container.innerHTML = `
No reading manga
`; - 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 ch = item.progress || 1; - - if (item.source === 'anilist') { - window.location.href = `http://localhost:54322/book/${item.entry_id}?chapter=${ch + 1}`; - } else { - window.location.href = `http://localhost:54322/read/${item.source}/${ch + 1}/${item.entry_id}?source=${item.source}`; - } - }; - - const progressText = item.total_episodes - ? `${item.progress || 0}/${item.total_episodes}` - : `${item.progress || 0}`; - - el.innerHTML = ` -
- -
-
-

${item.title}

-

Ch ${progressText} - ${item.source}

-
- `; - - container.appendChild(el); - }); } function renderList(id, list) { @@ -616,5 +117,18 @@ function renderList(id, list) { }); } -init(); -loadContinueReading(); \ No newline at end of file +function saveToList() { + const bookId = ListModalManager.currentData ? ListModalManager.currentData.id : null; + if (!bookId) return; + ListModalManager.save(bookId, 'anilist'); +} + +function deleteFromList() { + const bookId = ListModalManager.currentData ? ListModalManager.currentData.id : null; + if (!bookId) return; + ListModalManager.delete(bookId, 'anilist'); +} + +function closeAddToListModal() { + ListModalManager.close(); +} \ No newline at end of file diff --git a/src/scripts/list.js b/src/scripts/list.js index bbad5a1..8e41c44 100644 --- a/src/scripts/list.js +++ b/src/scripts/list.js @@ -1,26 +1,6 @@ const API_BASE = '/api'; let currentList = []; let filteredList = []; -let currentEditingEntry = null; - -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}` - }; -} document.addEventListener('DOMContentLoaded', async () => { await loadList(); @@ -33,10 +13,8 @@ function getEntryLink(item) { const source = item.source || 'anilist'; if (source === 'anilist') { - return `${baseRoute}/${item.entry_id}`; } else { - return `${baseRoute}/${source}/${item.entry_id}`; } } @@ -70,6 +48,34 @@ async function populateSourceFilter() { } } +function updateLocalList(entryData, action) { + const entryId = entryData.entry_id; + const source = entryData.source; + + const findIndex = (list) => list.findIndex(e => + e.entry_id === entryId && e.source === source + ); + + const currentIndex = findIndex(currentList); + if (currentIndex !== -1) { + if (action === 'update') { + + currentList[currentIndex] = { ...currentList[currentIndex], ...entryData }; + } else if (action === 'delete') { + currentList.splice(currentIndex, 1); + } + } else if (action === 'update') { + + currentList.push(entryData); + } + + filteredList = [...currentList]; + + updateStats(); + applyFilters(); + window.ListModalManager.close(); +} + function setupEventListeners() { document.querySelectorAll('.view-btn').forEach(btn => { @@ -102,6 +108,43 @@ function setupEventListeners() { } applyFilters(); }); + + document.getElementById('modal-save-btn')?.addEventListener('click', async () => { + + const entryToSave = window.ListModalManager.currentEntry || window.ListModalManager.currentData; + + if (!entryToSave) return; + + const success = await window.ListModalManager.save(entryToSave.entry_id, entryToSave.source); + + if (success) { + + const updatedEntry = window.ListModalManager.currentEntry; + updatedEntry.updated_at = new Date().toISOString(); + + updateLocalList(updatedEntry, 'update'); + } + + }); + + document.getElementById('modal-delete-btn')?.addEventListener('click', async () => { + const entryToDelete = window.ListModalManager.currentEntry || window.ListModalManager.currentData; + + if (!entryToDelete) return; + + const success = await window.ListModalManager.delete(entryToDelete.entry_id, entryToDelete.source); + + if (success) { + updateLocalList(entryToDelete, 'delete'); + } + + }); + + document.getElementById('add-list-modal')?.addEventListener('click', (e) => { + if (e.target.id === 'add-list-modal') { + window.ListModalManager.close(); + } + }); } async function loadList() { @@ -117,7 +160,7 @@ async function loadList() { container.innerHTML = ''; const response = await fetch(`${API_BASE}/list`, { - headers: getSimpleAuthHeaders() + headers: window.AuthUtils.getSimpleAuthHeaders() }); if (!response.ok) { @@ -139,11 +182,16 @@ async function loadList() { } catch (error) { console.error('Error loading list:', error); loadingState.style.display = 'none'; - alert('Failed to load your list. Please try again.'); + if (window.NotificationUtils) { + window.NotificationUtils.error('Failed to load your list. Please try again.'); + } else { + alert('Failed to load your list. Please try again.'); + } } } function updateStats() { + const total = currentList.length; const watching = currentList.filter(item => item.status === 'WATCHING').length; const completed = currentList.filter(item => item.status === 'COMPLETED').length; @@ -187,6 +235,7 @@ function applyFilters() { break; case 'updated': default: + filtered.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); break; } @@ -199,10 +248,18 @@ function renderList(items) { container.innerHTML = ''; if (items.length === 0) { - container.innerHTML = '

No entries match your filters

'; + + if (currentList.length === 0) { + document.getElementById('empty-state').style.display = 'flex'; + } else { + + container.innerHTML = '

No entries match your filters

'; + } return; } + document.getElementById('empty-state').style.display = 'none'; + items.forEach(item => { const element = createListItem(item); container.appendChild(element); @@ -237,6 +294,8 @@ function createListItem(item) { } const statusLabels = { + + 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', 'WATCHING': entryType === 'ANIME' ? 'Watching' : 'Reading', 'COMPLETED': 'Completed', 'PLANNING': 'Planning', @@ -252,6 +311,7 @@ function createListItem(item) { extraInfo.push('🔒 Private'); } + const entryDataString = JSON.stringify(item).replace(/'/g, '''); div.innerHTML = ` @@ -280,250 +340,30 @@ function createListItem(item) {
- `; - return div; -} + const editBtn = div.querySelector('.edit-icon-btn'); + editBtn.addEventListener('click', (e) => { + try { + const entryData = JSON.parse(e.currentTarget.dataset.entry); -function openEditModal(item) { - currentEditingEntry = item; + window.ListModalManager.isInList = true; + window.ListModalManager.currentEntry = entryData; + window.ListModalManager.currentData = entryData; - // Campos existentes - let modalStatus = item.status; - const type = item.entry_type?.toUpperCase(); - - if ((type === 'MANGA' || type === 'NOVEL') && item.status === 'READING') { - modalStatus = 'WATCHING'; // solo para mostrar correctamente - } - - document.getElementById('edit-status').value = modalStatus; - document.getElementById('edit-progress').value = item.progress || 0; - // Asegura que el score se muestre si existe. - document.getElementById('edit-score').value = item.score !== null && item.score !== undefined ? item.score : ''; - - // Nuevos campos - // Usamos split('T')[0] para asegurar que solo se muestra la parte de la fecha (YYYY-MM-DD) si viene con formato DATETIME. - document.getElementById('edit-start-date').value = item.start_date?.split('T')[0] || ''; - document.getElementById('edit-end-date').value = item.end_date?.split('T')[0] || ''; - document.getElementById('edit-repeat-count').value = item.repeat_count || 0; - document.getElementById('edit-notes').value = item.notes || ''; - // Maneja el booleano o el entero (1/0) - document.getElementById('edit-is-private').checked = item.is_private === 1 || item.is_private === true; - - - const entryType = (item.entry_type).toUpperCase(); - const progressLabel = document.querySelector('label[for="edit-progress"]'); - if (progressLabel) { - if (entryType === 'MANGA') { - progressLabel.textContent = 'Chapters Read'; - } else if (entryType === 'NOVEL') { - progressLabel.textContent = 'Chapters/Volumes Read'; - } else { - progressLabel.textContent = 'Episodes Watched'; - } - } - - const totalUnits = item.entry_type === 'ANIME' ? - item.total_episodes || 999 : - item.total_chapters || 999; - document.getElementById('edit-progress').max = totalUnits; - - document.getElementById('edit-modal').classList.add('active'); - const statusSelect = document.getElementById('edit-status'); - const type2 = item.entry_type?.toUpperCase(); - - [...statusSelect.options].forEach(opt => { - if (opt.value === 'WATCHING') { - opt.textContent = (type2 === 'MANGA' || type2 === 'NOVEL') - ? 'Reading' - : 'Watching'; + window.ListModalManager.open(entryData, entryData.source); + } catch (error) { + console.error('Error parsing entry data for modal:', error); + if (window.NotificationUtils) { + window.NotificationUtils.error('Could not open modal. Check HTML form IDs.'); + } } }); -} - -function closeEditModal() { - currentEditingEntry = null; - - document.getElementById('edit-modal').classList.remove('active'); -} - -async function saveEntry() { - if (!currentEditingEntry) return; - - let status = document.getElementById('edit-status').value; - - const anilistStatusMap = { - WATCHING: 'CURRENT', - COMPLETED: 'COMPLETED', - PLANNING: 'PLANNING', - PAUSED: 'PAUSED', - DROPPED: 'DROPPED' - }; - - const anilistStatus = anilistStatusMap[status]; - - const progress = parseInt(document.getElementById('edit-progress').value) || 0; - const scoreValue = document.getElementById('edit-score').value; - const score = scoreValue ? parseFloat(scoreValue) : null; - - const start_date = document.getElementById('edit-start-date').value || null; - const end_date = document.getElementById('edit-end-date').value || null; - const repeat_count = parseInt(document.getElementById('edit-repeat-count').value) || 0; - const notesValue = document.getElementById('edit-notes').value; - const notes = notesValue ? notesValue : null; - const is_private = document.getElementById('edit-is-private').checked; - - try { - const response = await fetch(`${API_BASE}/list/entry`, { - method: 'POST', - headers: getAuthHeaders(), - body: JSON.stringify({ - entry_id: currentEditingEntry.entry_id, - source: currentEditingEntry.source, - entry_type: currentEditingEntry.entry_type, - status: anilistStatus, - progress, - score, - start_date, - end_date, - repeat_count, - notes, - is_private - }) - }); - - if (!response.ok) throw new Error('Failed to update entry'); - - // ✅ ACTUALIZAR EN MEMORIA - const index = currentList.findIndex(e => - e.entry_id === currentEditingEntry.entry_id && - e.source === currentEditingEntry.source - ); - - if (index !== -1) { - currentList[index] = { - ...currentList[index], - status, - progress, - score, - start_date, - end_date, - repeat_count, - notes, - is_private, - updated_at: new Date().toISOString() - }; - } - - filteredList = [...currentList]; - updateStats(); - applyFilters(); - - closeEditModal(); - showNotification('Entry updated successfully!', 'success'); - - } catch (error) { - console.error('Error updating entry:', error); - showNotification('Failed to update entry', 'error'); - } -} - - -async function deleteEntry() { - if (!currentEditingEntry) return; - - if (!confirm('Are you sure you want to remove this entry from your list?')) { - return; - } - - try { - const response = await fetch( - `${API_BASE}/list/entry/${currentEditingEntry.entry_id}?source=${currentEditingEntry.source}`, - { - method: 'DELETE', - headers: getSimpleAuthHeaders() - } - ); - - if (!response.ok) throw new Error('Failed to delete entry'); - - // ✅ ELIMINAR EN MEMORIA - currentList = currentList.filter(item => - !(item.entry_id === currentEditingEntry.entry_id && - item.source === currentEditingEntry.source) - ); - - filteredList = [...currentList]; - updateStats(); - applyFilters(); - - closeEditModal(); - showNotification('Entry removed from list', 'success'); - - } catch (error) { - console.error('Error deleting entry:', error); - showNotification('Failed to remove entry', '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); -} - -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); - -document.getElementById('edit-modal').addEventListener('click', (e) => { - if (e.target.id === 'edit-modal') { - closeEditModal(); - } -}); \ No newline at end of file + return div; +} \ No newline at end of file diff --git a/src/scripts/users.js b/src/scripts/users.js index 82d93f5..be21eca 100644 --- a/src/scripts/users.js +++ b/src/scripts/users.js @@ -502,7 +502,7 @@ function showConfirmationModal(title, message, confirmAction) { -

+

${message}

@@ -550,7 +550,7 @@ function openAniListModal(userId) { ` : `

Connect with AniList

-

+

Sync your anime list by logging in with AniList.

@@ -560,7 +560,7 @@ function openAniListModal(userId) {
-

You will be redirected and then returned here.

+

You will be redirected and then returned here.

`} @@ -570,7 +570,7 @@ function openAniListModal(userId) { }).catch(err => { console.error(err); - aniListContent.innerHTML = `
Error loading integration status.
`; + aniListContent.innerHTML = `
Error loading integration status.
`; modalAniList.classList.add('active'); }); } diff --git a/src/scripts/utils/auth-utils.js b/src/scripts/utils/auth-utils.js new file mode 100644 index 0000000..e6d0623 --- /dev/null +++ b/src/scripts/utils/auth-utils.js @@ -0,0 +1,26 @@ +const AuthUtils = { + getToken() { + return localStorage.getItem('token'); + }, + + getAuthHeaders() { + const token = this.getToken(); + return { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }; + }, + + getSimpleAuthHeaders() { + const token = this.getToken(); + return { + 'Authorization': `Bearer ${token}` + }; + }, + + isAuthenticated() { + return !!this.getToken(); + } +}; + +window.AuthUtils = AuthUtils; \ No newline at end of file diff --git a/src/scripts/utils/continue-watching-manager.js b/src/scripts/utils/continue-watching-manager.js new file mode 100644 index 0000000..dbdea0c --- /dev/null +++ b/src/scripts/utils/continue-watching-manager.js @@ -0,0 +1,86 @@ +const ContinueWatchingManager = { + API_BASE: '/api', + + async load(containerId, status = 'watching', entryType = 'ANIME') { + if (!AuthUtils.isAuthenticated()) return; + + const container = document.getElementById(containerId); + if (!container) return; + + try { + const res = await fetch(`${this.API_BASE}/list/filter?status=${status}&entry_type=${entryType}`, { + headers: AuthUtils.getAuthHeaders() + }); + + if (!res.ok) return; + + const data = await res.json(); + const list = data.results || []; + + this.render(containerId, list, entryType); + } catch (err) { + console.error(`Continue ${entryType === 'ANIME' ? 'Watching' : 'Reading'} Error:`, err); + } + }, + + render(containerId, list, entryType = 'ANIME') { + const container = document.getElementById(containerId); + if (!container) return; + + container.innerHTML = ''; + + if (list.length === 0) { + const label = entryType === 'ANIME' ? 'watching anime' : 'reading manga'; + container.innerHTML = `
No ${label}
`; + return; + } + + list.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + + list.forEach(item => { + const card = this.createCard(item, entryType); + container.appendChild(card); + }); + }, + + createCard(item, entryType) { + const el = document.createElement('div'); + el.className = 'card'; + + const nextProgress = (item.progress || 0) + 1; + let url; + + if (entryType === 'ANIME') { + url = item.source === 'anilist' + ? `/watch/${item.entry_id}/${nextProgress}` + : `/watch/${item.entry_id}/${nextProgress}?${item.source}`; + } else { + + url = item.source === 'anilist' + ? `/book/${item.entry_id}?chapter=${nextProgress}` + : `/read/${item.source}/${nextProgress}/${item.entry_id}?source=${item.source}`; + } + + el.onclick = () => window.location.href = url; + + const progressText = item.total_episodes || item.total_chapters + ? `${item.progress || 0}/${item.total_episodes || item.total_chapters}` + : `${item.progress || 0}`; + + const unitLabel = entryType === 'ANIME' ? 'Ep' : 'Ch'; + + el.innerHTML = ` +
+ ${item.title} +
+
+

${item.title}

+

${unitLabel} ${progressText} - ${item.source}

+
+ `; + + return el; + } +}; + +window.ContinueWatchingManager = ContinueWatchingManager; \ No newline at end of file diff --git a/src/scripts/utils/list-modal-manager.js b/src/scripts/utils/list-modal-manager.js new file mode 100644 index 0000000..6742115 --- /dev/null +++ b/src/scripts/utils/list-modal-manager.js @@ -0,0 +1,234 @@ +const ListModalManager = { + API_BASE: '/api', + currentData: null, + isInList: false, + currentEntry: null, + + STATUS_MAP: { + WATCHING: 'CURRENT', + COMPLETED: 'COMPLETED', + PLANNING: 'PLANNING', + PAUSED: 'PAUSED', + DROPPED: 'DROPPED', + REPEATING: 'REPEATING' + }, + + getEntryType(data) { + if (!data) return 'ANIME'; + + if (data.format) { + const format = data.format.toUpperCase(); + if (['MANGA', 'ONE_SHOT', 'MANHWA'].includes(format)) { + return 'MANGA'; + } + if (['NOVEL', 'LIGHT_NOVEL'].includes(format)) { + return 'NOVEL'; + } + } + + return 'ANIME'; + }, + + async checkIfInList(entryId, source = 'anilist', entryType) { + if (!AuthUtils.isAuthenticated()) return false; + + const url = `${this.API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`; + + try { + const response = await fetch(url, { + headers: AuthUtils.getSimpleAuthHeaders() + }); + + if (response.ok) { + const data = await response.json(); + this.isInList = data.found && !!data.entry; + this.currentEntry = data.entry || null; + } else { + this.isInList = false; + this.currentEntry = null; + } + + return this.isInList; + } catch (error) { + console.error('Error checking list entry:', error); + return false; + } + }, + + updateButton(buttonSelector = '.hero-buttons .btn-blur') { + const btn = document.querySelector(buttonSelector); + if (!btn) return; + + if (this.isInList) { + btn.innerHTML = ` + + + + In Your ${this.currentData?.format ? 'Library' : '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 ${this.currentData?.format ? 'Library' : 'List'}`; + btn.style.background = null; + btn.style.color = null; + btn.style.borderColor = null; + } + }, + + open(data, source = 'anilist') { + if (!AuthUtils.isAuthenticated()) { + NotificationUtils.error('Please log in to manage your list.'); + return; + } + + this.currentData = data; + const entryType = this.getEntryType(data); + const totalUnits = data.episodes || data.chapters || data.volumes || 999; + + const modalTitle = document.getElementById('modal-title'); + const deleteBtn = document.getElementById('modal-delete-btn'); + const progressLabel = document.querySelector('label[for="entry-progress"]') || + document.getElementById('progress-label'); + + if (this.isInList && this.currentEntry) { + document.getElementById('entry-status').value = this.currentEntry.status || 'PLANNING'; + document.getElementById('entry-progress').value = this.currentEntry.progress || 0; + document.getElementById('entry-score').value = this.currentEntry.score || ''; + document.getElementById('entry-start-date').value = this.currentEntry.start_date?.split('T')[0] || ''; + document.getElementById('entry-end-date').value = this.currentEntry.end_date?.split('T')[0] || ''; + document.getElementById('entry-repeat-count').value = this.currentEntry.repeat_count || 0; + document.getElementById('entry-notes').value = this.currentEntry.notes || ''; + document.getElementById('entry-is-private').checked = this.currentEntry.is_private === true || this.currentEntry.is_private === 1; + + modalTitle.textContent = `Edit ${entryType === 'ANIME' ? 'List' : '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 ${entryType === 'ANIME' ? 'List' : 'Library'}`; + deleteBtn.style.display = 'none'; + } + + if (progressLabel) { + if (entryType === 'ANIME') { + progressLabel.textContent = 'Episodes Watched'; + } else if (entryType === 'MANGA') { + progressLabel.textContent = 'Chapters Read'; + } else { + progressLabel.textContent = 'Volumes/Parts Read'; + } + } + + const statusSelect = document.getElementById('entry-status'); + [...statusSelect.options].forEach(opt => { + if (opt.value === 'WATCHING') { + opt.textContent = entryType === 'ANIME' ? 'Watching' : 'Reading'; + } + }); + + document.getElementById('entry-progress').max = totalUnits; + document.getElementById('add-list-modal').classList.add('active'); + }, + + close() { + document.getElementById('add-list-modal').classList.remove('active'); + }, + + async save(entryId, source = 'anilist') { + const uiStatus = document.getElementById('entry-status').value; + const status = this.STATUS_MAP[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; + + const entryType = this.getEntryType(this.currentData); + + try { + const response = await fetch(`${this.API_BASE}/list/entry`, { + method: 'POST', + headers: AuthUtils.getAuthHeaders(), + body: JSON.stringify({ + entry_id: entryId, + source, + entry_type: entryType, + status, + progress, + score, + start_date, + end_date, + repeat_count, + notes, + is_private + }) + }); + + if (!response.ok) throw new Error('Failed to save entry'); + + const data = await response.json(); + this.isInList = true; + this.currentEntry = data.entry; + this.updateButton(); + this.close(); + NotificationUtils.success(this.isInList ? 'Updated successfully!' : 'Added to your list!'); + } catch (error) { + console.error('Error saving to list:', error); + NotificationUtils.error('Failed to save. Please try again.'); + } + }, + + async delete(entryId, source = 'anilist') { + if (!confirm(`Remove this ${this.getEntryType(this.currentData).toLowerCase()} from your list?`)) { + return; + } + + const entryType = this.getEntryType(this.currentData); + + try { + const response = await fetch( + `${this.API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`, + { + method: 'DELETE', + headers: AuthUtils.getSimpleAuthHeaders() + } + ); + + if (!response.ok) throw new Error('Failed to delete entry'); + + this.isInList = false; + this.currentEntry = null; + this.updateButton(); + this.close(); + NotificationUtils.success('Removed from your list'); + } catch (error) { + console.error('Error deleting from list:', error); + NotificationUtils.error('Failed to remove. Please try again.'); + } + } +}; + +document.addEventListener('DOMContentLoaded', () => { + const modal = document.getElementById('add-list-modal'); + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target.id === 'add-list-modal') { + ListModalManager.close(); + } + }); + } +}); + +window.ListModalManager = ListModalManager; \ No newline at end of file diff --git a/src/scripts/utils/media-metadata-utils.js b/src/scripts/utils/media-metadata-utils.js new file mode 100644 index 0000000..dc00179 --- /dev/null +++ b/src/scripts/utils/media-metadata-utils.js @@ -0,0 +1,192 @@ +const MediaMetadataUtils = { + + getTitle(data) { + if (!data) return "Unknown Title"; + + if (data.title) { + if (typeof data.title === 'string') return data.title; + return data.title.english || data.title.romaji || data.title.native || "Unknown Title"; + } + + return data.name || "Unknown Title"; + }, + + getDescription(data) { + const rawDesc = data.description || data.summary || "No description available."; + + const tmp = document.createElement("DIV"); + tmp.innerHTML = rawDesc; + return tmp.textContent || tmp.innerText || rawDesc; + }, + + getPosterUrl(data, isExtension = false) { + if (isExtension) { + return data.image || ''; + } + + if (data.coverImage) { + return data.coverImage.extraLarge || data.coverImage.large || data.coverImage.medium || ''; + } + + return data.image || ''; + }, + + getBannerUrl(data, isExtension = false) { + if (isExtension) { + return data.image || ''; + } + + return data.bannerImage || this.getPosterUrl(data, isExtension); + }, + + getScore(data, isExtension = false) { + if (isExtension) { + return data.score ? Math.round(data.score * 10) : '?'; + } + + return data.averageScore || '?'; + }, + + getYear(data, isExtension = false) { + if (isExtension) { + return data.year || data.published || '????'; + } + + if (data.seasonYear) return data.seasonYear; + if (data.startDate?.year) return data.startDate.year; + + return '????'; + }, + + getGenres(data, maxGenres = 3) { + if (!data.genres || !Array.isArray(data.genres)) return ''; + return data.genres.slice(0, maxGenres).join(' • '); + }, + + getSeason(data, isExtension = false) { + if (isExtension) { + return data.season || 'Unknown'; + } + + if (data.season && data.seasonYear) { + return `${data.season} ${data.seasonYear}`; + } + + if (data.startDate?.year && data.startDate?.month) { + const months = ['', 'Winter', 'Winter', 'Spring', 'Spring', 'Spring', + 'Summer', 'Summer', 'Summer', 'Fall', 'Fall', 'Fall', 'Winter']; + const season = months[data.startDate.month] || ''; + return season ? `${season} ${data.startDate.year}` : `${data.startDate.year}`; + } + + return 'Unknown'; + }, + + getStudio(data, isExtension = false) { + if (isExtension) { + return data.studio || "Unknown"; + } + + if (data.studios?.nodes?.[0]?.name) { + return data.studios.nodes[0].name; + } + + if (data.studios?.edges?.[0]?.node?.name) { + return data.studios.edges[0].node.name; + } + + return 'Unknown Studio'; + }, + + getCharacters(data, isExtension = false, maxChars = 5) { + let characters = []; + + if (isExtension) { + characters = data.characters || []; + } else { + if (data.characters?.nodes?.length > 0) { + characters = data.characters.nodes; + } else if (data.characters?.edges?.length > 0) { + characters = data.characters.edges + .filter(edge => edge?.node?.name?.full) + .map(edge => edge.node); + } + } + + return characters.slice(0, maxChars).map(char => ({ + name: char?.name?.full || char?.name || "Unknown", + image: char?.image?.large || char?.image?.medium || null + })); + }, + + getTotalEpisodes(data, isExtension = false) { + if (isExtension) { + return data.episodes || 1; + } + + if (data.nextAiringEpisode?.episode) { + return data.nextAiringEpisode.episode - 1; + } + + return data.episodes || 12; + }, + + truncateDescription(text, maxSentences = 4) { + const tmp = document.createElement("DIV"); + tmp.innerHTML = text; + const cleanText = tmp.textContent || tmp.innerText || ""; + + const sentences = cleanText.match(/[^\.!\?]+[\.!\?]+/g) || [cleanText]; + + if (sentences.length > maxSentences) { + return { + short: sentences.slice(0, maxSentences).join(' ') + '...', + full: text, + isTruncated: true + }; + } + + return { + short: text, + full: text, + isTruncated: false + }; + }, + + formatBookData(data, isExtension = false) { + return { + title: this.getTitle(data), + description: this.getDescription(data), + score: this.getScore(data, isExtension), + year: this.getYear(data, isExtension), + status: data.status || 'Unknown', + format: data.format || (isExtension ? 'LN' : 'MANGA'), + chapters: data.chapters || '?', + volumes: data.volumes || '?', + poster: this.getPosterUrl(data, isExtension), + banner: this.getBannerUrl(data, isExtension), + genres: this.getGenres(data) + }; + }, + + formatAnimeData(data, isExtension = false) { + return { + title: this.getTitle(data), + description: this.getDescription(data), + score: this.getScore(data, isExtension), + year: this.getYear(data, isExtension), + season: this.getSeason(data, isExtension), + status: data.status || 'Unknown', + format: data.format || 'TV', + episodes: this.getTotalEpisodes(data, isExtension), + poster: this.getPosterUrl(data, isExtension), + banner: this.getBannerUrl(data, isExtension), + genres: this.getGenres(data), + studio: this.getStudio(data, isExtension), + characters: this.getCharacters(data, isExtension), + trailer: data.trailer || null + }; + } +}; + +window.MediaMetadataUtils = MediaMetadataUtils; \ No newline at end of file diff --git a/src/scripts/utils/notification-utils.js b/src/scripts/utils/notification-utils.js new file mode 100644 index 0000000..fef13d1 --- /dev/null +++ b/src/scripts/utils/notification-utils.js @@ -0,0 +1,52 @@ +const NotificationUtils = { + show(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); + }, + + success(message) { + this.show(message, 'success'); + }, + + error(message) { + this.show(message, 'error'); + }, + + info(message) { + this.show(message, 'info'); + } +}; + +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); + +window.NotificationUtils = NotificationUtils; \ No newline at end of file diff --git a/src/scripts/utils/pagination-manager.js b/src/scripts/utils/pagination-manager.js new file mode 100644 index 0000000..d16483d --- /dev/null +++ b/src/scripts/utils/pagination-manager.js @@ -0,0 +1,91 @@ +const PaginationManager = { + currentPage: 1, + itemsPerPage: 12, + totalItems: 0, + onPageChange: null, + + init(itemsPerPage = 12, onPageChange = null) { + this.itemsPerPage = itemsPerPage; + this.onPageChange = onPageChange; + this.currentPage = 1; + }, + + setTotalItems(total) { + this.totalItems = total; + }, + + getTotalPages() { + return Math.ceil(this.totalItems / this.itemsPerPage); + }, + + getCurrentPageItems(items) { + const start = (this.currentPage - 1) * this.itemsPerPage; + const end = start + this.itemsPerPage; + return items.slice(start, end); + }, + + getPageRange() { + const start = (this.currentPage - 1) * this.itemsPerPage; + const end = Math.min(start + this.itemsPerPage, this.totalItems); + return { start, end }; + }, + + nextPage() { + if (this.currentPage < this.getTotalPages()) { + this.currentPage++; + if (this.onPageChange) this.onPageChange(); + return true; + } + return false; + }, + + prevPage() { + if (this.currentPage > 1) { + this.currentPage--; + if (this.onPageChange) this.onPageChange(); + return true; + } + return false; + }, + + goToPage(page) { + const totalPages = this.getTotalPages(); + if (page >= 1 && page <= totalPages) { + this.currentPage = page; + if (this.onPageChange) this.onPageChange(); + return true; + } + return false; + }, + + reset() { + this.currentPage = 1; + }, + + renderControls(containerId, pageInfoId, prevBtnId, nextBtnId) { + const container = document.getElementById(containerId); + const pageInfo = document.getElementById(pageInfoId); + const prevBtn = document.getElementById(prevBtnId); + const nextBtn = document.getElementById(nextBtnId); + + if (!container || !pageInfo || !prevBtn || !nextBtn) return; + + const totalPages = this.getTotalPages(); + + if (totalPages <= 1) { + container.style.display = 'none'; + return; + } + + container.style.display = 'flex'; + pageInfo.innerText = `Page ${this.currentPage} of ${totalPages}`; + + prevBtn.disabled = this.currentPage === 1; + nextBtn.disabled = this.currentPage >= totalPages; + + prevBtn.onclick = () => this.prevPage(); + nextBtn.onclick = () => this.nextPage(); + } +}; + +window.PaginationManager = PaginationManager; \ No newline at end of file diff --git a/src/scripts/utils/search-manager.js b/src/scripts/utils/search-manager.js new file mode 100644 index 0000000..5414754 --- /dev/null +++ b/src/scripts/utils/search-manager.js @@ -0,0 +1,176 @@ +const SearchManager = { + availableExtensions: [], + searchTimeout: null, + + init(inputSelector, resultsSelector, type = 'anime') { + const searchInput = document.querySelector(inputSelector); + const searchResults = document.querySelector(resultsSelector); + + if (!searchInput || !searchResults) { + console.error('Search elements not found'); + return; + } + + this.loadExtensions(type); + + searchInput.addEventListener('input', (e) => { + const query = e.target.value; + clearTimeout(this.searchTimeout); + + if (query.length < 2) { + searchResults.classList.remove('active'); + searchResults.innerHTML = ''; + searchInput.style.borderRadius = '99px'; + return; + } + + this.searchTimeout = setTimeout(() => { + this.search(query, type, searchResults); + }, 300); + }); + + document.addEventListener('click', (e) => { + if (!e.target.closest('.search-wrapper')) { + searchResults.classList.remove('active'); + searchInput.style.borderRadius = '99px'; + } + }); + }, + + async loadExtensions(type) { + try { + const endpoint = type === 'book' ? '/api/extensions/book' : '/api/extensions/anime'; + const res = await fetch(endpoint); + const data = await res.json(); + this.availableExtensions = data.extensions || []; + console.log(`${type} extensions loaded:`, this.availableExtensions); + } catch (err) { + console.error('Error loading extensions:', err); + } + }, + + async search(query, type, resultsContainer) { + try { + let apiUrl, extensionName = null, finalQuery = query; + + const parts = query.split(':'); + if (parts.length >= 2) { + const potentialExtension = parts[0].trim().toLowerCase(); + const foundExtension = this.availableExtensions.find( + ext => ext.toLowerCase() === potentialExtension + ); + + if (foundExtension) { + extensionName = foundExtension; + finalQuery = parts.slice(1).join(':').trim(); + + if (finalQuery.length === 0) { + this.renderResults([], resultsContainer, type); + return; + } + } + } + + if (extensionName) { + const endpoint = type === 'book' ? 'books' : ''; + apiUrl = `/api/search/${endpoint ? endpoint + '/' : ''}${extensionName}?q=${encodeURIComponent(finalQuery)}`; + } else { + const endpoint = type === 'book' ? '/api/search/books' : '/api/search'; + apiUrl = `${endpoint}?q=${encodeURIComponent(query)}`; + } + + const res = await fetch(apiUrl); + const data = await res.json(); + + const results = (data.results || []).map(item => ({ + ...item, + isExtensionResult: !!extensionName, + extensionName + })); + + this.renderResults(results, resultsContainer, type); + } catch (err) { + console.error("Search Error:", err); + this.renderResults([], resultsContainer, type); + } + }, + + renderResults(results, container, type) { + container.innerHTML = ''; + + if (!results || results.length === 0) { + container.innerHTML = '
No results found
'; + } else { + results.forEach(item => { + const resultElement = this.createResultElement(item, type); + container.appendChild(resultElement); + }); + } + + container.classList.add('active'); + const searchInput = container.previousElementSibling || document.querySelector('.search-input'); + if (searchInput) { + searchInput.style.borderRadius = '12px 12px 0 0'; + } + }, + + createResultElement(item, type) { + const element = document.createElement('a'); + element.className = 'search-item'; + + if (type === 'book') { + const title = item.title?.english || item.title?.romaji || "Unknown"; + const img = item.coverImage?.medium || item.coverImage?.large || ''; + const rating = Number.isInteger(item.averageScore) ? `${item.averageScore}%` : item.averageScore || 'N/A'; + const year = item.seasonYear || item.startDate?.year || '????'; + const format = item.format || 'MANGA'; + + element.href = item.isExtensionResult + ? `/book/${item.extensionName}/${item.id}` + : `/book/${item.id}`; + + element.innerHTML = ` + ${title} +
+
${title}
+
+ ${rating} + • ${year} + • ${format} +
+
+ `; + } else { + + const title = item.title?.english || item.title?.romaji || "Unknown Title"; + const img = item.coverImage?.medium || item.coverImage?.large || ''; + const rating = item.averageScore ? `${item.averageScore}%` : 'N/A'; + const year = item.seasonYear || ''; + const format = item.format || 'TV'; + + element.href = item.isExtensionResult + ? `/anime/${item.extensionName}/${item.id}` + : `/anime/${item.id}`; + + element.innerHTML = ` + ${title} +
+
${title}
+
+ ${rating} + • ${year} + • ${format} +
+
+ `; + } + + return element; + }, + + getTitle(item) { + return item.title?.english || item.title?.romaji || "Unknown Title"; + } +}; + +window.SearchManager = SearchManager; \ No newline at end of file diff --git a/src/scripts/utils/url-utils.js b/src/scripts/utils/url-utils.js new file mode 100644 index 0000000..57479a8 --- /dev/null +++ b/src/scripts/utils/url-utils.js @@ -0,0 +1,51 @@ +const URLUtils = { + + parseEntityPath(basePath = 'anime') { + const path = window.location.pathname; + const parts = path.split("/").filter(Boolean); + + if (parts[0] !== basePath) { + return null; + } + + if (parts.length === 3) { + + return { + extensionName: parts[1], + entityId: parts[2], + slug: parts[2] + }; + } else if (parts.length === 2) { + + return { + extensionName: null, + entityId: parts[1], + slug: null + }; + } + + return null; + }, + + buildWatchUrl(animeId, episode, extensionName = null) { + const base = `/watch/${animeId}/${episode}`; + return extensionName ? `${base}?${extensionName}` : base; + }, + + buildReadUrl(bookId, chapterId, provider, extensionName = null) { + const c = encodeURIComponent(chapterId); + const p = encodeURIComponent(provider); + const extension = extensionName ? `?source=${extensionName}` : "?source=anilist"; + return `/read/${p}/${c}/${bookId}${extension}`; + }, + + getQueryParams() { + return new URLSearchParams(window.location.search); + }, + + getQueryParam(key) { + return this.getQueryParams().get(key); + } +}; + +window.URLUtils = URLUtils; \ No newline at end of file diff --git a/src/scripts/utils/youtube-player-utils.js b/src/scripts/utils/youtube-player-utils.js new file mode 100644 index 0000000..d4c2169 --- /dev/null +++ b/src/scripts/utils/youtube-player-utils.js @@ -0,0 +1,111 @@ +const YouTubePlayerUtils = { + player: null, + isAPIReady: false, + pendingVideoId: null, + + init(containerId = 'player') { + if (this.isAPIReady) return; + + const tag = document.createElement('script'); + tag.src = "https://www.youtube.com/iframe_api"; + const firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + + window.onYouTubeIframeAPIReady = () => { + this.isAPIReady = true; + this.createPlayer(containerId); + }; + }, + + createPlayer(containerId, videoId = null) { + if (!this.isAPIReady) { + this.pendingVideoId = videoId; + return; + } + + const config = { + height: '100%', + width: '100%', + playerVars: { + autoplay: 1, + controls: 0, + mute: 1, + loop: 1, + showinfo: 0, + modestbranding: 1, + disablekb: 1 + }, + events: { + onReady: (event) => { + event.target.mute(); + if (this.pendingVideoId) { + this.loadVideo(this.pendingVideoId); + this.pendingVideoId = null; + } else { + event.target.playVideo(); + } + } + } + }; + + if (videoId) { + config.videoId = videoId; + config.playerVars.playlist = videoId; + } + + this.player = new YT.Player(containerId, config); + }, + + loadVideo(videoId) { + if (!this.player || !this.player.loadVideoById) { + this.pendingVideoId = videoId; + return; + } + + this.player.loadVideoById(videoId); + this.player.mute(); + }, + + playTrailer(trailerData, containerId = 'player', fallbackImage = null) { + if (!trailerData || trailerData.site !== 'youtube' || !trailerData.id) { + + if (fallbackImage) { + this.showFallbackImage(containerId, fallbackImage); + } + return false; + } + + if (!this.isAPIReady) { + this.init(containerId); + this.pendingVideoId = trailerData.id; + } else if (this.player) { + this.loadVideo(trailerData.id); + } else { + this.createPlayer(containerId, trailerData.id); + } + + return true; + }, + + showFallbackImage(containerId, imageUrl) { + const container = document.querySelector(`#${containerId}`)?.parentElement; + if (!container) return; + + container.innerHTML = ``; + }, + + stop() { + if (this.player && this.player.stopVideo) { + this.player.stopVideo(); + } + }, + + destroy() { + if (this.player && this.player.destroy) { + this.player.destroy(); + this.player = null; + } + } +}; + +window.YouTubePlayerUtils = YouTubePlayerUtils; \ No newline at end of file diff --git a/views/anime/anime.html b/views/anime/anime.html index d3cd896..742c4e7 100644 --- a/views/anime/anime.html +++ b/views/anime/anime.html @@ -210,6 +210,14 @@ + + + + + + + + \ No newline at end of file diff --git a/views/anime/animes.html b/views/anime/animes.html index 3db2b3c..907248c 100644 --- a/views/anime/animes.html +++ b/views/anime/animes.html @@ -250,9 +250,14 @@ Click To Download
- - + + + + + + + diff --git a/views/books/book.html b/views/books/book.html index e1c6eb5..c202e9d 100644 --- a/views/books/book.html +++ b/views/books/book.html @@ -204,6 +204,13 @@ + + + + + + + diff --git a/views/books/books.html b/views/books/books.html index e92ff26..d7fb490 100644 --- a/views/books/books.html +++ b/views/books/books.html @@ -12,7 +12,6 @@ -
@@ -220,6 +219,13 @@
+ + + + + + + diff --git a/views/list.html b/views/list.html index aa4d9a8..4b192b6 100644 --- a/views/list.html +++ b/views/list.html @@ -189,70 +189,73 @@
-