From 20ea5bee9cf9e0300a05bafc28a221a886c25101 Mon Sep 17 00:00:00 2001
From: lenafx
Date: Wed, 31 Dec 2025 21:51:17 +0100
Subject: [PATCH] new dashboard page
---
desktop/src/scripts/dashboard.js | 923 +++++++------
desktop/src/scripts/local-library-books.js | 106 --
desktop/src/scripts/local-library.js | 209 ---
desktop/views/anime/animes.html | 41 -
desktop/views/books/books.html | 27 -
desktop/views/css/dashboard.css | 1386 ++++++++------------
desktop/views/dashboard.html | 142 +-
docker/src/scripts/dashboard.js | 923 +++++++------
docker/src/scripts/local-library-books.js | 106 --
docker/src/scripts/local-library.js | 209 ---
docker/views/anime/animes.html | 42 +-
docker/views/books/books.html | 33 -
docker/views/css/dashboard.css | 1386 ++++++++------------
docker/views/dashboard.html | 153 +--
14 files changed, 2261 insertions(+), 3425 deletions(-)
delete mode 100644 desktop/src/scripts/local-library-books.js
delete mode 100644 desktop/src/scripts/local-library.js
delete mode 100644 docker/src/scripts/local-library-books.js
delete mode 100644 docker/src/scripts/local-library.js
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 = `
-
-
-
-
-
-
- `;
+
+ `;
- // 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 = `
`;
+ } 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 = `
+
+

+ ${!isMatched ? `
` : ''}
+
+
+
${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}
-
- ${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}
-
- ${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}
-
- ${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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -31,10 +31,6 @@
-
-
-

-
WaifuBoard
-
-
-
-
-
-
-