637 lines
28 KiB
JavaScript
637 lines
28 KiB
JavaScript
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 = '<div class="empty-text" style="grid-column: 1/-1; text-align:center; color: var(--color-text-secondary);">No matches found</div>';
|
|
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(`<span class="meta-pill repeat-pill">🔁 ${item.repeat_count}</span>`);
|
|
if (item.is_private) extraInfo.push('<span class="meta-pill private-pill">🔒 Private</span>');
|
|
|
|
div.innerHTML = `
|
|
<a href="${itemLink}" class="item-poster-link">
|
|
<img src="${posterUrl}" alt="${item.title}" class="item-poster" onerror="this.src='/public/assets/placeholder.svg'">
|
|
</a>
|
|
<div class="item-content">
|
|
<div>
|
|
<a href="${itemLink}" style="text-decoration:none; color:inherit;">
|
|
<h3 class="item-title">${item.title || 'Unknown'}</h3>
|
|
</a>
|
|
<div class="item-meta">
|
|
<span class="meta-pill status-pill">${statusLabels[item.status] || item.status}</span>
|
|
<span class="meta-pill type-pill">${entryType}</span>
|
|
<span class="meta-pill source-pill">${item.source.toUpperCase()}</span>
|
|
${extraInfo.join('')}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="progress-bar-container"><div class="progress-bar" style="width: ${progressPercent}%"></div></div>
|
|
<div class="progress-text">
|
|
<span>${progress}${totalUnits > 0 ? ` / ${totalUnits}` : ''}</span>
|
|
${score ? `<span class="score-badge">⭐ ${score}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="edit-icon-btn">
|
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M15.232 5.232l3.536 3.536m-2.036-5.808a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.536L15.232 5.232z"/></svg>
|
|
</button>
|
|
`;
|
|
|
|
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 = `<div class="empty-state"><p>Error loading library</p></div>`;
|
|
} finally {
|
|
loading.style.display = 'none';
|
|
}
|
|
},
|
|
|
|
renderGrid: function(entries, type) {
|
|
const container = document.getElementById('local-list-container');
|
|
container.innerHTML = '';
|
|
|
|
if (entries.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state" style="grid-column: 1/-1;">
|
|
<p>No ${type} files found.</p>
|
|
<button onclick="DashboardApp.Library.triggerScan('incremental')" class="btn-blur">Scan Now</button>
|
|
</div>`;
|
|
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 = `
|
|
<div class="item-poster-link" onclick="${isMatched ? `window.location='${url}'` : ''}" style="cursor: ${isMatched ? 'pointer' : 'default'}">
|
|
<img src="${poster}" class="item-poster" loading="lazy">
|
|
${!isMatched ? `<div class="unmatched-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg> UNMATCHED</div>` : ''}
|
|
</div>
|
|
<div class="item-content">
|
|
<h3 class="item-title" title="${entry.path}">${title}</h3>
|
|
<div class="item-meta">
|
|
<span class="meta-pill type-pill">${entry.files} FILES</span>
|
|
${isMatched ? '<span class="meta-pill status-pill">MATCHED</span>' : ''}
|
|
</div>
|
|
<div class="folder-path-tooltip">${entry.path}</div>
|
|
</div>
|
|
<button class="edit-icon-btn" onclick="DashboardApp.Library.openManualMatch('${entry.id}', '${type}')" title="${isMatched ? 'Rematch' : 'Fix Match'}">
|
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
|
</button>
|
|
`;
|
|
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(); |