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 = '
No matches found
'; - return; - } + if (list.length === 0) { + container.innerHTML = '
No matches found
'; + return; + } - const itemsToShow = filteredList.slice(0, visibleCount); - itemsToShow.forEach(item => container.appendChild(createListItem(item))); + const itemsToShow = list.slice(0, count); + itemsToShow.forEach(item => container.appendChild(this.createItemElement(item))); - if (visibleCount < filteredList.length) { - const remaining = filteredList.length - visibleCount; - const btnContainer = document.createElement('div'); - btnContainer.style.gridColumn = "1 / -1"; - btnContainer.style.display = "flex"; - btnContainer.style.justifyContent = "center"; - btnContainer.style.padding = "2rem 0"; + if (count < list.length) { + this.renderLoadMoreButton(container, list.length - count); + } + }, - const loadMoreBtn = document.createElement('button'); - loadMoreBtn.className = "btn-blur"; - loadMoreBtn.textContent = `Show All (${remaining} more)`; - loadMoreBtn.onclick = () => { visibleCount = filteredList.length; renderList(); }; - btnContainer.appendChild(loadMoreBtn); - container.appendChild(btnContainer); - } -} + createItemElement: function(item) { + const div = document.createElement('div'); + div.className = 'list-item'; -function createListItem(item) { - const div = document.createElement('div'); - div.className = 'list-item'; + const itemLink = this.getEntryLink(item); + const posterUrl = item.poster || '/public/assets/placeholder.svg'; + const progress = item.progress || 0; + const totalUnits = item.entry_type === 'ANIME' ? item.total_episodes || 0 : item.total_chapters || 0; + const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0; + const score = item.score ? item.score.toFixed(1) : null; + const entryType = (item.entry_type).toUpperCase(); - const itemLink = getEntryLink(item); - const posterUrl = item.poster || '/public/assets/placeholder.svg'; - const progress = item.progress || 0; - const totalUnits = item.entry_type === 'ANIME' ? item.total_episodes || 0 : item.total_chapters || 0; - 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(); - const unitLabel = entryType === 'ANIME' ? 'episodes' : 'chapters'; + const statusLabels = { + 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', + 'COMPLETED': 'Completed', 'PLANNING': 'Planning', 'PAUSED': 'Paused', + 'DROPPED': 'Dropped', 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' + }; - const statusLabels = { - 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', - 'COMPLETED': 'Completed', - 'PLANNING': 'Planning', - 'PAUSED': 'Paused', - 'DROPPED': 'Dropped', - 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' - }; + const extraInfo = []; + if (item.repeat_count > 0) extraInfo.push(`🔁 ${item.repeat_count}`); + if (item.is_private) extraInfo.push('🔒 Private'); - const extraInfo = []; - if (repeatCount > 0) extraInfo.push(`🔁 ${repeatCount}`); - if (item.is_private) extraInfo.push('🔒 Private'); - - div.innerHTML = ` - - ${item.title || 'Entry'} - -
-
- -

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

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

${item.title || 'Unknown'}

+
+
+ ${statusLabels[item.status] || item.status} + ${entryType} + ${item.source.toUpperCase()} + ${extraInfo.join('')} +
+
+
+
+
+ ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} + ${score ? `⭐ ${score}` : ''} +
+
-
-
-
-
-
-
- ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel} ${score ? `⭐ ${score}` : ''} -
-
-
- - `; + + `; - // Lógica para abrir el Modal (Estilo book.js: seteamos currentData) - const editBtn = div.querySelector('.edit-icon-btn'); - editBtn.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); + div.querySelector('.edit-icon-btn').onclick = (e) => { + e.preventDefault(); e.stopPropagation(); + window.ListModalManager.currentData = item; + window.ListModalManager.isInList = true; + window.ListModalManager.currentEntry = item; + window.ListModalManager.open(item, item.source || 'anilist'); + }; - // 1. Configuramos el modal manager con los datos de ESTE item - window.ListModalManager.currentData = item; - window.ListModalManager.isInList = true; - window.ListModalManager.currentEntry = item; // Ya tenemos los datos de la lista + return div; + }, - // 2. Abrimos el modal - window.ListModalManager.open(item, item.source || 'anilist'); - }; + renderLoadMoreButton: function(container, remaining) { + const btnContainer = document.createElement('div'); + Object.assign(btnContainer.style, { gridColumn: "1 / -1", display: "flex", justifyContent: "center", padding: "2rem 0" }); - return div; -} + const loadMoreBtn = document.createElement('button'); + loadMoreBtn.className = "btn-blur"; + loadMoreBtn.textContent = `Show All (${remaining} more)`; + loadMoreBtn.onclick = () => { + DashboardApp.State.pagination.visibleCount = DashboardApp.State.filteredList.length; + this.render(); + }; -function getEntryLink(item) { - const isAnime = item.entry_type?.toUpperCase() === 'ANIME'; - const baseRoute = isAnime ? '/anime' : '/book'; - return `${baseRoute}/${item.entry_id}`; -} + btnContainer.appendChild(loadMoreBtn); + container.appendChild(btnContainer); + }, -// ========================================================= -// EXPORTS GLOBALES (Estilo book.js) -// Estas funciones son llamadas por los onclick del HTML del Modal -// ========================================================= + getEntryLink: function(item) { + const baseRoute = (item.entry_type?.toUpperCase() === 'ANIME') ? '/anime' : '/book'; + return `${baseRoute}/${item.entry_id}`; + } + }, + + Library: { + loadStats: async function() { + 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) { + const data = await res.json(); + const el = document.getElementById(elements[type]); + if (el) el.textContent = `${data.length} items`; + } + } catch (e) { console.error(e); } + } + }, + + loadContent: async function(type) { + DashboardApp.State.currentLocalType = type; + const container = document.getElementById('local-list-container'); + const loading = document.getElementById('local-loading'); + const searchInput = document.getElementById('local-search-input'); + + container.innerHTML = ''; + loading.style.display = 'flex'; + if(searchInput) searchInput.value = ''; + + try { + const res = await fetch(`${API_BASE}/library/${type}`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); + if (!res.ok) throw new Error('Failed to load local content'); + + const data = await res.json(); + DashboardApp.State.localLibraryData = data; + this.renderGrid(data, type); + } catch (err) { + console.error(err); + container.innerHTML = `

Error loading library

`; + } finally { + loading.style.display = 'none'; + } + }, + + renderGrid: function(entries, type) { + const container = document.getElementById('local-list-container'); + container.innerHTML = ''; + + if (entries.length === 0) { + container.innerHTML = ` +
+

No ${type} files found.

+ +
`; + return; + } + + entries.forEach(entry => { + const isMatched = entry.matched && entry.metadata; + const meta = entry.metadata || {}; + let poster = meta.coverImage?.large || '/public/assets/placeholder.svg'; + + let title = isMatched ? (meta.title?.english || meta.title?.romaji) : entry.folder_name; + if (!isMatched) title = title.replace(/\[.*?\]|\(.*?\)|\.mkv|\.mp4/g, '').trim(); + + const url = isMatched ? (type === 'anime' ? `/anime/${meta.id}` : `/book/${meta.id}`) : '#'; + + const div = document.createElement('div'); + div.className = 'list-item'; + div.innerHTML = ` + +
+

${title}

+
+ ${entry.files ? entry.files.length : 0} FILES + ${isMatched ? 'MATCHED' : ''} +
+
${entry.folder_name}
+
+ + `; + container.appendChild(div); + }); + }, + + filterContent: function(query) { + if (!DashboardApp.State.localLibraryData) return; + const lowerQuery = query.toLowerCase(); + const filtered = DashboardApp.State.localLibraryData.filter(entry => { + const metaTitle = entry.metadata?.title?.english || entry.metadata?.title?.romaji || ''; + const folderName = entry.folder_name || ''; + return metaTitle.toLowerCase().includes(lowerQuery) || folderName.toLowerCase().includes(lowerQuery); + }); + this.renderGrid(filtered, DashboardApp.State.currentLocalType); + }, + + triggerScan: async function(mode) { + const consoleDiv = document.getElementById('scan-console'); + const statusText = document.getElementById('scan-status-text'); + if(consoleDiv) consoleDiv.style.display = 'flex'; + if(statusText) 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) { + if(statusText) statusText.textContent = "Scan completed successfully!"; + setTimeout(() => { if(consoleDiv) consoleDiv.style.display = 'none'; this.loadStats(); }, 3000); + } else throw new Error('Scan failed'); + } catch (e) { + if(statusText) { statusText.textContent = "Error during scan."; statusText.style.color = 'var(--color-danger)'; } + } + }, + + openManualMatch: function(id, type) { + const newId = prompt("Enter AniList ID to force match:"); + if (newId) { + fetch(`${API_BASE}/library/${type}/${id}/match`, { + method: 'POST', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ source: 'anilist', matched_id: parseInt(newId) }) + }).then(res => { + if(res.ok) { alert("Matched! Refreshing..."); this.loadContent(type); } + else { alert("Failed to match."); } + }); + } + }, + + switchType: function(type, btnElement) { + document.querySelectorAll('.type-pill-btn').forEach(b => b.classList.remove('active')); + if(btnElement) btnElement.classList.add('active'); + this.loadContent(type); + } + }, + + UI: { + setupTabSystem: function() { + 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') { + DashboardApp.Library.loadStats(); + DashboardApp.Library.loadContent('anime'); + } + }); + }); + } + }, + + Utils: { + fileToBase64: (file) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + }) + } +}; + +document.addEventListener('DOMContentLoaded', () => { + DashboardApp.init(); +}); + +window.switchLocalType = (type, btn) => DashboardApp.Library.switchType(type, btn); window.saveToList = async () => { - // Recuperamos los datos que seteamos en el onclick del botón editar const data = window.ListModalManager.currentData; if (!data) return; - - // En la vista de lista, el ID suele ser 'entry_id', pero usamos un fallback const idToSave = data.entry_id || data.id; - const source = data.source || 'anilist'; - - // Llamamos al manager pasando ID y Source explícitamente como en book.js - await window.ListModalManager.save(idToSave, source); - - // IMPORTANTE: Recargar la lista para ver los cambios - await loadList(); + await window.ListModalManager.save(idToSave, data.source || 'anilist'); + await DashboardApp.Tracking.load(); }; window.deleteFromList = async () => { const data = window.ListModalManager.currentData; if (!data) return; - const idToDelete = data.entry_id || data.id; - const source = data.source || 'anilist'; - - await window.ListModalManager.delete(idToDelete, source); - - // IMPORTANTE: Recargar la lista - await loadList(); + await window.ListModalManager.delete(idToDelete, data.source || 'anilist'); + await DashboardApp.Tracking.load(); }; -window.closeAddToListModal = () => { - window.ListModalManager.close(); -}; \ No newline at end of file +window.closeAddToListModal = () => window.ListModalManager.close(); \ No newline at end of file diff --git a/desktop/src/scripts/local-library-books.js b/desktop/src/scripts/local-library-books.js deleted file mode 100644 index 3ab311c..0000000 --- a/desktop/src/scripts/local-library-books.js +++ /dev/null @@ -1,106 +0,0 @@ -let activeFilter = 'all'; -let activeSort = 'az'; -let isLocalMode = false; -let localEntries = []; - -function toggleLibraryMode() { - isLocalMode = !isLocalMode; - const btn = document.getElementById('library-mode-btn'); - const onlineContent = document.getElementById('online-content'); - const localContent = document.getElementById('local-content'); - - if (isLocalMode) { - btn.classList.add('active'); - onlineContent.classList.add('hidden'); - localContent.classList.remove('hidden'); - loadLocalEntries(); - } else { - btn.classList.remove('active'); - onlineContent.classList.remove('hidden'); - localContent.classList.add('hidden'); - } -} - -async function loadLocalEntries() { - const grid = document.getElementById('local-entries-grid'); - grid.innerHTML = '
'.repeat(6); - - try { - const [mangaRes, novelRes] = await Promise.all([ - fetch('/api/library/manga'), - fetch('/api/library/novels') - ]); - - const [manga, novel] = await Promise.all([ - mangaRes.json(), - novelRes.json() - ]); - - localEntries = [ - ...manga.map(e => ({ ...e, type: 'manga' })), - ...novel.map(e => ({ ...e, type: 'novel' })) - ]; - - if (localEntries.length === 0) { - grid.innerHTML = '

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 ` -
-
- ${title} -
-
-
${title}
-

- ${chapters} Chapters -

