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 = '
No matches found
'; return; } const itemsToShow = list.slice(0, count); itemsToShow.forEach(item => container.appendChild(this.createItemElement(item))); if (count < list.length) { this.renderLoadMoreButton(container, list.length - count); } }, createItemElement: function(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 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'); div.innerHTML = ` ${item.title}

${item.title || 'Unknown'}

${statusLabels[item.status] || item.status} ${entryType} ${item.source.toUpperCase()} ${extraInfo.join('')}
${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''} ${score ? `⭐ ${score}` : ''}
`; 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'); }; return div; }, renderLoadMoreButton: function(container, remaining) { const btnContainer = document.createElement('div'); Object.assign(btnContainer.style, { gridColumn: "1 / -1", display: "flex", justifyContent: "center", padding: "2rem 0" }); 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(); }; btnContainer.appendChild(loadMoreBtn); container.appendChild(btnContainer); }, getEntryLink: function(item) { const baseRoute = (item.entry_type?.toUpperCase() === 'ANIME') ? '/anime' : '/book'; return `${baseRoute}/${item.entry_id}`; } }, Library: { tempMatchContext: null, 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.path; 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} FILES ${isMatched ? 'MATCHED' : ''}
${entry.path}
`; 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 item = DashboardApp.State.localLibraryData.find(x => x.id === id); const pathName = item ? item.path : 'Unknown path'; this.tempMatchContext = { id, type }; document.getElementById('manual-match-path').textContent = pathName; document.getElementById('manual-match-id').value = ''; const modal = document.getElementById('manual-match-modal'); modal.classList.remove('hidden'); setTimeout(() => document.getElementById('manual-match-id').focus(), 100); }, closeManualMatch: function() { document.getElementById('manual-match-modal').classList.add('hidden'); this.tempMatchContext = null; }, submitManualMatch: async function() { if (!this.tempMatchContext) return; const newId = document.getElementById('manual-match-id').value; if (!newId) { alert("Please enter a valid ID"); return; } const { id, type } = this.tempMatchContext; const confirmBtn = document.querySelector('#manual-match-modal .btn-primary'); const originalText = confirmBtn.textContent; confirmBtn.textContent = "Matching..."; confirmBtn.disabled = true; try { const res = await 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) }) }); if(res.ok) { this.closeManualMatch(); this.loadContent(type); } else { const errData = await res.json(); alert("Failed to match: " + (errData.error || "Unknown error")); } } catch (e) { console.error(e); alert("Connection error"); } finally { confirmBtn.textContent = originalText; confirmBtn.disabled = false; } }, 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 () => { const data = window.ListModalManager.currentData; if (!data) return; const idToSave = data.entry_id || data.id; 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; await window.ListModalManager.delete(idToDelete, data.source || 'anilist'); await DashboardApp.Tracking.load(); }; window.closeAddToListModal = () => window.ListModalManager.close();