diff --git a/desktop/src/scripts/dashboard.js b/desktop/src/scripts/dashboard.js index 1c7b341..8d3c445 100644 --- a/desktop/src/scripts/dashboard.js +++ b/desktop/src/scripts/dashboard.js @@ -1,468 +1,585 @@ const API_BASE = '/api'; -let currentList = []; -let filteredList = []; -let currentUserId = null; -// Configuración de paginación -const ITEMS_PER_PAGE = 50; -let visibleCount = ITEMS_PER_PAGE; +const DashboardApp = { -// Inicialización -document.addEventListener('DOMContentLoaded', async () => { - await initUser(); - await loadList(); - setupEventListeners(); - setupTabSystem(); -}); + State: { + currentList: [], + filteredList: [], + localLibraryData: [], + currentUserId: null, + currentLocalType: 'anime', + pagination: { + itemsPerPage: 50, + visibleCount: 50 + } + }, -async function initUser() { - try { - const headers = window.AuthUtils.getSimpleAuthHeaders(); - const res = await fetch(`${API_BASE}/me`, { headers }); + init: async function() { + console.log('Initializing Dashboard...'); + await this.User.init(); + await this.Tracking.load(); - if (res.ok) { - const data = await res.json(); - document.getElementById('user-username').textContent = data.username; - document.getElementById('setting-username').value = data.username; + this.UI.setupTabSystem(); + this.initListeners(); - if (data.avatar) { - document.getElementById('user-avatar').src = data.avatar; - if (data.avatar.startsWith('http')) { - document.getElementById('setting-avatar-url').value = data.avatar; - } else { - document.getElementById('setting-avatar-url').placeholder = "Image uploaded via file (Base64)"; - document.getElementById('setting-avatar-url').value = ""; + const localInput = document.getElementById('local-search-input'); + if(localInput) { + localInput.addEventListener('input', (e) => this.Library.filterContent(e.target.value)); + } + }, + + initListeners: function() { + + document.getElementById('scan-incremental-btn')?.addEventListener('click', () => this.Library.triggerScan('incremental')); + document.getElementById('scan-full-btn')?.addEventListener('click', () => this.Library.triggerScan('full')); + + document.getElementById('profile-form')?.addEventListener('submit', (e) => this.User.updateProfile(e)); + document.getElementById('password-form')?.addEventListener('submit', (e) => this.User.changePassword(e)); + document.getElementById('logout-btn')?.addEventListener('click', () => window.AuthUtils.logout()); + + const fileInput = document.getElementById('avatar-upload'); + if (fileInput) { + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = evt => { + document.getElementById('user-avatar').src = evt.target.result; + const urlInput = document.getElementById('setting-avatar-url'); + if(urlInput) urlInput.value = ''; + }; + reader.readAsDataURL(file); + } + }); + } + + const trackingInput = document.getElementById('tracking-search-input'); + if (trackingInput) trackingInput.addEventListener('input', () => this.Tracking.applyFilters()); + + ['status-filter', 'type-filter', 'sort-filter'].forEach(id => { + document.getElementById(id)?.addEventListener('change', () => this.Tracking.applyFilters()); + }); + + 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'); + view === 'list' ? container.classList.add('list-view') : container.classList.remove('list-view'); + }); + }); + }, + + User: { + init: async function() { + try { + const headers = window.AuthUtils.getSimpleAuthHeaders(); + const res = await fetch(`${API_BASE}/me`, { headers }); + + if (res.ok) { + const data = await res.json(); + document.getElementById('user-username').textContent = data.username; + const settingUsername = document.getElementById('setting-username'); + if(settingUsername) settingUsername.value = data.username; + + if (data.avatar) { + document.getElementById('user-avatar').src = data.avatar; + } + } + + const token = localStorage.getItem('token'); + if (token) { + const payload = JSON.parse(atob(token.split('.')[1])); + DashboardApp.State.currentUserId = payload.id; + await this.checkIntegrations(payload.id); + } + } catch (err) { + console.error("Error loading user profile:", err); + } + }, + + checkIntegrations: async function(userId) { + if (!userId) return; + try { + const res = await fetch(`${API_BASE}/users/${userId}/integration`); + let data = { connected: false }; + if (res.ok) data = await res.json(); + + this.updateIntegrationUI(data, userId); + } catch (e) { console.error("Integration check error:", e); } + }, + + updateIntegrationUI: function(data, userId) { + const statusEl = document.getElementById('anilist-status'); + const btn = document.getElementById('anilist-action-btn'); + const headerBadge = document.getElementById('header-anilist-link'); + + if (data.connected) { + if (headerBadge) { + headerBadge.style.display = 'flex'; + headerBadge.href = `https://anilist.co/user/${data.anilistUserId}`; + headerBadge.title = `Connected as ${data.anilistUserId}`; + } + if (statusEl) { + statusEl.textContent = `Connected as ID: ${data.anilistUserId}`; + statusEl.style.color = 'var(--color-success)'; + } + if (btn) { + btn.textContent = 'Disconnect'; + btn.className = 'btn-stream-outline link-danger'; + + btn.onclick = () => this.disconnectAniList(userId); + } + } else { + if (headerBadge) headerBadge.style.display = 'none'; + if (statusEl) { + statusEl.textContent = 'Not connected'; + statusEl.style.color = 'var(--color-text-secondary)'; + } + if (btn) { + btn.textContent = 'Connect'; + btn.className = 'btn-stream-outline'; + btn.onclick = () => this.redirectToAniListLogin(); } } - } + }, - const token = localStorage.getItem('token'); - if (token) { - const payload = JSON.parse(atob(token.split('.')[1])); - currentUserId = payload.id; - await checkIntegrations(currentUserId); - } + redirectToAniListLogin: async function() { + if (!DashboardApp.State.currentUserId) return; + try { + const clientId = 32898; + const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist'); + const state = encodeURIComponent(DashboardApp.State.currentUserId); + window.location.href = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&state=${state}`; + } catch (err) { console.error(err); alert('Error starting AniList login'); } + }, - } catch (err) { - console.error("Error loading user profile:", err); - } -} + disconnectAniList: async function(userId) { + if(!confirm("Disconnect AniList?")) return; + try { + const token = localStorage.getItem('token'); + await fetch(`${API_BASE}/users/${userId}/integration`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + this.checkIntegrations(userId); + } catch (e) { alert("Failed to disconnect"); } + }, -async function checkIntegrations(userId) { - if (!userId) return; - try { - const res = await fetch(`${API_BASE}/users/${userId}/integration`); - let data = { connected: false }; - if (res.ok) data = await res.json(); + updateProfile: async function(e) { + e.preventDefault(); + const userId = DashboardApp.State.currentUserId; + if (!userId) return; - const statusEl = document.getElementById('anilist-status'); - const btn = document.getElementById('anilist-action-btn'); - const headerBadge = document.getElementById('header-anilist-link'); + const username = document.getElementById('setting-username').value; + const urlInput = document.getElementById('setting-avatar-url')?.value || ''; + const fileInput = document.getElementById('avatar-upload'); + let finalAvatar = null; - if (data.connected) { - if (headerBadge) { - headerBadge.style.display = 'flex'; - headerBadge.href = `https://anilist.co/user/${data.anilistUserId}`; - headerBadge.title = `Connected as ${data.anilistUserId}`; + if (fileInput && fileInput.files && fileInput.files[0]) { + try { + finalAvatar = await DashboardApp.Utils.fileToBase64(fileInput.files[0]); + } catch (err) { alert("Error reading file"); return; } + } else if (urlInput.trim() !== "") { + finalAvatar = urlInput.trim(); } - if (statusEl) { - statusEl.textContent = `Connected as ID: ${data.anilistUserId}`; - statusEl.style.color = 'var(--color-success)'; - } - if (btn) { - btn.textContent = 'Disconnect'; - btn.classList.add('btn-danger-outline'); - btn.classList.remove('btn-blur'); - btn.onclick = () => disconnectAniList(userId); - } - } else { - if (headerBadge) headerBadge.style.display = 'none'; - if (statusEl) { - statusEl.textContent = 'Not connected'; - statusEl.style.color = 'var(--color-text-secondary)'; - } - if (btn) { - btn.textContent = 'Connect'; - btn.classList.remove('btn-danger-outline'); - btn.classList.add('btn-blur'); - btn.onclick = () => redirectToAniListLogin(); - } - } - } catch (e) { console.error("Integration check error:", e); } -} -async function redirectToAniListLogin() { - if (!currentUserId) return; - try { - const clientId = 32898; - const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist'); - const state = encodeURIComponent(currentUserId); + const bodyData = { username }; + if (finalAvatar) bodyData.profilePictureUrl = finalAvatar; - window.location.href = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&state=${state}`; - } catch (err) { console.error(err); alert('Error starting AniList login'); } -} + try { + const res = await fetch(`${API_BASE}/users/${userId}`, { + method: 'PUT', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify(bodyData) + }); + if (res.ok) alert('Profile updated successfully!'); + else { const err = await res.json(); alert(err.error || 'Update failed'); } + } catch (e) { console.error(e); } + }, -async function disconnectAniList(userId) { - if(!confirm("Disconnect AniList?")) return; - try { - const token = localStorage.getItem('token'); - await fetch(`${API_BASE}/users/${userId}/integration`, { - method: 'DELETE', - headers: { 'Authorization': `Bearer ${token}` } - }); - checkIntegrations(userId); - } catch (e) { alert("Failed to disconnect"); } -} + changePassword: async function(e) { + e.preventDefault(); + const userId = DashboardApp.State.currentUserId; + if (!userId) return; + const currentPassword = document.getElementById('current-password').value; + const newPassword = document.getElementById('new-password').value; -function setupTabSystem() { - const tabs = document.querySelectorAll('.nav-pill'); - const sections = document.querySelectorAll('.tab-section'); - - tabs.forEach(tab => { - tab.addEventListener('click', () => { - tabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - - const targetId = `section-${tab.dataset.target}`; - sections.forEach(sec => { - sec.classList.remove('active'); - if (sec.id === targetId) sec.classList.add('active'); - }); - - if (tab.dataset.target === 'local') loadLocalStats(); - }); - }); -} - -async function loadLocalStats() { - const types = ['anime', 'manga', 'novels']; - const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' }; - - for (const type of types) { - try { - const res = await fetch(`${API_BASE}/library/${type}`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); - if(res.ok) { + try { + const res = await fetch(`${API_BASE}/users/${userId}/password`, { + method: 'PUT', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }) + }); const data = await res.json(); - const elId = elements[type]; - if (document.getElementById(elId)) document.getElementById(elId).textContent = `${data.length} items`; - } - } catch (e) { console.error(e); } - } -} + if (res.ok) { alert("Password updated successfully"); document.getElementById('password-form').reset(); } + else alert(data.error || "Failed to update password"); + } catch (e) { console.error(e); } + } + }, -async function triggerScan(mode) { - const consoleDiv = document.getElementById('scan-console'); - const statusText = document.getElementById('scan-status-text'); - consoleDiv.style.display = 'flex'; - statusText.textContent = `Starting ${mode} scan...`; - - try { - const res = await fetch(`${API_BASE}/library/scan?mode=${mode}`, { - method: 'POST', - headers: window.AuthUtils.getSimpleAuthHeaders() - }); - if (res.ok) { - statusText.textContent = "Scan completed successfully!"; - setTimeout(() => { consoleDiv.style.display = 'none'; loadLocalStats(); }, 3000); - } else throw new Error('Scan failed'); - } catch (e) { - statusText.textContent = "Error during scan."; - statusText.style.color = 'var(--color-danger)'; - } -} - -async function handleProfileUpdate(e) { - e.preventDefault(); - if (!currentUserId) return; - - const username = document.getElementById('setting-username').value; - const urlInput = document.getElementById('setting-avatar-url').value; - const fileInput = document.getElementById('avatar-upload'); - let finalAvatar = null; - - if (fileInput.files && fileInput.files[0]) { - const toBase64 = file => new Promise((res, rej) => { - const r = new FileReader(); r.readAsDataURL(file); - r.onload = () => res(r.result); r.onerror = rej; - }); - try { finalAvatar = await toBase64(fileInput.files[0]); } catch (err) { alert("Error reading file"); return; } - } else if (urlInput.trim() !== "") { - finalAvatar = urlInput.trim(); - } - - const bodyData = { username }; - if (finalAvatar) bodyData.profilePictureUrl = finalAvatar; - - try { - const res = await fetch(`${API_BASE}/users/${currentUserId}`, { - method: 'PUT', - headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, - body: JSON.stringify(bodyData) - }); - if (res.ok) alert('Profile updated successfully!'); - else { const err = await res.json(); alert(err.error || 'Update failed'); } - } catch (e) { console.error(e); } -} - -async function handlePasswordChange(e) { - e.preventDefault(); - if (!currentUserId) return; - const currentPassword = document.getElementById('current-password').value; - const newPassword = document.getElementById('new-password').value; - - try { - const res = await fetch(`${API_BASE}/users/${currentUserId}/password`, { - method: 'PUT', - headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, - body: JSON.stringify({ currentPassword, newPassword }) - }); - const data = await res.json(); - if (res.ok) { alert("Password updated successfully"); document.getElementById('password-form').reset(); } - else alert(data.error || "Failed to update password"); - } catch (e) { console.error(e); } -} - -function setupEventListeners() { - document.getElementById('scan-incremental-btn')?.addEventListener('click', () => triggerScan('incremental')); - document.getElementById('scan-full-btn')?.addEventListener('click', () => triggerScan('full')); - document.getElementById('profile-form')?.addEventListener('submit', handleProfileUpdate); - document.getElementById('password-form')?.addEventListener('submit', handlePasswordChange); - document.getElementById('logout-btn')?.addEventListener('click', () => window.AuthUtils.logout()); - - const fileInput = document.getElementById('avatar-upload'); - if (fileInput) { - fileInput.addEventListener('change', function(e) { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = evt => { - document.getElementById('user-avatar').src = evt.target.result; - document.getElementById('setting-avatar-url').value = ''; - }; - reader.readAsDataURL(file); - } - }); - } - - document.querySelector('.search-input').addEventListener('input', () => applyFilters()); - ['status-filter', 'type-filter', 'sort-filter'].forEach(id => { - document.getElementById(id).addEventListener('change', () => applyFilters()); - }); - - 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; + Tracking: { + load: async function() { + const loadingState = document.getElementById('loading-state'); + const emptyState = document.getElementById('empty-state'); const container = document.getElementById('list-container'); - if (view === 'list') container.classList.add('list-view'); - else container.classList.remove('list-view'); - }); - }); -} -// --- LOGICA DE LISTA --- + try { + loadingState.style.display = 'flex'; + emptyState.style.display = 'none'; + container.innerHTML = ''; -async function loadList() { - const loadingState = document.getElementById('loading-state'); - const emptyState = document.getElementById('empty-state'); - const container = document.getElementById('list-container'); + const response = await fetch(`${API_BASE}/list`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); + if (!response.ok) throw new Error('Failed'); - try { - loadingState.style.display = 'flex'; - emptyState.style.display = 'none'; - container.innerHTML = ''; + const data = await response.json(); + DashboardApp.State.currentList = data.results || []; - const response = await fetch(`${API_BASE}/list`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); - if (!response.ok) throw new Error('Failed'); + this.updateStats(); - const data = await response.json(); - currentList = data.results || []; + loadingState.style.display = 'none'; + if (DashboardApp.State.currentList.length === 0) emptyState.style.display = 'flex'; + else this.applyFilters(); - const animeCount = currentList.filter(item => item.entry_type === 'ANIME').length; - const mangaCount = currentList.filter(item => item.entry_type === 'MANGA').length; + } catch (error) { + console.error(error); + loadingState.style.display = 'none'; + } + }, - document.getElementById('total-stat').textContent = currentList.length; - if (document.getElementById('anime-stat')) document.getElementById('anime-stat').textContent = animeCount; - if (document.getElementById('manga-stat')) document.getElementById('manga-stat').textContent = mangaCount; + updateStats: function() { + const list = DashboardApp.State.currentList; + const animeCount = list.filter(item => item.entry_type === 'ANIME').length; + const mangaCount = list.filter(item => item.entry_type === 'MANGA').length; - loadingState.style.display = 'none'; - if (currentList.length === 0) emptyState.style.display = 'flex'; - else applyFilters(); + document.getElementById('total-stat').textContent = list.length; + if (document.getElementById('anime-stat')) document.getElementById('anime-stat').textContent = animeCount; + if (document.getElementById('manga-stat')) document.getElementById('manga-stat').textContent = mangaCount; + }, - } catch (error) { - console.error(error); - loadingState.style.display = 'none'; - } -} + applyFilters: function() { + const statusFilter = document.getElementById('status-filter').value; + const typeFilter = document.getElementById('type-filter').value; + const sortFilter = document.getElementById('sort-filter').value; + const searchInput = document.getElementById('tracking-search-input'); + const searchQuery = searchInput ? searchInput.value.toLowerCase().trim() : ''; -function applyFilters() { - const statusFilter = document.getElementById('status-filter').value; - const typeFilter = document.getElementById('type-filter').value; - const sortFilter = document.getElementById('sort-filter').value; - const searchQuery = document.querySelector('.search-input').value.toLowerCase().trim(); + let result = [...DashboardApp.State.currentList]; - let result = [...currentList]; + if (searchQuery) { + result = result.filter(item => (item.title ? item.title.toLowerCase() : '').includes(searchQuery)); + } - if (searchQuery) result = result.filter(item => (item.title || '').toLowerCase().includes(searchQuery)); - if (statusFilter !== 'all') result = result.filter(item => item.status === statusFilter); - if (typeFilter !== 'all') result = result.filter(item => item.entry_type === typeFilter); + if (statusFilter !== 'all') result = result.filter(item => item.status === statusFilter); + if (typeFilter !== 'all') result = result.filter(item => item.entry_type === typeFilter); - if (sortFilter === 'title') result.sort((a, b) => (a.title || '').localeCompare(b.title || '')); - else if (sortFilter === 'score') result.sort((a, b) => (b.score || 0) - (a.score || 0)); - else result.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + if (sortFilter === 'title') result.sort((a, b) => (a.title || '').localeCompare(b.title || '')); + else if (sortFilter === 'score') result.sort((a, b) => (b.score || 0) - (a.score || 0)); + else result.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); - filteredList = result; - visibleCount = ITEMS_PER_PAGE; - renderList(); -} + DashboardApp.State.filteredList = result; + DashboardApp.State.pagination.visibleCount = DashboardApp.State.pagination.itemsPerPage; + this.render(); + }, -function renderList() { - const container = document.getElementById('list-container'); - container.innerHTML = ''; + render: function() { + const container = document.getElementById('list-container'); + container.innerHTML = ''; + const list = DashboardApp.State.filteredList; + const count = DashboardApp.State.pagination.visibleCount; - if (filteredList.length === 0) { - container.innerHTML = '
Error loading library
No ${type} files found.
+ +No books found.
'; - return; - } - - renderLocalEntries(localEntries); - } catch { - grid.innerHTML = 'Error loading library.
'; - } -} - -function filterLocal(type) { - if (type === 'all') renderLocalEntries(localEntries); - else renderLocalEntries(localEntries.filter(e => e.type === type)); -} - -function renderLocalEntries(entries) { - const grid = document.getElementById('local-entries-grid'); - grid.innerHTML = entries.map(entry => { - const title = entry.metadata?.title?.romaji || entry.metadata?.title?.english || entry.id; - const cover = entry.metadata?.coverImage?.extraLarge || '/public/assets/placeholder.jpg'; - const chapters = entry.metadata?.chapters || '??'; - - return ` -- ${chapters} Chapters -
-No anime found in your local library. Click "Scan Library" to scan your folders.
'; - - return; - } - - // Renderizar grid - grid.innerHTML = entries.map(entry => { - const title = entry.metadata?.title?.romaji || entry.metadata?.title?.english || entry.id; - const cover = entry.metadata?.coverImage?.extraLarge || entry.metadata?.coverImage?.large || '/public/assets/placeholder.jpg'; - const score = entry.metadata?.averageScore || '--'; - const episodes = entry.metadata?.episodes || '??'; - - return ` -- ${score}% • ${episodes} Eps -
-Error loading local library. Make sure the backend is running.
'; - } -} - - -async function scanLocalLibrary() { - const btnText = document.getElementById('scan-text'); - const originalText = btnText.innerText; - btnText.innerText = "Scanning..."; - - try { - const response = await fetch('/api/library/scan?mode=incremental', { - method: 'POST' - }); - - if (response.ok) { - await loadLocalEntries(); - // Mostrar notificación de éxito si tienes sistema de notificaciones - if (window.NotificationUtils) { - NotificationUtils.show('Library scanned successfully!', 'success'); - } - } else { - throw new Error('Scan failed'); - } - } catch (err) { - console.error("Scan failed", err); - alert("Failed to scan library. Check console for details."); - - // Mostrar notificación de error si tienes sistema de notificaciones - if (window.NotificationUtils) { - NotificationUtils.show('Failed to scan library', 'error'); - } - } finally { - btnText.innerText = originalText; - } -} - -function viewLocalEntry(anilistId) { - if (!anilistId) { - console.warn('Anime not linked'); - return; - } - window.location.href = `/anime/${anilistId}`; -} - -function renderLocalEntries(entries) { - const grid = document.getElementById('local-entries-grid'); - - grid.innerHTML = entries.map(entry => { - const title = entry.metadata?.title?.romaji - || entry.metadata?.title?.english - || entry.id; - - const cover = - entry.metadata?.coverImage?.extraLarge - || entry.metadata?.coverImage?.large - || '/public/assets/placeholder.jpg'; - - const score = entry.metadata?.averageScore || '--'; - const episodes = entry.metadata?.episodes || '??'; - - return ` -- ${score}% • ${episodes} Eps -
-