-
${entry.type}
-
- ${entry.matched ? '● Linked' : '○ Unlinked'} -
-
-
- `; - }).join(''); -} - -async function scanLocalLibrary() { - const btnText = document.getElementById('scan-text'); - btnText.innerText = "Scanning..."; - try { - // Asumiendo que el scan de libros usa este query param - const response = await fetch('/api/library/scan?mode=incremental', { method: 'POST' }); - if (response.ok) { - await loadLocalEntries(); - if (window.NotificationUtils) NotificationUtils.show('Library scanned!', 'success'); - } - } catch (err) { - if (window.NotificationUtils) NotificationUtils.show('Scan failed', 'error'); - } finally { - btnText.innerText = "Scan Library"; - } -} - -function viewLocalEntry(id) { - if (id) window.location.href = `/book/${id}`; -} \ No newline at end of file diff --git a/desktop/src/scripts/local-library.js b/desktop/src/scripts/local-library.js deleted file mode 100644 index 76c89aa..0000000 --- a/desktop/src/scripts/local-library.js +++ /dev/null @@ -1,209 +0,0 @@ -let activeFilter = 'all'; -let activeSort = 'az'; -let isLocalMode = false; -let localEntries = []; - -function toggleLibraryMode() { - isLocalMode = !isLocalMode; - - const btn = document.getElementById('library-mode-btn'); - const onlineContent = document.getElementById('online-content'); - const localContent = document.getElementById('local-content'); - const svg = btn.querySelector('svg'); - const label = btn.querySelector('span'); - - if (isLocalMode) { - // LOCAL MODE - btn.classList.add('active'); - onlineContent.classList.add('hidden'); - localContent.classList.remove('hidden'); - loadLocalEntries(); - - svg.innerHTML = ` - - - `; - } else { - // ONLINE MODE - btn.classList.remove('active'); - onlineContent.classList.remove('hidden'); - localContent.classList.add('hidden'); - - svg.innerHTML = ` - - - - `; - } -} - -async function loadLocalEntries() { - const grid = document.getElementById('local-entries-grid'); - grid.innerHTML = '
'.repeat(8); - - try { - const response = await fetch('/api/library/anime'); - const entries = await response.json(); - localEntries = entries; - - if (entries.length === 0) { - grid.innerHTML = '

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 ` -
-
- ${title} -
-
-
${title}
-

- ${score}% • ${episodes} Eps -

-
- ${entry.matched ? '● Linked' : '○ Unlinked'} -
-
-
- `; - }).join(''); - } catch (err) { - console.error('Error loading local entries:', err); - grid.innerHTML = '

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 ` -
-
- ${title} -
-
-
${title}
-

- ${score}% • ${episodes} Eps -

