Files

568 lines
22 KiB
JavaScript

import { app } from "../../scripts/app.js";
const cssId = 'animadex-css';
if (!document.getElementById(cssId)) {
const head = document.getElementsByTagName('head')[0];
const link = document.createElement('link');
link.id = cssId;
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'extensions/ComfyUI-Animadex-Node/animadex.css';
link.media = 'all';
head.appendChild(link);
}
let currentModal = null;
let metadataCache = null;
const FAV_STORAGE_KEY = 'animadex_favorites';
const FAV_COPY_STORAGE_KEY = 'animadex_copy_favorites';
const nodeStates = {};
app.registerExtension({
name: "ComfyUI.Animadex.Node",
async nodeCreated(node) {
if (node.comfyClass !== "AnimadexCharacterNode") return;
const charWidget = node.widgets.find(w => w.name === "character");
const btnWidget = node.addWidget("button", "Open Gallery 🖼️", "open", () => {
openAnimadexModal(charWidget, btnWidget, node);
});
if (charWidget && charWidget.value) {
const shortName = charWidget.value.split(" (")[0];
btnWidget.name = "🖼️ " + shortName;
}
node.setSize(node.computeSize());
}
});
function getFavorites(key) {
try {
return JSON.parse(localStorage.getItem(key)) || [];
} catch {
return [];
}
}
function toggleFavorite(key, id, isFav) {
let favs = getFavorites(key);
if (isFav && !favs.includes(id)) {
favs.push(id);
} else if (!isFav) {
favs = favs.filter(f => f !== id);
}
localStorage.setItem(key, JSON.stringify(favs));
}
function openAnimadexModal(charWidget, btnWidget, node) {
if (currentModal) return;
if (!nodeStates[node.id]) {
nodeStates[node.id] = {
currentPage: 1,
currentQuery: "",
currentCopyright: "",
currentExactChar: "",
favOnly: false,
isRandom: false,
selectedChar: null
};
}
const state = nodeStates[node.id];
let totalPages = 1;
const overlay = document.createElement('div');
overlay.className = 'animadex-modal-overlay';
const modal = document.createElement('div');
modal.className = 'animadex-modal';
// ==========================================
// Sidebar
// ==========================================
const sidebar = document.createElement('div');
sidebar.className = 'animadex-sidebar';
const sidebarTitle = document.createElement('div');
sidebarTitle.className = 'animadex-sidebar-title';
sidebarTitle.textContent = 'Filters';
const filterSection = document.createElement('div');
filterSection.className = 'animadex-filter-section';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'animadex-search-input';
searchInput.placeholder = 'Type to search...';
filterSection.appendChild(searchInput);
const sidebarLists = document.createElement('div');
sidebarLists.className = 'animadex-sidebar-lists';
const createAccordion = (title, itemsContainer) => {
const wrap = document.createElement('div');
wrap.className = 'animadex-accordion';
const hdr = document.createElement('div');
hdr.className = 'animadex-accordion-header';
hdr.innerHTML = `<span><span style="color:#f38ba8; margin-right:5px;">|</span> ${title}</span> <span>▾</span>`;
const body = document.createElement('div');
body.className = 'animadex-accordion-body';
body.appendChild(itemsContainer);
hdr.onclick = () => {
body.style.display = body.style.display === 'none' ? 'block' : 'none';
};
wrap.appendChild(hdr);
wrap.appendChild(body);
return { wrap, body };
};
const charListContainer = document.createElement('div');
const copyListContainer = document.createElement('div');
const charAccordion = createAccordion('Character', charListContainer);
const copyAccordion = createAccordion('Copyright', copyListContainer);
sidebarLists.appendChild(charAccordion.wrap);
sidebarLists.appendChild(copyAccordion.wrap);
sidebar.appendChild(sidebarTitle);
sidebar.appendChild(filterSection);
sidebar.appendChild(sidebarLists);
// ==========================================
// Main Area
// ==========================================
const main = document.createElement('div');
main.className = 'animadex-main';
const header = document.createElement('div');
header.className = 'animadex-main-header';
const infoText = document.createElement('span');
infoText.innerHTML = 'Loading...';
const headerControls = document.createElement('div');
headerControls.className = 'animadex-header-controls';
const randomBtn = document.createElement('button');
randomBtn.className = 'animadex-icon-btn';
randomBtn.innerHTML = '🎲 <span>Random</span>';
randomBtn.onclick = () => {
state.isRandom = true;
state.currentPage = 1;
loadData();
};
const favBtn = document.createElement('button');
favBtn.className = 'animadex-icon-btn';
if (state.favOnly) favBtn.classList.add('active');
favBtn.innerHTML = '♥ <span>Favorites</span>';
favBtn.onclick = () => {
state.favOnly = !state.favOnly;
favBtn.classList.toggle('active', state.favOnly);
state.currentPage = 1;
renderSidebar(); // Update sidebar list to show only favs
loadData();
};
const closeBtn = document.createElement('button');
closeBtn.className = 'animadex-close-btn';
closeBtn.innerHTML = '✖';
closeBtn.onclick = () => close();
headerControls.appendChild(randomBtn);
headerControls.appendChild(favBtn);
headerControls.appendChild(closeBtn);
header.appendChild(infoText);
header.appendChild(headerControls);
const gridContainer = document.createElement('div');
gridContainer.className = 'animadex-grid-container';
const grid = document.createElement('div');
grid.className = 'animadex-grid';
gridContainer.appendChild(grid);
const footer = document.createElement('div');
footer.className = 'animadex-pagination';
main.appendChild(header);
main.appendChild(gridContainer);
main.appendChild(footer);
modal.appendChild(sidebar);
modal.appendChild(main);
overlay.appendChild(modal);
document.body.appendChild(overlay);
currentModal = overlay;
const close = () => {
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
currentModal = null;
};
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
const formatNumber = (num) => {
if (!num) return "0";
if (num >= 1000) {
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
}
return num.toString();
};
// Pagination Renderer
const renderPagination = () => {
footer.innerHTML = '';
if (totalPages <= 1) return;
const createBtn = (text, page, isActive = false) => {
const b = document.createElement('button');
b.className = 'animadex-page-btn';
b.innerHTML = text;
if (isActive) b.classList.add('active');
b.onclick = () => {
state.currentPage = page;
loadData();
};
return b;
};
const createEllipsis = () => {
const span = document.createElement('span');
span.className = 'animadex-page-ellipsis';
span.innerHTML = '...';
return span;
};
// Prev Button
const prev = document.createElement('button');
prev.className = 'animadex-page-btn';
prev.innerHTML = '◀ Prev';
prev.disabled = state.currentPage <= 1;
prev.onclick = () => { state.currentPage--; loadData(); };
footer.appendChild(prev);
// Confirm Button
const confirmBtn = document.createElement('button');
confirmBtn.className = 'animadex-page-btn confirm-btn';
confirmBtn.style.background = '#a6e3a1';
confirmBtn.style.borderColor = '#a6e3a1';
confirmBtn.style.color = '#11111b';
confirmBtn.style.fontWeight = 'bold';
confirmBtn.innerHTML = '✅ Confirm';
confirmBtn.onclick = () => {
if (state.selectedChar && charWidget) {
charWidget.value = state.selectedChar._display_name;
btnWidget.name = "🖼️ " + state.selectedChar.name;
if (app.graph) app.graph.setDirtyCanvas(true);
close();
} else if (!state.selectedChar) {
alert("Please select a character first!");
}
};
footer.appendChild(confirmBtn);
// Calculate visible pages
let startPage = Math.max(1, state.currentPage - 2);
let endPage = Math.min(totalPages, state.currentPage + 2);
if (state.currentPage <= 3) {
endPage = Math.min(5, totalPages);
}
if (state.currentPage >= totalPages - 2) {
startPage = Math.max(1, totalPages - 4);
}
if (startPage > 1) {
footer.appendChild(createBtn('1', 1));
if (startPage > 2) footer.appendChild(createEllipsis());
}
for (let i = startPage; i <= endPage; i++) {
footer.appendChild(createBtn(i.toString(), i, i === state.currentPage));
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) footer.appendChild(createEllipsis());
footer.appendChild(createBtn(totalPages.toString(), totalPages));
}
// Next Button
const next = document.createElement('button');
next.className = 'animadex-page-btn';
next.innerHTML = 'Next ▶';
next.disabled = state.currentPage >= totalPages;
next.onclick = () => { state.currentPage++; loadData(); };
footer.appendChild(next);
};
const renderSidebar = () => {
if (!metadataCache) return;
const renderList = (container, data, isCopyright) => {
container.innerHTML = '';
const q = state.currentQuery.toLowerCase();
let filtered = data;
const favKey = isCopyright ? FAV_COPY_STORAGE_KEY : FAV_STORAGE_KEY;
const currentFavs = getFavorites(favKey);
if (state.favOnly) {
// If favorites mode, filter by favorites
filtered = filtered.filter(d => isCopyright ? currentFavs.includes(d.name) : currentFavs.includes(d.display_name));
}
if (q) {
filtered = filtered.filter(d => d.name.toLowerCase().includes(q));
}
const sliced = filtered.slice(0, 100);
sliced.forEach(item => {
const row = document.createElement('div');
row.className = 'animadex-list-item';
if (isCopyright && state.currentCopyright === item.name) {
row.classList.add('active');
} else if (!isCopyright && state.currentExactChar === item.display_name) {
row.classList.add('active');
}
const nameSpan = document.createElement('span');
nameSpan.className = 'animadex-list-item-name';
nameSpan.textContent = item.name;
nameSpan.title = item.name;
const countSpan = document.createElement('span');
countSpan.className = 'animadex-list-item-count';
countSpan.textContent = formatNumber(item.count);
row.appendChild(nameSpan);
row.appendChild(countSpan);
// Add heart button for copyrights
if (isCopyright) {
const favBtn = document.createElement('button');
favBtn.className = 'animadex-list-fav-btn';
favBtn.innerHTML = '♥';
if (currentFavs.includes(item.name)) favBtn.classList.add('active');
favBtn.onclick = (e) => {
e.stopPropagation();
const willBeFav = !favBtn.classList.contains('active');
toggleFavorite(FAV_COPY_STORAGE_KEY, item.name, willBeFav);
favBtn.classList.toggle('active');
if (state.favOnly) renderSidebar(); // immediately hide if unfavorited
};
row.appendChild(favBtn);
}
row.onclick = () => {
if (isCopyright) {
state.currentCopyright = state.currentCopyright === item.name ? "" : item.name;
state.currentExactChar = ""; // clear character isolation if work selected
state.currentPage = 1;
loadData();
renderSidebar();
} else {
state.currentExactChar = state.currentExactChar === item.display_name ? "" : item.display_name;
state.currentPage = 1;
loadData();
renderSidebar();
}
};
container.appendChild(row);
});
};
renderList(charListContainer, metadataCache.characters, false);
renderList(copyListContainer, metadataCache.copyrights, true);
};
const loadData = async () => {
grid.innerHTML = '<div style="grid-column: 1/-1; text-align:center; padding: 50px; font-size: 16px; color: #a6adc8;">Loading characters...</div>';
try {
if (!metadataCache) {
const metaResp = await fetch(`/animadex/metadata`);
metadataCache = await metaResp.json();
renderSidebar();
}
const favsList = getFavorites(FAV_STORAGE_KEY).join(",");
let url = `/animadex/search?q=${encodeURIComponent(state.currentQuery)}&page=${state.currentPage}&copyright=${encodeURIComponent(state.currentCopyright)}&favorites=${encodeURIComponent(favsList)}&exact=${encodeURIComponent(state.currentExactChar)}`;
if (state.isRandom) url += `&random=1`;
if (state.favOnly && !state.currentCopyright) url += `&fav_only=1`;
state.isRandom = false;
const resp = await fetch(url);
const data = await resp.json();
totalPages = data.pages;
grid.innerHTML = '';
infoText.innerHTML = `<b>${data.total.toLocaleString()}</b> characters - page ${data.page} of ${data.pages}`;
const currentFavs = getFavorites(FAV_STORAGE_KEY);
data.results.forEach(char => {
const card = document.createElement('div');
card.className = 'animadex-card';
const imgContainer = document.createElement('div');
imgContainer.className = 'animadex-card-img-container';
const imgUrl = char.thumb_url || char.img_url;
const img = document.createElement('img');
img.className = 'animadex-card-img';
img.src = imgUrl;
img.onerror = () => { img.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="%23dce0e8"/></svg>'; };
imgContainer.appendChild(img);
const overlay = document.createElement('div');
overlay.className = 'animadex-card-overlay';
const overlayTop = document.createElement('div');
overlayTop.className = 'animadex-overlay-top';
const favBtn = document.createElement('button');
favBtn.className = 'animadex-fav-btn';
const isFaved = currentFavs.includes(char._display_name);
if (isFaved) favBtn.classList.add('active');
favBtn.innerHTML = '♥';
favBtn.onclick = (e) => {
e.stopPropagation();
const willBeFav = !favBtn.classList.contains('active');
toggleFavorite(FAV_STORAGE_KEY, char._display_name, willBeFav);
favBtn.classList.toggle('active');
};
overlayTop.appendChild(favBtn);
overlay.appendChild(overlayTop);
if (char.trigger) {
const trigLabel = document.createElement('div');
trigLabel.className = 'animadex-overlay-title';
trigLabel.textContent = 'TRIGGER';
overlay.appendChild(trigLabel);
const trigBox = document.createElement('div');
trigBox.className = 'animadex-trigger-box';
trigBox.textContent = char.trigger;
overlay.appendChild(trigBox);
}
if (char.tags && char.tags.length > 0) {
const tagsLabel = document.createElement('div');
tagsLabel.className = 'animadex-overlay-title';
tagsLabel.textContent = `TAGS - ${char.tags.length}`;
overlay.appendChild(tagsLabel);
const tagsBox = document.createElement('div');
tagsBox.className = 'animadex-tags-container';
char.tags.slice(0, 15).forEach(t => {
const tg = document.createElement('div');
tg.className = 'animadex-tag-badge';
tg.textContent = t;
tagsBox.appendChild(tg);
});
overlay.appendChild(tagsBox);
}
imgContainer.appendChild(overlay);
const info = document.createElement('div');
info.className = 'animadex-card-info';
const title = document.createElement('div');
title.className = 'animadex-card-title';
title.textContent = char.name;
title.title = char.name;
const subtitle = document.createElement('div');
subtitle.className = 'animadex-card-subtitle';
subtitle.textContent = char.copyright_name || char.copyright || 'UNKNOWN';
info.appendChild(title);
info.appendChild(subtitle);
const cFooter = document.createElement('div');
cFooter.className = 'animadex-card-footer';
const fLeft = document.createElement('div');
fLeft.className = 'animadex-card-footer-left';
const up = char.rating ? char.rating.up : 0;
const down = char.rating ? char.rating.down : 0;
fLeft.innerHTML = `
<div class="animadex-card-footer-item">👍 ${formatNumber(up)}</div>
<div class="animadex-card-footer-item">👎 ${formatNumber(down)}</div>
`;
const fRight = document.createElement('div');
fRight.className = 'animadex-card-footer-right';
const count = char.count || 0;
const favs = char.fav_count || 0;
fRight.innerHTML = `
<div class="animadex-card-footer-item">▲ ${count.toLocaleString()}</div>
<div class="animadex-card-footer-item">♥ ${formatNumber(favs)}</div>
`;
cFooter.appendChild(fLeft);
cFooter.appendChild(fRight);
card.appendChild(imgContainer);
card.appendChild(info);
card.appendChild(cFooter);
if (state.selectedChar && state.selectedChar._display_name === char._display_name) {
card.classList.add('selected');
}
card.onclick = () => {
document.querySelectorAll('.animadex-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
state.selectedChar = char;
};
card.ondblclick = () => {
card.onclick();
document.querySelector('.confirm-btn').click();
};
grid.appendChild(card);
});
renderPagination();
} catch (e) {
console.error("Animadex fetch error:", e);
grid.innerHTML = `<div style="grid-column: 1/-1; text-align:center; color:#f38ba8;">Error loading data. Is the backend running?</div>`;
footer.innerHTML = '';
}
};
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
state.currentQuery = searchInput.value.trim();
renderSidebar();
searchTimeout = setTimeout(() => {
state.currentPage = 1;
loadData();
}, 400);
});
setTimeout(() => {
if (state.currentQuery) searchInput.value = state.currentQuery;
searchInput.focus();
loadData();
}, 100);
}