const API_BASE = '/api'; const DashboardApp = { State: { currentList: [], filteredList: [], localLibraryData: [], currentUserId: null, currentLocalType: 'anime', pagination: { itemsPerPage: 50, visibleCount: 50 } }, init: async function() { console.log('Initializing Dashboard...'); await this.User.init(); await this.Tracking.load(); this.UI.setupTabSystem(); this.initListeners(); 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(); } } }, 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'); } }, 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"); } }, updateProfile: async function(e) { e.preventDefault(); const userId = DashboardApp.State.currentUserId; if (!userId) 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 && 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(); } const bodyData = { username }; if (finalAvatar) bodyData.profilePictureUrl = finalAvatar; 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); } }, 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; 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(); 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); } } }, Tracking: { load: async function() { const loadingState = document.getElementById('loading-state'); const emptyState = document.getElementById('empty-state'); const container = document.getElementById('list-container'); try { loadingState.style.display = 'flex'; emptyState.style.display = 'none'; container.innerHTML = ''; const response = await fetch(`${API_BASE}/list`, { headers: window.AuthUtils.getSimpleAuthHeaders() }); if (!response.ok) throw new Error('Failed'); const data = await response.json(); DashboardApp.State.currentList = data.results || []; this.updateStats(); loadingState.style.display = 'none'; if (DashboardApp.State.currentList.length === 0) emptyState.style.display = 'flex'; else this.applyFilters(); } catch (error) { console.error(error); loadingState.style.display = 'none'; } }, 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; 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; }, 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() : ''; let result = [...DashboardApp.State.currentList]; if (searchQuery) { result = result.filter(item => (item.title ? 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 (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)); DashboardApp.State.filteredList = result; DashboardApp.State.pagination.visibleCount = DashboardApp.State.pagination.itemsPerPage; this.render(); }, render: function() { const container = document.getElementById('list-container'); container.innerHTML = ''; const list = DashboardApp.State.filteredList; const count = DashboardApp.State.pagination.visibleCount; if (list.length === 0) { container.innerHTML = '
Error loading library
No ${type} files found.