-
- ${entry.matched ? '● Linked' : '○ Unlinked'} -
-
-
- `; - }).join(''); -} - -function applyLocalFilters() { - let filtered = [...localEntries]; - - if (activeFilter === 'linked') { - filtered = filtered.filter(e => e.matched); - } - - if (activeFilter === 'unlinked') { - filtered = filtered.filter(e => !e.matched); - } - - if (activeSort === 'az') { - filtered.sort((a, b) => - (a.metadata?.title?.romaji || a.id) - .localeCompare(b.metadata?.title?.romaji || b.id) - ); - } - - if (activeSort === 'za') { - filtered.sort((a, b) => - (b.metadata?.title?.romaji || b.id) - .localeCompare(a.metadata?.title?.romaji || a.id) - ); - } - - renderLocalEntries(filtered); -} - -document.addEventListener('click', e => { - const btn = e.target.closest('.filter-btn'); - if (!btn) return; - - if (btn.dataset.filter) { - activeFilter = btn.dataset.filter; - } - - if (btn.dataset.sort) { - activeSort = btn.dataset.sort; - } - - btn - .closest('.local-filters') - .querySelectorAll('.filter-btn') - .forEach(b => b.classList.remove('active')); - - btn.classList.add('active'); - - applyLocalFilters(); -}); diff --git a/desktop/views/anime/animes.html b/desktop/views/anime/animes.html index 801c84f..be468d2 100644 --- a/desktop/views/anime/animes.html +++ b/desktop/views/anime/animes.html @@ -117,46 +117,6 @@ - -
-
-
-
Local Anime Library
- -
-
-
- - - - - -
- -
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
Local Books Library
- -
-
-
- - -
-
- -
-
-
-
-
-
-
-
@@ -132,7 +106,6 @@ - diff --git a/desktop/views/css/dashboard.css b/desktop/views/css/dashboard.css index 19d193b..f46e5cf 100644 --- a/desktop/views/css/dashboard.css +++ b/desktop/views/css/dashboard.css @@ -9,13 +9,11 @@ overflow-x: hidden; } -/* Efecto de fondo ambiental global (Glow superior) */ +/* Efecto de fondo ambiental (Glow superior) */ .main-wrapper::before { content: ''; position: absolute; - top: 0; - left: 0; - right: 0; + top: 0; left: 0; right: 0; height: 60vh; background: radial-gradient(circle at 50% 0%, rgba(139, 92, 246, 0.15), transparent 70%); z-index: -1; @@ -35,728 +33,11 @@ } /* ========================================= - 2. PROFILE HERO SECTION (Estilo Apple TV Profile) - ========================================= */ -.profile-hero { - position: relative; - padding: 8rem 3rem 2rem 3rem; /* Espacio extra arriba para titlebar/navbar */ - margin-bottom: 2rem; - display: flex; - flex-direction: column; - justify-content: flex-end; -} - -.profile-content { - display: flex; - align-items: flex-end; - gap: 2.5rem; - position: relative; - z-index: 2; -} - -.profile-avatar-container { - position: relative; - width: 160px; - height: 160px; - flex-shrink: 0; -} - -.profile-avatar { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - border: 4px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); - transition: transform 0.3s ease; -} - -.profile-avatar:hover { - transform: scale(1.02); - border-color: var(--color-primary); -} - -.edit-avatar-btn { - position: absolute; - bottom: 5px; - right: 5px; - background: var(--color-bg-elevated); - border: 1px solid rgba(255,255,255,0.2); - color: white; - width: 40px; - height: 40px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 4px 10px rgba(0,0,0,0.3); - transition: all 0.2s; -} - -.edit-avatar-btn:hover { - background: var(--color-primary); - transform: scale(1.1); -} - -.profile-info { - margin-bottom: 1.5rem; -} - -.profile-name { - font-size: 3.5rem; - font-weight: 800; - margin: 0 0 0.5rem 0; - line-height: 1; - letter-spacing: -0.02em; - background: linear-gradient(to bottom right, #fff, #a1a1aa); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -.profile-stats-pills { - display: flex; - gap: 0.8rem; - flex-wrap: wrap; -} - -.stat-pill { - background: rgba(255, 255, 255, 0.06); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - padding: 0.4rem 1rem; - border-radius: 99px; - font-size: 0.85rem; - font-weight: 600; - color: var(--color-text-secondary); - display: flex; - align-items: center; - gap: 0.5rem; - transition: 0.2s; -} - -.stat-pill:hover { - background: rgba(255, 255, 255, 0.1); - color: white; -} - -.stat-pill.highlight { - background: rgba(139, 92, 246, 0.15); - color: #a78bfa; - border-color: rgba(139, 92, 246, 0.3); -} - -/* ========================================= - 3. NAVEGACIÓN (Tabs) - ========================================= */ -.hub-navigation { - display: flex; - gap: 2.5rem; - margin-top: 3rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - padding-left: 3rem; -} - -.nav-tab { - background: none; - border: none; - color: var(--color-text-muted); - font-size: 1.1rem; - font-weight: 600; - padding: 1rem 0; - cursor: pointer; - position: relative; - display: flex; - align-items: center; - gap: 0.6rem; - transition: color 0.3s ease; -} - -.nav-tab svg { - opacity: 0.7; - transition: 0.3s; -} - -.nav-tab:hover { - color: white; -} - -.nav-tab:hover svg { - opacity: 1; -} - -.nav-tab.active { - color: white; -} - -.nav-tab.active::after { - content: ''; - position: absolute; - bottom: -1px; - left: 0; - width: 100%; - height: 3px; - background: var(--color-primary); - border-radius: 3px 3px 0 0; - box-shadow: 0 -4px 15px var(--color-primary-glow); -} - -.tab-section { - display: none; - opacity: 0; - transform: translateY(10px); - transition: opacity 0.3s, transform 0.3s; -} - -.tab-section.active { - display: block; - opacity: 1; - transform: translateY(0); -} - -/* ========================================= - 4. TOOLBAR & FILTROS - ========================================= */ -.toolbar { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2.5rem; - background: rgba(255,255,255,0.03); - border: 1px solid rgba(255,255,255,0.05); - padding: 0.75rem 1rem; - border-radius: 12px; - backdrop-filter: blur(5px); - flex-wrap: wrap; - gap: 1rem; -} - -.search-box { - position: relative; - flex-grow: 1; - max-width: 400px; -} - -.search-box svg { - position: absolute; - left: 14px; - top: 50%; - transform: translateY(-50%); - color: var(--color-text-muted); - pointer-events: none; -} - -.search-input { - width: 100%; - background: rgba(0,0,0,0.2); - border: 1px solid transparent; - padding: 0.7rem 1rem 0.7rem 2.8rem; - border-radius: 8px; - color: white; - font-family: inherit; - font-size: 0.95rem; - transition: all 0.2s; -} - -.search-input:focus { - background: rgba(0,0,0,0.4); - border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); -} - -.filters-inline { - display: flex; - gap: 0.8rem; - align-items: center; - flex-wrap: wrap; -} - -.minimal-select { - background: rgba(255,255,255,0.05); - border: 1px solid rgba(255,255,255,0.08); - color: var(--color-text-secondary); - padding: 0.6rem 2rem 0.6rem 1rem; /* Padding derecho para la flecha */ - border-radius: 8px; - cursor: pointer; - font-family: inherit; - font-size: 0.9rem; - font-weight: 500; - transition: 0.2s; - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23a1a1aa'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.7rem center; - background-size: 1rem; -} - -.minimal-select:hover { - background: rgba(255,255,255,0.1); - color: white; -} - -.minimal-select:focus { - border-color: var(--color-primary); - color: white; -} - -.view-toggle { - display: flex; - background: rgba(0,0,0,0.3); - border-radius: 8px; - padding: 2px; -} - -.view-btn { - background: transparent; - border: none; - color: var(--color-text-muted); - padding: 0.5rem 0.8rem; - border-radius: 6px; - cursor: pointer; - transition: 0.2s; - font-size: 1.1rem; - line-height: 1; -} - -.view-btn:hover { - color: white; -} - -.view-btn.active { - background: rgba(255,255,255,0.1); - color: white; -} - -/* ========================================= - 5. LIST GRID (Estilo Netflix Cards) - ========================================= */ -.list-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 2rem 1.5rem; -} - -.list-item { - position: relative; - border-radius: 8px; - transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.3s; - cursor: pointer; - background: transparent; -} - -.list-item:hover { - transform: scale(1.05); - z-index: 10; -} - -/* Poster */ -.item-poster-link { - display: block; - position: relative; - border-radius: 8px; - overflow: hidden; - aspect-ratio: 2/3; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); -} - -.item-poster { - width: 100%; - height: 100%; - object-fit: cover; - transition: filter 0.3s; -} - -/* Oscurecer ligeramente la imagen en hover para destacar texto si se desea, o brillo */ -.list-item:hover .item-poster { - filter: brightness(1.1); -} - -/* Contenido debajo del poster (Título, etc) */ -.item-content { - padding-top: 0.8rem; -} - -.item-title { - font-size: 0.95rem; - font-weight: 700; - margin: 0 0 0.4rem 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: rgba(255,255,255,0.9); -} - -.list-item:hover .item-title { - color: var(--color-primary); -} - -.item-meta { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; - margin-bottom: 0.5rem; -} - -.meta-pill { - font-size: 0.65rem; - padding: 0.15rem 0.4rem; - border-radius: 4px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.status-pill { background: rgba(34, 197, 94, 0.15); color: #4ade80; } -.type-pill { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; } - -.progress-text { - font-size: 0.8rem; - color: var(--color-text-secondary); - display: flex; - justify-content: space-between; -} - -/* Botón Editar flotante (solo visible en hover) */ -.edit-icon-btn { - position: absolute; - top: 10px; - right: 10px; - background: rgba(0,0,0,0.7); - backdrop-filter: blur(4px); - border: 1px solid rgba(255,255,255,0.2); - color: white; - width: 32px; - height: 32px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - transform: translateY(-5px); - transition: all 0.2s; - z-index: 20; -} - -.list-item:hover .edit-icon-btn { - opacity: 1; - transform: translateY(0); -} - -.edit-icon-btn:hover { - background: var(--color-primary); - border-color: var(--color-primary); -} - -/* --- LIST VIEW MODE --- */ -.list-grid.list-view { - grid-template-columns: 1fr; - gap: 1rem; -} - -.list-grid.list-view .list-item { - display: flex; - align-items: center; - background: var(--color-bg-elevated); - padding: 1rem; - border: 1px solid rgba(255,255,255,0.05); -} - -.list-grid.list-view .list-item:hover { - transform: scale(1.01); - border-color: var(--color-primary); - background: var(--color-bg-elevated-hover); -} - -.list-grid.list-view .item-poster-link { - width: 60px; - aspect-ratio: 2/3; - margin-right: 1.5rem; - flex-shrink: 0; -} - -.list-grid.list-view .item-content { - padding-top: 0; - flex-grow: 1; - display: flex; - align-items: center; - justify-content: space-between; -} - -.list-grid.list-view .item-title { - font-size: 1.1rem; - margin-bottom: 0.2rem; -} - -.list-grid.list-view .edit-icon-btn { - position: static; - opacity: 1; - transform: none; - background: transparent; - border: 1px solid rgba(255,255,255,0.1); -} - -/* ========================================= - 6. LOCAL LIBRARY (Dashboard Widgets) - ========================================= */ -.local-stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.local-card { - background: var(--color-bg-elevated); - border: 1px solid rgba(255,255,255,0.05); - padding: 1.5rem; - border-radius: 16px; - position: relative; - overflow: hidden; - transition: 0.3s; -} - -.local-card::after { - content: ''; - position: absolute; - top: 0; right: 0; width: 100px; height: 100px; - background: radial-gradient(circle at top right, rgba(139, 92, 246, 0.1), transparent 70%); -} - -.local-card:hover { - border-color: rgba(139, 92, 246, 0.3); - transform: translateY(-5px); -} - -.local-card h3 { - margin: 0; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 1px; - color: var(--color-text-secondary); -} - -.local-card p { - font-size: 2.5rem; - font-weight: 800; - margin: 0.5rem 0 0 0; - color: white; -} - -.section-header-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2rem; - padding-bottom: 1rem; - border-bottom: 1px solid rgba(255,255,255,0.05); -} - -.console-output { - background: #09090b; - border: 1px solid #27272a; - padding: 1.5rem; - border-radius: 12px; - font-family: 'JetBrains Mono', monospace; - font-size: 0.9rem; - color: #4ade80; - display: flex; - align-items: center; - gap: 1rem; - box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); -} - -/* ========================================= - 7. SETTINGS (Clean Forms) - ========================================= */ -.settings-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - gap: 2rem; -} - -.settings-card { - background: rgba(20, 20, 23, 0.6); - backdrop-filter: blur(10px); - border: 1px solid rgba(255,255,255,0.08); - padding: 2.5rem; - border-radius: 20px; -} - -.settings-card.full-width { - grid-column: 1 / -1; -} - -.settings-card h3 { - margin-top: 0; - font-size: 1.3rem; - border-bottom: 1px solid rgba(255,255,255,0.05); - padding-bottom: 1rem; - margin-bottom: 1.5rem; -} - -.form-group { - margin-bottom: 1.5rem; -} - -.form-group label { - display: block; - margin-bottom: 0.6rem; - font-size: 0.9rem; - color: var(--color-text-secondary); - font-weight: 500; -} - -.input-field { - width: 100%; - background: rgba(0,0,0,0.2); - border: 1px solid rgba(255,255,255,0.1); - padding: 0.9rem 1rem; - border-radius: 10px; - color: white; - font-size: 1rem; - transition: 0.2s; -} - -.input-field:focus { - background: rgba(0,0,0,0.4); - border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); -} - -.integration-row { - display: flex; - justify-content: space-between; - align-items: center; - background: rgba(255,255,255,0.03); - padding: 1.5rem; - border-radius: 12px; - border: 1px solid rgba(255,255,255,0.05); -} - -/* Botones específicos de settings */ -.btn-danger-outline { - background: transparent; - border: 1px solid rgba(239, 68, 68, 0.5); - color: #ef4444; - padding: 0.8rem 1.5rem; - border-radius: 99px; - cursor: pointer; - font-weight: 600; - transition: 0.2s; -} - -.btn-danger-outline:hover { - background: rgba(239, 68, 68, 0.1); - border-color: #ef4444; -} - -/* ========================================= - 8. ESTADOS DE CARGA Y VACÍO - ========================================= */ -.loading-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 4rem 0; - color: var(--color-text-secondary); -} - -.spinner { - width: 40px; - height: 40px; - border: 3px solid rgba(139, 92, 246, 0.1); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin-bottom: 1rem; -} - -.spinner.small { - width: 20px; - height: 20px; - border-width: 2px; - margin-bottom: 0; -} - -@keyframes spin { to { transform: rotate(360deg); } } - -.empty-state { - text-align: center; - padding: 5rem 2rem; - background: rgba(255,255,255,0.02); - border-radius: 20px; - border: 2px dashed rgba(255,255,255,0.05); -} - -.empty-state p { - font-size: 1.2rem; - color: var(--color-text-secondary); - margin-bottom: 1.5rem; -} - -/* ========================================= - 9. MOBILE RESPONSIVE - ========================================= */ -@media (max-width: 768px) { - .profile-hero { - padding: 6rem 1.5rem 1rem 1.5rem; - align-items: center; - text-align: center; - } - - .profile-content { - flex-direction: column; - align-items: center; - gap: 1.5rem; - } - - .profile-avatar-container { - width: 120px; - height: 120px; - } - - .profile-name { - font-size: 2.5rem; - } - - .hub-navigation { - justify-content: center; - padding-left: 0; - gap: 1.5rem; - font-size: 0.9rem; - } - - .toolbar { - flex-direction: column; - align-items: stretch; - } - - .search-box { - max-width: 100%; - } - - .filters-inline { - justify-content: space-between; - } - - .minimal-select { - flex: 1; - } - - .settings-grid { - grid-template-columns: 1fr; - } -} - -/* ========================================= - NUEVO DISEÑO DE PERFIL (Modern Header) + 2. PERFIL (Modern Header) ========================================= */ .profile-header { - background: var(--color-bg-elevated); - border-bottom: 1px solid var(--border-subtle); + background: var(--color-bg-elevated, #18181b); + border-bottom: 1px solid var(--border-subtle, rgba(255,255,255,0.05)); padding-bottom: 0; margin-bottom: 2rem; position: relative; @@ -781,19 +62,21 @@ z-index: 2; } +/* Avatar */ .profile-avatar-wrapper { position: relative; width: 160px; height: 160px; + flex-shrink: 0; } .avatar-img { width: 100%; height: 100%; border-radius: 50%; - border: 5px solid var(--color-bg-base); + border: 5px solid var(--color-bg-base, #09090b); object-fit: cover; - background: var(--color-bg-elevated); + background: var(--color-bg-elevated, #18181b); } .avatar-edit-overlay { @@ -815,6 +98,7 @@ opacity: 1; } +/* Detalles de Texto y Badges */ .profile-details { flex-grow: 1; display: flex; @@ -825,27 +109,32 @@ gap: 1rem; } +.username-wrapper { + display: flex; + align-items: center; + gap: 1rem; +} + .profile-text h1 { font-size: 2.5rem; margin: 0; line-height: 1.2; } -.user-badge { - background: rgba(139, 92, 246, 0.2); - color: #a78bfa; - border: 1px solid rgba(139, 92, 246, 0.3); - padding: 0.2rem 0.6rem; - border-radius: 6px; - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; +.header-anilist-badge { + width: 32px; + height: 32px; + border-radius: 8px; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; + display: flex; } -.profile-stats-grid { - display: flex; - gap: 1.5rem; -} +.header-anilist-badge img { width: 100%; height: 100%; object-fit: cover; } +.header-anilist-badge:hover { transform: scale(1.1); box-shadow: 0 0 15px rgba(61, 180, 242, 0.5); } + +/* Estadísticas */ +.profile-stats-grid { display: flex; gap: 1.5rem; } .stat-card { display: flex; @@ -857,20 +146,20 @@ border: 1px solid rgba(255,255,255,0.05); } -.stat-value { - font-size: 1.4rem; - font-weight: 800; - color: white; +.stat-value { font-size: 1.4rem; font-weight: 800; color: white; } +.stat-label { font-size: 0.8rem; color: var(--color-text-secondary, #a1a1aa); text-transform: uppercase; letter-spacing: 0.5px; } + +/* Responsive Profile */ +@media (max-width: 900px) { + .profile-body { flex-direction: column; align-items: center; margin-top: -100px; text-align: center; } + .profile-details { flex-direction: column; align-items: center; width: 100%; } + .profile-stats-grid { justify-content: center; width: 100%; } + .username-wrapper { justify-content: center; } } -.stat-label { - font-size: 0.8rem; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* NAVIGATION TABS (Modern Pills) */ +/* ========================================= + 3. NAVEGACIÓN (Pills Modernos) + ========================================= */ .hub-navigation-modern { display: flex; gap: 1rem; @@ -882,7 +171,7 @@ .nav-pill { background: transparent; border: none; - color: var(--color-text-secondary); + color: var(--color-text-secondary, #a1a1aa); padding: 0.6rem 1.2rem; font-size: 0.95rem; font-weight: 600; @@ -891,141 +180,528 @@ transition: 0.2s; } -.nav-pill:hover { - background: rgba(255,255,255,0.05); - color: white; +.nav-pill:hover { background: rgba(255,255,255,0.05); color: white; } +.nav-pill.active { background: var(--color-primary, #8b5cf6); color: white; } + +@media (max-width: 768px) { + .hub-navigation-modern { justify-content: center; padding-left: 1rem; padding-right: 1rem; flex-wrap: wrap; } } -.nav-pill.active { - background: var(--color-primary); - color: white; +.tab-section { + display: none; + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s, transform 0.3s; } -/* SETTINGS REDESIGN */ -.settings-layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - max-width: 1200px; - margin: 0 auto; +.tab-section.active { + display: block; + opacity: 1; + transform: translateY(0); } -.settings-card { - background: var(--color-bg-elevated); - border: 1px solid var(--border-medium); - padding: 2rem; - border-radius: 16px; - height: fit-content; -} - -.settings-stack { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.input-modern { - width: 100%; - background: var(--color-bg-base); - border: 1px solid rgba(255,255,255,0.1); - padding: 0.8rem 1rem; - border-radius: 8px; - color: white; - font-size: 0.95rem; - transition: 0.2s; -} - -.input-modern:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); -} - -.full-width { width: 100%; justify-content: center; margin-top: 1rem;} - -.avatar-options { +/* ========================================= + 4. TOOLBAR & CONTROLES + ========================================= */ +.toolbar { display: flex; + justify-content: space-between; align-items: center; - gap: 1rem; -} -.divider-text { color: var(--color-text-muted); font-size: 0.8rem; font-weight: bold; } - -/* Integration Item */ -.integration-item { - display: flex; - align-items: center; - gap: 1rem; - background: var(--color-bg-base); - padding: 1rem; + margin-bottom: 2.5rem; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.05); + padding: 0.75rem 1rem; border-radius: 12px; + backdrop-filter: blur(5px); + flex-wrap: wrap; + gap: 1rem; } -.int-icon { - width: 40px; height: 40px; border-radius: 8px; overflow: hidden; -} -.int-icon img { width: 100%; height: 100%; } - -.int-details { flex-grow: 1; display: flex; flex-direction: column; } -.int-details strong { font-size: 0.95rem; } -.int-details span { font-size: 0.8rem; color: var(--color-text-secondary); } - -.btn-sm { - padding: 0.4rem 1rem; - border-radius: 6px; - background: var(--color-bg-elevated); - border: 1px solid rgba(255,255,255,0.2); - color: white; - cursor: pointer; +/* Local Toolbar Variant */ +.toolbar.local-toolbar { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 1.5rem; } -.btn-danger { - background: rgba(239, 68, 68, 0.1); - color: #ef4444; - border: 1px solid rgba(239, 68, 68, 0.3); +.search-box { + position: relative; + flex-grow: 1; + max-width: 400px; +} + +.search-box svg { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-muted, #71717a); + pointer-events: none; +} + +.search-input { width: 100%; - padding: 0.8rem; + background: rgba(0,0,0,0.2); + border: 1px solid transparent; + padding: 0.7rem 1rem 0.7rem 2.8rem; border-radius: 8px; - font-weight: 700; + color: white; + font-family: inherit; + font-size: 0.95rem; + transition: all 0.2s; +} + +.search-input:focus { + background: rgba(0,0,0,0.4); + border-color: var(--color-primary, #8b5cf6); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); +} + +.filters-inline { + display: flex; + gap: 0.8rem; + align-items: center; + flex-wrap: wrap; +} + +.minimal-select { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.08); + color: var(--color-text-secondary, #a1a1aa); + padding: 0.6rem 2rem 0.6rem 1rem; + border-radius: 8px; + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + font-weight: 500; + transition: 0.2s; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23a1a1aa'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.7rem center; + background-size: 1rem; +} + +.minimal-select:hover { background: rgba(255,255,255,0.1); color: white; } +.minimal-select:focus { border-color: var(--color-primary, #8b5cf6); color: white; } + +.view-toggle { + display: flex; + background: rgba(0,0,0,0.3); + border-radius: 8px; + padding: 2px; +} + +.view-btn { + background: transparent; + border: none; + color: var(--color-text-muted, #71717a); + padding: 0.5rem 0.8rem; + border-radius: 6px; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; +} + +.view-btn.active, .view-btn:hover { background: rgba(255,255,255,0.1); color: white; } + +/* Switcher Local (Segmented Control) */ +.local-type-switcher { + display: flex; + background: rgba(0,0,0,0.3); + padding: 4px; + border-radius: 10px; + border: 1px solid rgba(255,255,255,0.05); +} + +.type-pill-btn { + background: transparent; + border: none; + color: var(--color-text-secondary, #a1a1aa); + padding: 0.5rem 1.2rem; + font-size: 0.9rem; + font-weight: 600; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; +} + +.type-pill-btn:hover { color: white; } +.type-pill-btn.active { + background: var(--color-bg-elevated, #18181b); + color: white; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); +} + +/* Action Buttons (Scan) */ +.actions-group { + display: flex; + gap: 0.5rem; + border-left: 1px solid rgba(255,255,255,0.1); + padding-left: 1.5rem; +} + +.action-icon-btn { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.1); + color: var(--color-text-secondary, #a1a1aa); + width: 40px; height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; transition: 0.2s; } -.btn-danger:hover { background: #ef4444; color: white; } -@media (max-width: 900px) { - .settings-layout { grid-template-columns: 1fr; } - .profile-body { flex-direction: column; align-items: center; margin-top: -100px; text-align: center; } - .profile-details { flex-direction: column; align-items: center; width: 100%; } - .profile-stats-grid { justify-content: center; width: 100%; } +.action-icon-btn:hover { background: var(--color-primary, #8b5cf6); border-color: var(--color-primary, #8b5cf6); color: white; } +.action-icon-btn.danger:hover { background: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #ef4444; } + +@media (max-width: 768px) { + .toolbar { flex-direction: column; align-items: stretch; } + .toolbar.local-toolbar { grid-template-columns: 1fr; gap: 1rem; } + .actions-group { border-left: none; padding-left: 0; justify-content: flex-end; } + .local-type-switcher, .type-pill-btn { width: 100%; flex: 1; } + .search-box { max-width: 100%; } + .filters-inline { justify-content: space-between; } + .minimal-select { flex: 1; } } -.username-wrapper { - display: flex; - align-items: center; - gap: 1rem; /* Espacio entre nombre e icono */ +/* ========================================= + 5. LIST GRID (Cards) + ========================================= */ +.list-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 2rem 1.5rem; } -.header-anilist-badge { - width: 32px; - height: 32px; +.list-item { + position: relative; + border-radius: 8px; + transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.3s; + cursor: pointer; +} + +.list-item:hover { transform: scale(1.05); z-index: 10; } + +.item-poster-link { + display: block; + position: relative; border-radius: 8px; overflow: hidden; - transition: transform 0.2s, box-shadow 0.2s; - display: flex; /* Para centrar la imagen */ + aspect-ratio: 2/3; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); } -.header-anilist-badge img { +.item-poster { width: 100%; height: 100%; object-fit: cover; + transition: filter 0.3s; } -.header-anilist-badge:hover { - transform: scale(1.1); - box-shadow: 0 0 15px rgba(61, 180, 242, 0.5); /* Color azul AniList */ +.list-item:hover .item-poster { filter: brightness(1.1); } + +.item-content { padding-top: 0.8rem; } + +.item-title { + font-size: 0.95rem; + font-weight: 700; + margin: 0 0 0.4rem 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: rgba(255,255,255,0.9); } -/* Ajuste Mobile */ +.list-item:hover .item-title { color: var(--color-primary, #8b5cf6); } + +.item-meta { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +.meta-pill { + font-size: 0.65rem; + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-pill { background: rgba(34, 197, 94, 0.15); color: #4ade80; } +.type-pill { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; } +.source-pill { background: rgba(59, 130, 246, 0.15); color: #60a5fa; } +.repeat-pill { background: rgba(234, 179, 8, 0.15); color: #facc15; } +.private-pill { background: rgba(239, 68, 68, 0.15); color: #f87171; } + +.progress-bar-container { + height: 4px; + background: rgba(255,255,255,0.1); + border-radius: 2px; + margin-bottom: 0.4rem; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: var(--color-primary, #8b5cf6); + border-radius: 2px; +} + +.progress-text { + font-size: 0.8rem; + color: var(--color-text-secondary, #a1a1aa); + display: flex; + justify-content: space-between; +} + +/* Edit Button (Hover) */ +.edit-icon-btn { + position: absolute; + top: 10px; right: 10px; + background: rgba(0,0,0,0.7); + backdrop-filter: blur(4px); + border: 1px solid rgba(255,255,255,0.2); + color: white; + width: 32px; height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translateY(-5px); + transition: all 0.2s; + z-index: 20; + cursor: pointer; +} + +.list-item:hover .edit-icon-btn { opacity: 1; transform: translateY(0); } +.edit-icon-btn:hover { background: var(--color-primary, #8b5cf6); border-color: var(--color-primary, #8b5cf6); } + +/* Badges adicionales */ +.unmatched-badge { + position: absolute; + top: 10px; left: 10px; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 4px 8px; + border-radius: 6px; + font-size: 0.7rem; + font-weight: 700; + color: #fbbf24; + display: flex; align-items: center; gap: 4px; +} + +.folder-path-tooltip { + font-size: 0.75rem; + color: var(--color-text-muted, #71717a); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.2rem; +} + +/* --- LIST VIEW MODE --- */ +.list-grid.list-view { grid-template-columns: 1fr; gap: 1rem; } + +.list-grid.list-view .list-item { + display: flex; + align-items: center; + background: var(--color-bg-elevated, #18181b); + padding: 1rem; + border: 1px solid rgba(255,255,255,0.05); +} + +.list-grid.list-view .list-item:hover { + transform: scale(1.01); + border-color: var(--color-primary, #8b5cf6); +} + +.list-grid.list-view .item-poster-link { width: 60px; aspect-ratio: 2/3; margin-right: 1.5rem; flex-shrink: 0; } +.list-grid.list-view .item-content { padding-top: 0; flex-grow: 1; display: flex; align-items: center; justify-content: space-between; } +.list-grid.list-view .item-title { font-size: 1.1rem; margin-bottom: 0.2rem; } +.list-grid.list-view .edit-icon-btn { position: static; opacity: 1; transform: none; background: transparent; border: 1px solid rgba(255,255,255,0.1); } + +/* ========================================= + 6. CONFIGURACIÓN (Stream Style) + ========================================= */ +.stream-settings-container { + max-width: 800px; + margin: 0 auto; + padding-top: 1rem; + color: #e5e5e5; +} + +.stream-section { + margin-bottom: 2.5rem; + padding-bottom: 2.5rem; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.stream-section.no-border { border-bottom: none; } + +.section-label { + font-size: 1.1rem; + color: #a1a1aa; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 1.5rem; + font-weight: 600; +} + +/* Row de Perfil */ +.stream-profile-row { display: flex; gap: 3rem; align-items: flex-start; } + +.stream-avatar-wrapper { + position: relative; + width: 120px; height: 120px; + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + background: #27272a; +} + +.stream-avatar-wrapper img { width: 100%; height: 100%; object-fit: cover; } + +.avatar-overlay { + position: absolute; inset: 0; + background: rgba(0,0,0,0.6); + display: flex; align-items: center; justify-content: center; + opacity: 0; cursor: pointer; transition: 0.2s; + font-weight: 600; text-transform: uppercase; font-size: 0.8rem; +} + +.stream-avatar-wrapper:hover .avatar-overlay { opacity: 1; } + +.stream-inputs-col { flex-grow: 1; display: flex; flex-direction: column; gap: 1.5rem; } + +/* Formularios */ +.stream-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: end; } + +.stream-input-group { display: flex; flex-direction: column; gap: 0.5rem; } +.stream-input-group label { font-size: 0.85rem; color: #d4d4d8; font-weight: 500; } + +.stream-input { + background: #27272a; + border: 1px solid transparent; + border-radius: 4px; + padding: 0.8rem 1rem; + color: white; + font-size: 1rem; + transition: 0.2s; +} + +.stream-input:focus { background: #3f3f46; outline: none; border-color: var(--color-primary, #8b5cf6); } + +/* Botones Settings */ +.stream-actions { margin-top: 0.5rem; } + +.btn-stream-primary { + background: white; color: black; + border: none; padding: 0.7rem 2rem; + font-weight: 700; border-radius: 4px; + cursor: pointer; transition: 0.2s; +} +.btn-stream-primary:hover { background: #d4d4d8; } + +.btn-stream-ghost { + background: transparent; border: 1px solid #52525b; + color: white; padding: 0.75rem 1.5rem; + border-radius: 4px; cursor: pointer; + font-weight: 600; height: 46px; +} +.btn-stream-ghost:hover { border-color: white; background: rgba(255,255,255,0.05); } + +/* Integraciones */ +.stream-integration-row { + display: flex; justify-content: space-between; align-items: center; + background: rgba(255,255,255,0.03); + padding: 1rem 1.5rem; + border-radius: 6px; +} + +.int-info { display: flex; align-items: center; gap: 1rem; } +.int-logo { width: 32px; height: 32px; } +.int-text { display: flex; flex-direction: column; } +.int-name { font-weight: 700; font-size: 1rem; } +.int-status { font-size: 0.85rem; color: #a1a1aa; } + +.btn-stream-outline { + background: transparent; border: none; + color: var(--color-primary, #8b5cf6); + font-weight: 600; cursor: pointer; font-size: 0.95rem; +} +.btn-stream-outline:hover { text-decoration: underline; } +.btn-danger-outline { color: #ef4444; border: 1px solid #ef4444; padding: 0.5rem 1rem; border-radius: 4px; } + +.link-danger { + background: transparent; border: none; + color: #ef4444; font-size: 1rem; + cursor: pointer; padding: 0; + text-decoration: underline; opacity: 0.8; +} +.link-danger:hover { opacity: 1; } + @media (max-width: 768px) { - .username-wrapper { - justify-content: center; - } -} \ No newline at end of file + .stream-profile-row { flex-direction: column; align-items: center; text-align: center; gap: 2rem; } + .stream-form-row { grid-template-columns: 1fr; } + .btn-stream-ghost { width: 100%; } + .stream-actions { display: flex; justify-content: center; } +} + +/* ========================================= + 7. UTILIDADES & ESTADOS (Loading/Console) + ========================================= */ +.loading-state { + display: flex; flex-direction: column; align-items: center; + justify-content: center; padding: 4rem 0; + color: var(--color-text-secondary, #a1a1aa); +} + +.spinner { + width: 40px; height: 40px; + border: 3px solid rgba(139, 92, 246, 0.1); + border-top-color: var(--color-primary, #8b5cf6); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 1rem; +} +.spinner.small { width: 20px; height: 20px; border-width: 2px; margin-bottom: 0; } +@keyframes spin { to { transform: rotate(360deg); } } + +.empty-state { + text-align: center; padding: 5rem 2rem; + background: rgba(255,255,255,0.02); + border-radius: 20px; + border: 2px dashed rgba(255,255,255,0.05); +} +.empty-state p { font-size: 1.2rem; color: var(--color-text-secondary, #a1a1aa); margin-bottom: 1.5rem; } + +.btn-blur { + background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.2); + color: white; padding: 0.8rem 1.5rem; + border-radius: 99px; text-decoration: none; + display: inline-block; transition: 0.2s; +} +.btn-blur:hover { background: white; color: black; } + +.console-output { + background: #09090b; border: 1px solid #27272a; + padding: 1.5rem; border-radius: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.9rem; color: #4ade80; + display: flex; align-items: center; gap: 1rem; + box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); + margin-bottom: 1.5rem; +} + +.hidden { display: none !important; } \ No newline at end of file diff --git a/desktop/views/dashboard.html b/desktop/views/dashboard.html index 5c91425..5ca795a 100644 --- a/desktop/views/dashboard.html +++ b/desktop/views/dashboard.html @@ -20,7 +20,7 @@ WaifuBoard
- +
@@ -31,10 +31,6 @@
Profile - -
@@ -77,7 +73,8 @@
@@ -98,81 +95,100 @@
-
-

Local Library

-
- - + +
+ + +
+ + + +
+ +
+ +
-
-

Anime Files

0

-

Manga Files

0

-

Novel Files

0

-
+ + +
+ +
-
-
-

Edit Profile

-
-
- - +
+ +
+ + + +
+ Avatar +
+ Edit +
+
-
- -
- - OR - +
+
+ + +
+ +
+
- - * If you upload a file, it will be converted automatically. The text box above is only for external URLs. -
-
-
-
-

Security

-
-
- - -
-
- - -
- -
-
- -
-

Integrations

-
-
AL
-
- AniList - Checking... -
- +
+ +
+
+ +
-
+
+ + +
+
+ +
+ +
-
-

Danger Zone

- +
+ +
+
+ +
+ AniList + Checking... +
+
+
+
diff --git a/docker/src/scripts/dashboard.js b/docker/src/scripts/dashboard.js index 1c7b341..8d3c445 100644 --- a/docker/src/scripts/dashboard.js +++ b/docker/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 = '
No matches found
'; - return; - } + if (list.length === 0) { + container.innerHTML = '
No matches found
'; + return; + } - const itemsToShow = filteredList.slice(0, visibleCount); - itemsToShow.forEach(item => container.appendChild(createListItem(item))); + const itemsToShow = list.slice(0, count); + itemsToShow.forEach(item => container.appendChild(this.createItemElement(item))); - if (visibleCount < filteredList.length) { - const remaining = filteredList.length - visibleCount; - const btnContainer = document.createElement('div'); - btnContainer.style.gridColumn = "1 / -1"; - btnContainer.style.display = "flex"; - btnContainer.style.justifyContent = "center"; - btnContainer.style.padding = "2rem 0"; + if (count < list.length) { + this.renderLoadMoreButton(container, list.length - count); + } + }, - const loadMoreBtn = document.createElement('button'); - loadMoreBtn.className = "btn-blur"; - loadMoreBtn.textContent = `Show All (${remaining} more)`; - loadMoreBtn.onclick = () => { visibleCount = filteredList.length; renderList(); }; - btnContainer.appendChild(loadMoreBtn); - container.appendChild(btnContainer); - } -} + createItemElement: function(item) { + const div = document.createElement('div'); + div.className = 'list-item'; -function createListItem(item) { - const div = document.createElement('div'); - div.className = 'list-item'; + const itemLink = this.getEntryLink(item); + const posterUrl = item.poster || '/public/assets/placeholder.svg'; + const progress = item.progress || 0; + const totalUnits = item.entry_type === 'ANIME' ? item.total_episodes || 0 : item.total_chapters || 0; + const progressPercent = totalUnits > 0 ? (progress / totalUnits) * 100 : 0; + const score = item.score ? item.score.toFixed(1) : null; + const entryType = (item.entry_type).toUpperCase(); - const itemLink = getEntryLink(item); - const posterUrl = item.poster || '/public/assets/placeholder.svg'; - const progress = item.progress || 0; - const totalUnits = item.entry_type === 'ANIME' ? item.total_episodes || 0 : item.total_chapters || 0; - 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(); - const unitLabel = entryType === 'ANIME' ? 'episodes' : 'chapters'; + const statusLabels = { + 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', + 'COMPLETED': 'Completed', 'PLANNING': 'Planning', 'PAUSED': 'Paused', + 'DROPPED': 'Dropped', 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' + }; - const statusLabels = { - 'CURRENT': entryType === 'ANIME' ? 'Watching' : 'Reading', - 'COMPLETED': 'Completed', - 'PLANNING': 'Planning', - 'PAUSED': 'Paused', - 'DROPPED': 'Dropped', - 'REPEATING': entryType === 'ANIME' ? 'Rewatching' : 'Rereading' - }; + const extraInfo = []; + if (item.repeat_count > 0) extraInfo.push(`🔁 ${item.repeat_count}`); + if (item.is_private) extraInfo.push('🔒 Private'); - const extraInfo = []; - if (repeatCount > 0) extraInfo.push(`🔁 ${repeatCount}`); - if (item.is_private) extraInfo.push('🔒 Private'); - - div.innerHTML = ` - - ${item.title || 'Entry'} - -
-
- -

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

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

${item.title || 'Unknown'}

+
+
+ ${statusLabels[item.status] || item.status} + ${entryType} + ${item.source.toUpperCase()} + ${extraInfo.join('')} +
+
+
+
+
+ ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} + ${score ? `⭐ ${score}` : ''} +
+
-
-
-
-
-
-
- ${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${unitLabel} ${score ? `⭐ ${score}` : ''} -
-
-
- - `; + + `; - // Lógica para abrir el Modal (Estilo book.js: seteamos currentData) - const editBtn = div.querySelector('.edit-icon-btn'); - editBtn.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); + div.querySelector('.edit-icon-btn').onclick = (e) => { + e.preventDefault(); e.stopPropagation(); + window.ListModalManager.currentData = item; + window.ListModalManager.isInList = true; + window.ListModalManager.currentEntry = item; + window.ListModalManager.open(item, item.source || 'anilist'); + }; - // 1. Configuramos el modal manager con los datos de ESTE item - window.ListModalManager.currentData = item; - window.ListModalManager.isInList = true; - window.ListModalManager.currentEntry = item; // Ya tenemos los datos de la lista + return div; + }, - // 2. Abrimos el modal - window.ListModalManager.open(item, item.source || 'anilist'); - }; + renderLoadMoreButton: function(container, remaining) { + const btnContainer = document.createElement('div'); + Object.assign(btnContainer.style, { gridColumn: "1 / -1", display: "flex", justifyContent: "center", padding: "2rem 0" }); - return div; -} + const loadMoreBtn = document.createElement('button'); + loadMoreBtn.className = "btn-blur"; + loadMoreBtn.textContent = `Show All (${remaining} more)`; + loadMoreBtn.onclick = () => { + DashboardApp.State.pagination.visibleCount = DashboardApp.State.filteredList.length; + this.render(); + }; -function getEntryLink(item) { - const isAnime = item.entry_type?.toUpperCase() === 'ANIME'; - const baseRoute = isAnime ? '/anime' : '/book'; - return `${baseRoute}/${item.entry_id}`; -} + btnContainer.appendChild(loadMoreBtn); + container.appendChild(btnContainer); + }, -// ========================================================= -// EXPORTS GLOBALES (Estilo book.js) -// Estas funciones son llamadas por los onclick del HTML del Modal -// ========================================================= + getEntryLink: function(item) { + const baseRoute = (item.entry_type?.toUpperCase() === 'ANIME') ? '/anime' : '/book'; + return `${baseRoute}/${item.entry_id}`; + } + }, + + Library: { + loadStats: async function() { + 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) { + const data = await res.json(); + const el = document.getElementById(elements[type]); + if (el) el.textContent = `${data.length} items`; + } + } catch (e) { console.error(e); } + } + }, + + loadContent: async function(type) { + DashboardApp.State.currentLocalType = type; + const container = document.getElementById('local-list-container'); + const loading = document.getElementById('local-loading'); + const searchInput = document.getElementById('local-search-input'); + + container.innerHTML = ''; + loading.style.display = 'flex'; + if(searchInput) searchInput.value = ''; + + try { + const res = await fetch(`${API_BASE}/library/${type}`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); + if (!res.ok) throw new Error('Failed to load local content'); + + const data = await res.json(); + DashboardApp.State.localLibraryData = data; + this.renderGrid(data, type); + } catch (err) { + console.error(err); + container.innerHTML = `

Error loading library

`; + } finally { + loading.style.display = 'none'; + } + }, + + renderGrid: function(entries, type) { + const container = document.getElementById('local-list-container'); + container.innerHTML = ''; + + if (entries.length === 0) { + container.innerHTML = ` +
+

No ${type} files found.

+ +
`; + return; + } + + entries.forEach(entry => { + const isMatched = entry.matched && entry.metadata; + const meta = entry.metadata || {}; + let poster = meta.coverImage?.large || '/public/assets/placeholder.svg'; + + let title = isMatched ? (meta.title?.english || meta.title?.romaji) : entry.folder_name; + if (!isMatched) title = title.replace(/\[.*?\]|\(.*?\)|\.mkv|\.mp4/g, '').trim(); + + const url = isMatched ? (type === 'anime' ? `/anime/${meta.id}` : `/book/${meta.id}`) : '#'; + + const div = document.createElement('div'); + div.className = 'list-item'; + div.innerHTML = ` + +
+

${title}

+
+ ${entry.files ? entry.files.length : 0} FILES + ${isMatched ? 'MATCHED' : ''} +
+
${entry.folder_name}
+
+ + `; + container.appendChild(div); + }); + }, + + filterContent: function(query) { + if (!DashboardApp.State.localLibraryData) return; + const lowerQuery = query.toLowerCase(); + const filtered = DashboardApp.State.localLibraryData.filter(entry => { + const metaTitle = entry.metadata?.title?.english || entry.metadata?.title?.romaji || ''; + const folderName = entry.folder_name || ''; + return metaTitle.toLowerCase().includes(lowerQuery) || folderName.toLowerCase().includes(lowerQuery); + }); + this.renderGrid(filtered, DashboardApp.State.currentLocalType); + }, + + triggerScan: async function(mode) { + const consoleDiv = document.getElementById('scan-console'); + const statusText = document.getElementById('scan-status-text'); + if(consoleDiv) consoleDiv.style.display = 'flex'; + if(statusText) 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) { + if(statusText) statusText.textContent = "Scan completed successfully!"; + setTimeout(() => { if(consoleDiv) consoleDiv.style.display = 'none'; this.loadStats(); }, 3000); + } else throw new Error('Scan failed'); + } catch (e) { + if(statusText) { statusText.textContent = "Error during scan."; statusText.style.color = 'var(--color-danger)'; } + } + }, + + openManualMatch: function(id, type) { + const newId = prompt("Enter AniList ID to force match:"); + if (newId) { + fetch(`${API_BASE}/library/${type}/${id}/match`, { + method: 'POST', + headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ source: 'anilist', matched_id: parseInt(newId) }) + }).then(res => { + if(res.ok) { alert("Matched! Refreshing..."); this.loadContent(type); } + else { alert("Failed to match."); } + }); + } + }, + + switchType: function(type, btnElement) { + document.querySelectorAll('.type-pill-btn').forEach(b => b.classList.remove('active')); + if(btnElement) btnElement.classList.add('active'); + this.loadContent(type); + } + }, + + UI: { + setupTabSystem: function() { + 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') { + DashboardApp.Library.loadStats(); + DashboardApp.Library.loadContent('anime'); + } + }); + }); + } + }, + + Utils: { + fileToBase64: (file) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + }) + } +}; + +document.addEventListener('DOMContentLoaded', () => { + DashboardApp.init(); +}); + +window.switchLocalType = (type, btn) => DashboardApp.Library.switchType(type, btn); window.saveToList = async () => { - // Recuperamos los datos que seteamos en el onclick del botón editar const data = window.ListModalManager.currentData; if (!data) return; - - // En la vista de lista, el ID suele ser 'entry_id', pero usamos un fallback const idToSave = data.entry_id || data.id; - const source = data.source || 'anilist'; - - // Llamamos al manager pasando ID y Source explícitamente como en book.js - await window.ListModalManager.save(idToSave, source); - - // IMPORTANTE: Recargar la lista para ver los cambios - await loadList(); + await window.ListModalManager.save(idToSave, data.source || 'anilist'); + await DashboardApp.Tracking.load(); }; window.deleteFromList = async () => { const data = window.ListModalManager.currentData; if (!data) return; - const idToDelete = data.entry_id || data.id; - const source = data.source || 'anilist'; - - await window.ListModalManager.delete(idToDelete, source); - - // IMPORTANTE: Recargar la lista - await loadList(); + await window.ListModalManager.delete(idToDelete, data.source || 'anilist'); + await DashboardApp.Tracking.load(); }; -window.closeAddToListModal = () => { - window.ListModalManager.close(); -}; \ No newline at end of file +window.closeAddToListModal = () => window.ListModalManager.close(); \ No newline at end of file diff --git a/docker/src/scripts/local-library-books.js b/docker/src/scripts/local-library-books.js deleted file mode 100644 index 3ab311c..0000000 --- a/docker/src/scripts/local-library-books.js +++ /dev/null @@ -1,106 +0,0 @@ -let activeFilter = 'all'; -let activeSort = 'az'; -let isLocalMode = false; -let localEntries = []; - -function toggleLibraryMode() { - isLocalMode = !isLocalMode; - const btn = document.getElementById('library-mode-btn'); - const onlineContent = document.getElementById('online-content'); - const localContent = document.getElementById('local-content'); - - if (isLocalMode) { - btn.classList.add('active'); - onlineContent.classList.add('hidden'); - localContent.classList.remove('hidden'); - loadLocalEntries(); - } else { - btn.classList.remove('active'); - onlineContent.classList.remove('hidden'); - localContent.classList.add('hidden'); - } -} - -async function loadLocalEntries() { - const grid = document.getElementById('local-entries-grid'); - grid.innerHTML = '
'.repeat(6); - - try { - const [mangaRes, novelRes] = await Promise.all([ - fetch('/api/library/manga'), - fetch('/api/library/novels') - ]); - - const [manga, novel] = await Promise.all([ - mangaRes.json(), - novelRes.json() - ]); - - localEntries = [ - ...manga.map(e => ({ ...e, type: 'manga' })), - ...novel.map(e => ({ ...e, type: 'novel' })) - ]; - - if (localEntries.length === 0) { - grid.innerHTML = '

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 ` -
-
- ${title} -
-
-
${title}
-

- ${chapters} Chapters -

-
${entry.type}
-
- ${entry.matched ? '● Linked' : '○ Unlinked'} -
-
-
- `; - }).join(''); -} - -async function scanLocalLibrary() { - const btnText = document.getElementById('scan-text'); - btnText.innerText = "Scanning..."; - try { - // Asumiendo que el scan de libros usa este query param - const response = await fetch('/api/library/scan?mode=incremental', { method: 'POST' }); - if (response.ok) { - await loadLocalEntries(); - if (window.NotificationUtils) NotificationUtils.show('Library scanned!', 'success'); - } - } catch (err) { - if (window.NotificationUtils) NotificationUtils.show('Scan failed', 'error'); - } finally { - btnText.innerText = "Scan Library"; - } -} - -function viewLocalEntry(id) { - if (id) window.location.href = `/book/${id}`; -} \ No newline at end of file diff --git a/docker/src/scripts/local-library.js b/docker/src/scripts/local-library.js deleted file mode 100644 index 76c89aa..0000000 --- a/docker/src/scripts/local-library.js +++ /dev/null @@ -1,209 +0,0 @@ -let activeFilter = 'all'; -let activeSort = 'az'; -let isLocalMode = false; -let localEntries = []; - -function toggleLibraryMode() { - isLocalMode = !isLocalMode; - - const btn = document.getElementById('library-mode-btn'); - const onlineContent = document.getElementById('online-content'); - const localContent = document.getElementById('local-content'); - const svg = btn.querySelector('svg'); - const label = btn.querySelector('span'); - - if (isLocalMode) { - // LOCAL MODE - btn.classList.add('active'); - onlineContent.classList.add('hidden'); - localContent.classList.remove('hidden'); - loadLocalEntries(); - - svg.innerHTML = ` - - - `; - } else { - // ONLINE MODE - btn.classList.remove('active'); - onlineContent.classList.remove('hidden'); - localContent.classList.add('hidden'); - - svg.innerHTML = ` - - - - `; - } -} - -async function loadLocalEntries() { - const grid = document.getElementById('local-entries-grid'); - grid.innerHTML = '
'.repeat(8); - - try { - const response = await fetch('/api/library/anime'); - const entries = await response.json(); - localEntries = entries; - - if (entries.length === 0) { - grid.innerHTML = '

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 ` -
-
- ${title} -
-
-
${title}
-

- ${score}% • ${episodes} Eps -

-
- ${entry.matched ? '● Linked' : '○ Unlinked'} -
-
-
- `; - }).join(''); - } catch (err) { - console.error('Error loading local entries:', err); - grid.innerHTML = '

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 ` -
-
- ${title} -
-
-
${title}
-

- ${score}% • ${episodes} Eps -

-
- ${entry.matched ? '● Linked' : '○ Unlinked'} -
-
-
- `; - }).join(''); -} - -function applyLocalFilters() { - let filtered = [...localEntries]; - - if (activeFilter === 'linked') { - filtered = filtered.filter(e => e.matched); - } - - if (activeFilter === 'unlinked') { - filtered = filtered.filter(e => !e.matched); - } - - if (activeSort === 'az') { - filtered.sort((a, b) => - (a.metadata?.title?.romaji || a.id) - .localeCompare(b.metadata?.title?.romaji || b.id) - ); - } - - if (activeSort === 'za') { - filtered.sort((a, b) => - (b.metadata?.title?.romaji || b.id) - .localeCompare(a.metadata?.title?.romaji || a.id) - ); - } - - renderLocalEntries(filtered); -} - -document.addEventListener('click', e => { - const btn = e.target.closest('.filter-btn'); - if (!btn) return; - - if (btn.dataset.filter) { - activeFilter = btn.dataset.filter; - } - - if (btn.dataset.sort) { - activeSort = btn.dataset.sort; - } - - btn - .closest('.local-filters') - .querySelectorAll('.filter-btn') - .forEach(b => b.classList.remove('active')); - - btn.classList.add('active'); - - applyLocalFilters(); -}); diff --git a/docker/views/anime/animes.html b/docker/views/anime/animes.html index 977c285..efc15c8 100644 --- a/docker/views/anime/animes.html +++ b/docker/views/anime/animes.html @@ -13,6 +13,7 @@ +
@@ -103,45 +104,6 @@
- -
-
-
-
Local Anime Library
- -
-
-
- - - - - -
- -
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
Local Books Library
- -
-
-
- - -
-
- -
-
-
-
-
-
-
-
@@ -119,7 +87,6 @@ - diff --git a/docker/views/css/dashboard.css b/docker/views/css/dashboard.css index 19d193b..f46e5cf 100644 --- a/docker/views/css/dashboard.css +++ b/docker/views/css/dashboard.css @@ -9,13 +9,11 @@ overflow-x: hidden; } -/* Efecto de fondo ambiental global (Glow superior) */ +/* Efecto de fondo ambiental (Glow superior) */ .main-wrapper::before { content: ''; position: absolute; - top: 0; - left: 0; - right: 0; + top: 0; left: 0; right: 0; height: 60vh; background: radial-gradient(circle at 50% 0%, rgba(139, 92, 246, 0.15), transparent 70%); z-index: -1; @@ -35,728 +33,11 @@ } /* ========================================= - 2. PROFILE HERO SECTION (Estilo Apple TV Profile) - ========================================= */ -.profile-hero { - position: relative; - padding: 8rem 3rem 2rem 3rem; /* Espacio extra arriba para titlebar/navbar */ - margin-bottom: 2rem; - display: flex; - flex-direction: column; - justify-content: flex-end; -} - -.profile-content { - display: flex; - align-items: flex-end; - gap: 2.5rem; - position: relative; - z-index: 2; -} - -.profile-avatar-container { - position: relative; - width: 160px; - height: 160px; - flex-shrink: 0; -} - -.profile-avatar { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - border: 4px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); - transition: transform 0.3s ease; -} - -.profile-avatar:hover { - transform: scale(1.02); - border-color: var(--color-primary); -} - -.edit-avatar-btn { - position: absolute; - bottom: 5px; - right: 5px; - background: var(--color-bg-elevated); - border: 1px solid rgba(255,255,255,0.2); - color: white; - width: 40px; - height: 40px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 4px 10px rgba(0,0,0,0.3); - transition: all 0.2s; -} - -.edit-avatar-btn:hover { - background: var(--color-primary); - transform: scale(1.1); -} - -.profile-info { - margin-bottom: 1.5rem; -} - -.profile-name { - font-size: 3.5rem; - font-weight: 800; - margin: 0 0 0.5rem 0; - line-height: 1; - letter-spacing: -0.02em; - background: linear-gradient(to bottom right, #fff, #a1a1aa); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -.profile-stats-pills { - display: flex; - gap: 0.8rem; - flex-wrap: wrap; -} - -.stat-pill { - background: rgba(255, 255, 255, 0.06); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - padding: 0.4rem 1rem; - border-radius: 99px; - font-size: 0.85rem; - font-weight: 600; - color: var(--color-text-secondary); - display: flex; - align-items: center; - gap: 0.5rem; - transition: 0.2s; -} - -.stat-pill:hover { - background: rgba(255, 255, 255, 0.1); - color: white; -} - -.stat-pill.highlight { - background: rgba(139, 92, 246, 0.15); - color: #a78bfa; - border-color: rgba(139, 92, 246, 0.3); -} - -/* ========================================= - 3. NAVEGACIÓN (Tabs) - ========================================= */ -.hub-navigation { - display: flex; - gap: 2.5rem; - margin-top: 3rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - padding-left: 3rem; -} - -.nav-tab { - background: none; - border: none; - color: var(--color-text-muted); - font-size: 1.1rem; - font-weight: 600; - padding: 1rem 0; - cursor: pointer; - position: relative; - display: flex; - align-items: center; - gap: 0.6rem; - transition: color 0.3s ease; -} - -.nav-tab svg { - opacity: 0.7; - transition: 0.3s; -} - -.nav-tab:hover { - color: white; -} - -.nav-tab:hover svg { - opacity: 1; -} - -.nav-tab.active { - color: white; -} - -.nav-tab.active::after { - content: ''; - position: absolute; - bottom: -1px; - left: 0; - width: 100%; - height: 3px; - background: var(--color-primary); - border-radius: 3px 3px 0 0; - box-shadow: 0 -4px 15px var(--color-primary-glow); -} - -.tab-section { - display: none; - opacity: 0; - transform: translateY(10px); - transition: opacity 0.3s, transform 0.3s; -} - -.tab-section.active { - display: block; - opacity: 1; - transform: translateY(0); -} - -/* ========================================= - 4. TOOLBAR & FILTROS - ========================================= */ -.toolbar { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2.5rem; - background: rgba(255,255,255,0.03); - border: 1px solid rgba(255,255,255,0.05); - padding: 0.75rem 1rem; - border-radius: 12px; - backdrop-filter: blur(5px); - flex-wrap: wrap; - gap: 1rem; -} - -.search-box { - position: relative; - flex-grow: 1; - max-width: 400px; -} - -.search-box svg { - position: absolute; - left: 14px; - top: 50%; - transform: translateY(-50%); - color: var(--color-text-muted); - pointer-events: none; -} - -.search-input { - width: 100%; - background: rgba(0,0,0,0.2); - border: 1px solid transparent; - padding: 0.7rem 1rem 0.7rem 2.8rem; - border-radius: 8px; - color: white; - font-family: inherit; - font-size: 0.95rem; - transition: all 0.2s; -} - -.search-input:focus { - background: rgba(0,0,0,0.4); - border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); -} - -.filters-inline { - display: flex; - gap: 0.8rem; - align-items: center; - flex-wrap: wrap; -} - -.minimal-select { - background: rgba(255,255,255,0.05); - border: 1px solid rgba(255,255,255,0.08); - color: var(--color-text-secondary); - padding: 0.6rem 2rem 0.6rem 1rem; /* Padding derecho para la flecha */ - border-radius: 8px; - cursor: pointer; - font-family: inherit; - font-size: 0.9rem; - font-weight: 500; - transition: 0.2s; - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23a1a1aa'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.7rem center; - background-size: 1rem; -} - -.minimal-select:hover { - background: rgba(255,255,255,0.1); - color: white; -} - -.minimal-select:focus { - border-color: var(--color-primary); - color: white; -} - -.view-toggle { - display: flex; - background: rgba(0,0,0,0.3); - border-radius: 8px; - padding: 2px; -} - -.view-btn { - background: transparent; - border: none; - color: var(--color-text-muted); - padding: 0.5rem 0.8rem; - border-radius: 6px; - cursor: pointer; - transition: 0.2s; - font-size: 1.1rem; - line-height: 1; -} - -.view-btn:hover { - color: white; -} - -.view-btn.active { - background: rgba(255,255,255,0.1); - color: white; -} - -/* ========================================= - 5. LIST GRID (Estilo Netflix Cards) - ========================================= */ -.list-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 2rem 1.5rem; -} - -.list-item { - position: relative; - border-radius: 8px; - transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.3s; - cursor: pointer; - background: transparent; -} - -.list-item:hover { - transform: scale(1.05); - z-index: 10; -} - -/* Poster */ -.item-poster-link { - display: block; - position: relative; - border-radius: 8px; - overflow: hidden; - aspect-ratio: 2/3; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); -} - -.item-poster { - width: 100%; - height: 100%; - object-fit: cover; - transition: filter 0.3s; -} - -/* Oscurecer ligeramente la imagen en hover para destacar texto si se desea, o brillo */ -.list-item:hover .item-poster { - filter: brightness(1.1); -} - -/* Contenido debajo del poster (Título, etc) */ -.item-content { - padding-top: 0.8rem; -} - -.item-title { - font-size: 0.95rem; - font-weight: 700; - margin: 0 0 0.4rem 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: rgba(255,255,255,0.9); -} - -.list-item:hover .item-title { - color: var(--color-primary); -} - -.item-meta { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; - margin-bottom: 0.5rem; -} - -.meta-pill { - font-size: 0.65rem; - padding: 0.15rem 0.4rem; - border-radius: 4px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.status-pill { background: rgba(34, 197, 94, 0.15); color: #4ade80; } -.type-pill { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; } - -.progress-text { - font-size: 0.8rem; - color: var(--color-text-secondary); - display: flex; - justify-content: space-between; -} - -/* Botón Editar flotante (solo visible en hover) */ -.edit-icon-btn { - position: absolute; - top: 10px; - right: 10px; - background: rgba(0,0,0,0.7); - backdrop-filter: blur(4px); - border: 1px solid rgba(255,255,255,0.2); - color: white; - width: 32px; - height: 32px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - transform: translateY(-5px); - transition: all 0.2s; - z-index: 20; -} - -.list-item:hover .edit-icon-btn { - opacity: 1; - transform: translateY(0); -} - -.edit-icon-btn:hover { - background: var(--color-primary); - border-color: var(--color-primary); -} - -/* --- LIST VIEW MODE --- */ -.list-grid.list-view { - grid-template-columns: 1fr; - gap: 1rem; -} - -.list-grid.list-view .list-item { - display: flex; - align-items: center; - background: var(--color-bg-elevated); - padding: 1rem; - border: 1px solid rgba(255,255,255,0.05); -} - -.list-grid.list-view .list-item:hover { - transform: scale(1.01); - border-color: var(--color-primary); - background: var(--color-bg-elevated-hover); -} - -.list-grid.list-view .item-poster-link { - width: 60px; - aspect-ratio: 2/3; - margin-right: 1.5rem; - flex-shrink: 0; -} - -.list-grid.list-view .item-content { - padding-top: 0; - flex-grow: 1; - display: flex; - align-items: center; - justify-content: space-between; -} - -.list-grid.list-view .item-title { - font-size: 1.1rem; - margin-bottom: 0.2rem; -} - -.list-grid.list-view .edit-icon-btn { - position: static; - opacity: 1; - transform: none; - background: transparent; - border: 1px solid rgba(255,255,255,0.1); -} - -/* ========================================= - 6. LOCAL LIBRARY (Dashboard Widgets) - ========================================= */ -.local-stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.local-card { - background: var(--color-bg-elevated); - border: 1px solid rgba(255,255,255,0.05); - padding: 1.5rem; - border-radius: 16px; - position: relative; - overflow: hidden; - transition: 0.3s; -} - -.local-card::after { - content: ''; - position: absolute; - top: 0; right: 0; width: 100px; height: 100px; - background: radial-gradient(circle at top right, rgba(139, 92, 246, 0.1), transparent 70%); -} - -.local-card:hover { - border-color: rgba(139, 92, 246, 0.3); - transform: translateY(-5px); -} - -.local-card h3 { - margin: 0; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 1px; - color: var(--color-text-secondary); -} - -.local-card p { - font-size: 2.5rem; - font-weight: 800; - margin: 0.5rem 0 0 0; - color: white; -} - -.section-header-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2rem; - padding-bottom: 1rem; - border-bottom: 1px solid rgba(255,255,255,0.05); -} - -.console-output { - background: #09090b; - border: 1px solid #27272a; - padding: 1.5rem; - border-radius: 12px; - font-family: 'JetBrains Mono', monospace; - font-size: 0.9rem; - color: #4ade80; - display: flex; - align-items: center; - gap: 1rem; - box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); -} - -/* ========================================= - 7. SETTINGS (Clean Forms) - ========================================= */ -.settings-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - gap: 2rem; -} - -.settings-card { - background: rgba(20, 20, 23, 0.6); - backdrop-filter: blur(10px); - border: 1px solid rgba(255,255,255,0.08); - padding: 2.5rem; - border-radius: 20px; -} - -.settings-card.full-width { - grid-column: 1 / -1; -} - -.settings-card h3 { - margin-top: 0; - font-size: 1.3rem; - border-bottom: 1px solid rgba(255,255,255,0.05); - padding-bottom: 1rem; - margin-bottom: 1.5rem; -} - -.form-group { - margin-bottom: 1.5rem; -} - -.form-group label { - display: block; - margin-bottom: 0.6rem; - font-size: 0.9rem; - color: var(--color-text-secondary); - font-weight: 500; -} - -.input-field { - width: 100%; - background: rgba(0,0,0,0.2); - border: 1px solid rgba(255,255,255,0.1); - padding: 0.9rem 1rem; - border-radius: 10px; - color: white; - font-size: 1rem; - transition: 0.2s; -} - -.input-field:focus { - background: rgba(0,0,0,0.4); - border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); -} - -.integration-row { - display: flex; - justify-content: space-between; - align-items: center; - background: rgba(255,255,255,0.03); - padding: 1.5rem; - border-radius: 12px; - border: 1px solid rgba(255,255,255,0.05); -} - -/* Botones específicos de settings */ -.btn-danger-outline { - background: transparent; - border: 1px solid rgba(239, 68, 68, 0.5); - color: #ef4444; - padding: 0.8rem 1.5rem; - border-radius: 99px; - cursor: pointer; - font-weight: 600; - transition: 0.2s; -} - -.btn-danger-outline:hover { - background: rgba(239, 68, 68, 0.1); - border-color: #ef4444; -} - -/* ========================================= - 8. ESTADOS DE CARGA Y VACÍO - ========================================= */ -.loading-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 4rem 0; - color: var(--color-text-secondary); -} - -.spinner { - width: 40px; - height: 40px; - border: 3px solid rgba(139, 92, 246, 0.1); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin-bottom: 1rem; -} - -.spinner.small { - width: 20px; - height: 20px; - border-width: 2px; - margin-bottom: 0; -} - -@keyframes spin { to { transform: rotate(360deg); } } - -.empty-state { - text-align: center; - padding: 5rem 2rem; - background: rgba(255,255,255,0.02); - border-radius: 20px; - border: 2px dashed rgba(255,255,255,0.05); -} - -.empty-state p { - font-size: 1.2rem; - color: var(--color-text-secondary); - margin-bottom: 1.5rem; -} - -/* ========================================= - 9. MOBILE RESPONSIVE - ========================================= */ -@media (max-width: 768px) { - .profile-hero { - padding: 6rem 1.5rem 1rem 1.5rem; - align-items: center; - text-align: center; - } - - .profile-content { - flex-direction: column; - align-items: center; - gap: 1.5rem; - } - - .profile-avatar-container { - width: 120px; - height: 120px; - } - - .profile-name { - font-size: 2.5rem; - } - - .hub-navigation { - justify-content: center; - padding-left: 0; - gap: 1.5rem; - font-size: 0.9rem; - } - - .toolbar { - flex-direction: column; - align-items: stretch; - } - - .search-box { - max-width: 100%; - } - - .filters-inline { - justify-content: space-between; - } - - .minimal-select { - flex: 1; - } - - .settings-grid { - grid-template-columns: 1fr; - } -} - -/* ========================================= - NUEVO DISEÑO DE PERFIL (Modern Header) + 2. PERFIL (Modern Header) ========================================= */ .profile-header { - background: var(--color-bg-elevated); - border-bottom: 1px solid var(--border-subtle); + background: var(--color-bg-elevated, #18181b); + border-bottom: 1px solid var(--border-subtle, rgba(255,255,255,0.05)); padding-bottom: 0; margin-bottom: 2rem; position: relative; @@ -781,19 +62,21 @@ z-index: 2; } +/* Avatar */ .profile-avatar-wrapper { position: relative; width: 160px; height: 160px; + flex-shrink: 0; } .avatar-img { width: 100%; height: 100%; border-radius: 50%; - border: 5px solid var(--color-bg-base); + border: 5px solid var(--color-bg-base, #09090b); object-fit: cover; - background: var(--color-bg-elevated); + background: var(--color-bg-elevated, #18181b); } .avatar-edit-overlay { @@ -815,6 +98,7 @@ opacity: 1; } +/* Detalles de Texto y Badges */ .profile-details { flex-grow: 1; display: flex; @@ -825,27 +109,32 @@ gap: 1rem; } +.username-wrapper { + display: flex; + align-items: center; + gap: 1rem; +} + .profile-text h1 { font-size: 2.5rem; margin: 0; line-height: 1.2; } -.user-badge { - background: rgba(139, 92, 246, 0.2); - color: #a78bfa; - border: 1px solid rgba(139, 92, 246, 0.3); - padding: 0.2rem 0.6rem; - border-radius: 6px; - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; +.header-anilist-badge { + width: 32px; + height: 32px; + border-radius: 8px; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; + display: flex; } -.profile-stats-grid { - display: flex; - gap: 1.5rem; -} +.header-anilist-badge img { width: 100%; height: 100%; object-fit: cover; } +.header-anilist-badge:hover { transform: scale(1.1); box-shadow: 0 0 15px rgba(61, 180, 242, 0.5); } + +/* Estadísticas */ +.profile-stats-grid { display: flex; gap: 1.5rem; } .stat-card { display: flex; @@ -857,20 +146,20 @@ border: 1px solid rgba(255,255,255,0.05); } -.stat-value { - font-size: 1.4rem; - font-weight: 800; - color: white; +.stat-value { font-size: 1.4rem; font-weight: 800; color: white; } +.stat-label { font-size: 0.8rem; color: var(--color-text-secondary, #a1a1aa); text-transform: uppercase; letter-spacing: 0.5px; } + +/* Responsive Profile */ +@media (max-width: 900px) { + .profile-body { flex-direction: column; align-items: center; margin-top: -100px; text-align: center; } + .profile-details { flex-direction: column; align-items: center; width: 100%; } + .profile-stats-grid { justify-content: center; width: 100%; } + .username-wrapper { justify-content: center; } } -.stat-label { - font-size: 0.8rem; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* NAVIGATION TABS (Modern Pills) */ +/* ========================================= + 3. NAVEGACIÓN (Pills Modernos) + ========================================= */ .hub-navigation-modern { display: flex; gap: 1rem; @@ -882,7 +171,7 @@ .nav-pill { background: transparent; border: none; - color: var(--color-text-secondary); + color: var(--color-text-secondary, #a1a1aa); padding: 0.6rem 1.2rem; font-size: 0.95rem; font-weight: 600; @@ -891,141 +180,528 @@ transition: 0.2s; } -.nav-pill:hover { - background: rgba(255,255,255,0.05); - color: white; +.nav-pill:hover { background: rgba(255,255,255,0.05); color: white; } +.nav-pill.active { background: var(--color-primary, #8b5cf6); color: white; } + +@media (max-width: 768px) { + .hub-navigation-modern { justify-content: center; padding-left: 1rem; padding-right: 1rem; flex-wrap: wrap; } } -.nav-pill.active { - background: var(--color-primary); - color: white; +.tab-section { + display: none; + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s, transform 0.3s; } -/* SETTINGS REDESIGN */ -.settings-layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - max-width: 1200px; - margin: 0 auto; +.tab-section.active { + display: block; + opacity: 1; + transform: translateY(0); } -.settings-card { - background: var(--color-bg-elevated); - border: 1px solid var(--border-medium); - padding: 2rem; - border-radius: 16px; - height: fit-content; -} - -.settings-stack { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.input-modern { - width: 100%; - background: var(--color-bg-base); - border: 1px solid rgba(255,255,255,0.1); - padding: 0.8rem 1rem; - border-radius: 8px; - color: white; - font-size: 0.95rem; - transition: 0.2s; -} - -.input-modern:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); -} - -.full-width { width: 100%; justify-content: center; margin-top: 1rem;} - -.avatar-options { +/* ========================================= + 4. TOOLBAR & CONTROLES + ========================================= */ +.toolbar { display: flex; + justify-content: space-between; align-items: center; - gap: 1rem; -} -.divider-text { color: var(--color-text-muted); font-size: 0.8rem; font-weight: bold; } - -/* Integration Item */ -.integration-item { - display: flex; - align-items: center; - gap: 1rem; - background: var(--color-bg-base); - padding: 1rem; + margin-bottom: 2.5rem; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.05); + padding: 0.75rem 1rem; border-radius: 12px; + backdrop-filter: blur(5px); + flex-wrap: wrap; + gap: 1rem; } -.int-icon { - width: 40px; height: 40px; border-radius: 8px; overflow: hidden; -} -.int-icon img { width: 100%; height: 100%; } - -.int-details { flex-grow: 1; display: flex; flex-direction: column; } -.int-details strong { font-size: 0.95rem; } -.int-details span { font-size: 0.8rem; color: var(--color-text-secondary); } - -.btn-sm { - padding: 0.4rem 1rem; - border-radius: 6px; - background: var(--color-bg-elevated); - border: 1px solid rgba(255,255,255,0.2); - color: white; - cursor: pointer; +/* Local Toolbar Variant */ +.toolbar.local-toolbar { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 1.5rem; } -.btn-danger { - background: rgba(239, 68, 68, 0.1); - color: #ef4444; - border: 1px solid rgba(239, 68, 68, 0.3); +.search-box { + position: relative; + flex-grow: 1; + max-width: 400px; +} + +.search-box svg { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-muted, #71717a); + pointer-events: none; +} + +.search-input { width: 100%; - padding: 0.8rem; + background: rgba(0,0,0,0.2); + border: 1px solid transparent; + padding: 0.7rem 1rem 0.7rem 2.8rem; border-radius: 8px; - font-weight: 700; + color: white; + font-family: inherit; + font-size: 0.95rem; + transition: all 0.2s; +} + +.search-input:focus { + background: rgba(0,0,0,0.4); + border-color: var(--color-primary, #8b5cf6); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); +} + +.filters-inline { + display: flex; + gap: 0.8rem; + align-items: center; + flex-wrap: wrap; +} + +.minimal-select { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.08); + color: var(--color-text-secondary, #a1a1aa); + padding: 0.6rem 2rem 0.6rem 1rem; + border-radius: 8px; + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + font-weight: 500; + transition: 0.2s; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23a1a1aa'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.7rem center; + background-size: 1rem; +} + +.minimal-select:hover { background: rgba(255,255,255,0.1); color: white; } +.minimal-select:focus { border-color: var(--color-primary, #8b5cf6); color: white; } + +.view-toggle { + display: flex; + background: rgba(0,0,0,0.3); + border-radius: 8px; + padding: 2px; +} + +.view-btn { + background: transparent; + border: none; + color: var(--color-text-muted, #71717a); + padding: 0.5rem 0.8rem; + border-radius: 6px; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; +} + +.view-btn.active, .view-btn:hover { background: rgba(255,255,255,0.1); color: white; } + +/* Switcher Local (Segmented Control) */ +.local-type-switcher { + display: flex; + background: rgba(0,0,0,0.3); + padding: 4px; + border-radius: 10px; + border: 1px solid rgba(255,255,255,0.05); +} + +.type-pill-btn { + background: transparent; + border: none; + color: var(--color-text-secondary, #a1a1aa); + padding: 0.5rem 1.2rem; + font-size: 0.9rem; + font-weight: 600; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; +} + +.type-pill-btn:hover { color: white; } +.type-pill-btn.active { + background: var(--color-bg-elevated, #18181b); + color: white; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); +} + +/* Action Buttons (Scan) */ +.actions-group { + display: flex; + gap: 0.5rem; + border-left: 1px solid rgba(255,255,255,0.1); + padding-left: 1.5rem; +} + +.action-icon-btn { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.1); + color: var(--color-text-secondary, #a1a1aa); + width: 40px; height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; transition: 0.2s; } -.btn-danger:hover { background: #ef4444; color: white; } -@media (max-width: 900px) { - .settings-layout { grid-template-columns: 1fr; } - .profile-body { flex-direction: column; align-items: center; margin-top: -100px; text-align: center; } - .profile-details { flex-direction: column; align-items: center; width: 100%; } - .profile-stats-grid { justify-content: center; width: 100%; } +.action-icon-btn:hover { background: var(--color-primary, #8b5cf6); border-color: var(--color-primary, #8b5cf6); color: white; } +.action-icon-btn.danger:hover { background: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #ef4444; } + +@media (max-width: 768px) { + .toolbar { flex-direction: column; align-items: stretch; } + .toolbar.local-toolbar { grid-template-columns: 1fr; gap: 1rem; } + .actions-group { border-left: none; padding-left: 0; justify-content: flex-end; } + .local-type-switcher, .type-pill-btn { width: 100%; flex: 1; } + .search-box { max-width: 100%; } + .filters-inline { justify-content: space-between; } + .minimal-select { flex: 1; } } -.username-wrapper { - display: flex; - align-items: center; - gap: 1rem; /* Espacio entre nombre e icono */ +/* ========================================= + 5. LIST GRID (Cards) + ========================================= */ +.list-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 2rem 1.5rem; } -.header-anilist-badge { - width: 32px; - height: 32px; +.list-item { + position: relative; + border-radius: 8px; + transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.3s; + cursor: pointer; +} + +.list-item:hover { transform: scale(1.05); z-index: 10; } + +.item-poster-link { + display: block; + position: relative; border-radius: 8px; overflow: hidden; - transition: transform 0.2s, box-shadow 0.2s; - display: flex; /* Para centrar la imagen */ + aspect-ratio: 2/3; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); } -.header-anilist-badge img { +.item-poster { width: 100%; height: 100%; object-fit: cover; + transition: filter 0.3s; } -.header-anilist-badge:hover { - transform: scale(1.1); - box-shadow: 0 0 15px rgba(61, 180, 242, 0.5); /* Color azul AniList */ +.list-item:hover .item-poster { filter: brightness(1.1); } + +.item-content { padding-top: 0.8rem; } + +.item-title { + font-size: 0.95rem; + font-weight: 700; + margin: 0 0 0.4rem 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: rgba(255,255,255,0.9); } -/* Ajuste Mobile */ +.list-item:hover .item-title { color: var(--color-primary, #8b5cf6); } + +.item-meta { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +.meta-pill { + font-size: 0.65rem; + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-pill { background: rgba(34, 197, 94, 0.15); color: #4ade80; } +.type-pill { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; } +.source-pill { background: rgba(59, 130, 246, 0.15); color: #60a5fa; } +.repeat-pill { background: rgba(234, 179, 8, 0.15); color: #facc15; } +.private-pill { background: rgba(239, 68, 68, 0.15); color: #f87171; } + +.progress-bar-container { + height: 4px; + background: rgba(255,255,255,0.1); + border-radius: 2px; + margin-bottom: 0.4rem; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: var(--color-primary, #8b5cf6); + border-radius: 2px; +} + +.progress-text { + font-size: 0.8rem; + color: var(--color-text-secondary, #a1a1aa); + display: flex; + justify-content: space-between; +} + +/* Edit Button (Hover) */ +.edit-icon-btn { + position: absolute; + top: 10px; right: 10px; + background: rgba(0,0,0,0.7); + backdrop-filter: blur(4px); + border: 1px solid rgba(255,255,255,0.2); + color: white; + width: 32px; height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translateY(-5px); + transition: all 0.2s; + z-index: 20; + cursor: pointer; +} + +.list-item:hover .edit-icon-btn { opacity: 1; transform: translateY(0); } +.edit-icon-btn:hover { background: var(--color-primary, #8b5cf6); border-color: var(--color-primary, #8b5cf6); } + +/* Badges adicionales */ +.unmatched-badge { + position: absolute; + top: 10px; left: 10px; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 4px 8px; + border-radius: 6px; + font-size: 0.7rem; + font-weight: 700; + color: #fbbf24; + display: flex; align-items: center; gap: 4px; +} + +.folder-path-tooltip { + font-size: 0.75rem; + color: var(--color-text-muted, #71717a); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.2rem; +} + +/* --- LIST VIEW MODE --- */ +.list-grid.list-view { grid-template-columns: 1fr; gap: 1rem; } + +.list-grid.list-view .list-item { + display: flex; + align-items: center; + background: var(--color-bg-elevated, #18181b); + padding: 1rem; + border: 1px solid rgba(255,255,255,0.05); +} + +.list-grid.list-view .list-item:hover { + transform: scale(1.01); + border-color: var(--color-primary, #8b5cf6); +} + +.list-grid.list-view .item-poster-link { width: 60px; aspect-ratio: 2/3; margin-right: 1.5rem; flex-shrink: 0; } +.list-grid.list-view .item-content { padding-top: 0; flex-grow: 1; display: flex; align-items: center; justify-content: space-between; } +.list-grid.list-view .item-title { font-size: 1.1rem; margin-bottom: 0.2rem; } +.list-grid.list-view .edit-icon-btn { position: static; opacity: 1; transform: none; background: transparent; border: 1px solid rgba(255,255,255,0.1); } + +/* ========================================= + 6. CONFIGURACIÓN (Stream Style) + ========================================= */ +.stream-settings-container { + max-width: 800px; + margin: 0 auto; + padding-top: 1rem; + color: #e5e5e5; +} + +.stream-section { + margin-bottom: 2.5rem; + padding-bottom: 2.5rem; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.stream-section.no-border { border-bottom: none; } + +.section-label { + font-size: 1.1rem; + color: #a1a1aa; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 1.5rem; + font-weight: 600; +} + +/* Row de Perfil */ +.stream-profile-row { display: flex; gap: 3rem; align-items: flex-start; } + +.stream-avatar-wrapper { + position: relative; + width: 120px; height: 120px; + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + background: #27272a; +} + +.stream-avatar-wrapper img { width: 100%; height: 100%; object-fit: cover; } + +.avatar-overlay { + position: absolute; inset: 0; + background: rgba(0,0,0,0.6); + display: flex; align-items: center; justify-content: center; + opacity: 0; cursor: pointer; transition: 0.2s; + font-weight: 600; text-transform: uppercase; font-size: 0.8rem; +} + +.stream-avatar-wrapper:hover .avatar-overlay { opacity: 1; } + +.stream-inputs-col { flex-grow: 1; display: flex; flex-direction: column; gap: 1.5rem; } + +/* Formularios */ +.stream-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: end; } + +.stream-input-group { display: flex; flex-direction: column; gap: 0.5rem; } +.stream-input-group label { font-size: 0.85rem; color: #d4d4d8; font-weight: 500; } + +.stream-input { + background: #27272a; + border: 1px solid transparent; + border-radius: 4px; + padding: 0.8rem 1rem; + color: white; + font-size: 1rem; + transition: 0.2s; +} + +.stream-input:focus { background: #3f3f46; outline: none; border-color: var(--color-primary, #8b5cf6); } + +/* Botones Settings */ +.stream-actions { margin-top: 0.5rem; } + +.btn-stream-primary { + background: white; color: black; + border: none; padding: 0.7rem 2rem; + font-weight: 700; border-radius: 4px; + cursor: pointer; transition: 0.2s; +} +.btn-stream-primary:hover { background: #d4d4d8; } + +.btn-stream-ghost { + background: transparent; border: 1px solid #52525b; + color: white; padding: 0.75rem 1.5rem; + border-radius: 4px; cursor: pointer; + font-weight: 600; height: 46px; +} +.btn-stream-ghost:hover { border-color: white; background: rgba(255,255,255,0.05); } + +/* Integraciones */ +.stream-integration-row { + display: flex; justify-content: space-between; align-items: center; + background: rgba(255,255,255,0.03); + padding: 1rem 1.5rem; + border-radius: 6px; +} + +.int-info { display: flex; align-items: center; gap: 1rem; } +.int-logo { width: 32px; height: 32px; } +.int-text { display: flex; flex-direction: column; } +.int-name { font-weight: 700; font-size: 1rem; } +.int-status { font-size: 0.85rem; color: #a1a1aa; } + +.btn-stream-outline { + background: transparent; border: none; + color: var(--color-primary, #8b5cf6); + font-weight: 600; cursor: pointer; font-size: 0.95rem; +} +.btn-stream-outline:hover { text-decoration: underline; } +.btn-danger-outline { color: #ef4444; border: 1px solid #ef4444; padding: 0.5rem 1rem; border-radius: 4px; } + +.link-danger { + background: transparent; border: none; + color: #ef4444; font-size: 1rem; + cursor: pointer; padding: 0; + text-decoration: underline; opacity: 0.8; +} +.link-danger:hover { opacity: 1; } + @media (max-width: 768px) { - .username-wrapper { - justify-content: center; - } -} \ No newline at end of file + .stream-profile-row { flex-direction: column; align-items: center; text-align: center; gap: 2rem; } + .stream-form-row { grid-template-columns: 1fr; } + .btn-stream-ghost { width: 100%; } + .stream-actions { display: flex; justify-content: center; } +} + +/* ========================================= + 7. UTILIDADES & ESTADOS (Loading/Console) + ========================================= */ +.loading-state { + display: flex; flex-direction: column; align-items: center; + justify-content: center; padding: 4rem 0; + color: var(--color-text-secondary, #a1a1aa); +} + +.spinner { + width: 40px; height: 40px; + border: 3px solid rgba(139, 92, 246, 0.1); + border-top-color: var(--color-primary, #8b5cf6); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 1rem; +} +.spinner.small { width: 20px; height: 20px; border-width: 2px; margin-bottom: 0; } +@keyframes spin { to { transform: rotate(360deg); } } + +.empty-state { + text-align: center; padding: 5rem 2rem; + background: rgba(255,255,255,0.02); + border-radius: 20px; + border: 2px dashed rgba(255,255,255,0.05); +} +.empty-state p { font-size: 1.2rem; color: var(--color-text-secondary, #a1a1aa); margin-bottom: 1.5rem; } + +.btn-blur { + background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.2); + color: white; padding: 0.8rem 1.5rem; + border-radius: 99px; text-decoration: none; + display: inline-block; transition: 0.2s; +} +.btn-blur:hover { background: white; color: black; } + +.console-output { + background: #09090b; border: 1px solid #27272a; + padding: 1.5rem; border-radius: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.9rem; color: #4ade80; + display: flex; align-items: center; gap: 1rem; + box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); + margin-bottom: 1.5rem; +} + +.hidden { display: none !important; } \ No newline at end of file diff --git a/docker/views/dashboard.html b/docker/views/dashboard.html index 5c91425..5a0bbc6 100644 --- a/docker/views/dashboard.html +++ b/docker/views/dashboard.html @@ -10,31 +10,14 @@ - - -
-
- - WaifuBoard -
-
- - - -
-
Profile - -
@@ -77,7 +60,8 @@
@@ -98,81 +82,100 @@
-
-

Local Library

-
- - + +
+ + +
+ + + +
+ +
+ +
-
-

Anime Files

0

-

Manga Files

0

-

Novel Files

0

-
+ + +
+ +
-
-
-

Edit Profile

-
-
- - +
+ +
+ + + +
+ Avatar +
+ Edit +
+
-
- -
- - OR - +
+
+ + +
+ +
+
- - * If you upload a file, it will be converted automatically. The text box above is only for external URLs. -
-
-
-
-

Security

-
-
- - -
-
- - -
- -
-
- -
-

Integrations

-
-
AL
-
- AniList - Checking... -
- +
+ +
+
+ +
-
+
+ + +
+
+ +
+ +
-
-

Danger Zone

- +
+ +
+
+ +
+ AniList + Checking... +
+
+
+