const LS_KEY_MARKETPLACE_URL = 'wb_marketplace_url'; const DEFAULT_URL_PLACEHOLDER = 'https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/marketplace.json'; const INSTALLED_EXTENSIONS_API = '/api/extensions'; const UPDATE_EXTENSIONS_API = '/api/extensions/update'; // DOM Elements const dom = { content: document.getElementById('marketplace-content'), configPanel: document.getElementById('config-panel'), inputUrl: document.getElementById('source-url-input'), // Filters & Toggles filterType: document.getElementById('filter-type'), filterLang: document.getElementById('filter-lang'), btnNsfw: document.getElementById('btn-toggle-nsfw'), // Buttons btnConfig: document.getElementById('btn-configure'), btnSaveSource: document.getElementById('btn-save-source'), btnResetSource: document.getElementById('btn-reset-source'), btnUpdateAll: document.getElementById('btn-update-all'), // Tabs tabs: document.querySelectorAll('.tab-btn'), // Modal modal: document.getElementById('customModal'), modalTitle: document.getElementById('modalTitle'), modalMsg: document.getElementById('modalMessage'), modalConfirm: document.getElementById('modalConfirmButton'), modalClose: document.getElementById('modalCloseButton') }; // State let state = { url: localStorage.getItem(LS_KEY_MARKETPLACE_URL) || null, metadata: {}, installed: [], currentTab: 'marketplace', showNsfw: false // Default to HIDDEN }; /* --- Initialization --- */ async function init() { setupEventListeners(); if (!state.url) { renderWelcomeState(); } else { await loadData(); } } function setupEventListeners() { // Config Panel dom.btnConfig.addEventListener('click', () => { dom.configPanel.classList.toggle('hidden'); if(!dom.configPanel.classList.contains('hidden')) dom.inputUrl.focus(); }); dom.btnSaveSource.addEventListener('click', saveSource); dom.btnResetSource.addEventListener('click', () => { dom.inputUrl.value = DEFAULT_URL_PLACEHOLDER; saveSource(); }); // Filters dom.filterType.addEventListener('change', render); dom.filterLang.addEventListener('change', render); // NSFW Toggle dom.btnNsfw.addEventListener('click', toggleNsfw); // Tabs dom.tabs.forEach(tab => { tab.addEventListener('click', () => { dom.tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); state.currentTab = tab.dataset.tab; if (state.currentTab === 'installed') dom.btnUpdateAll.classList.remove('hidden'); else dom.btnUpdateAll.classList.add('hidden'); render(); }); }); if(dom.btnUpdateAll) dom.btnUpdateAll.addEventListener('click', handleUpdateAll); // Modal dom.modalClose.addEventListener('click', hideModal); } /* --- Logic --- */ function toggleNsfw() { state.showNsfw = !state.showNsfw; // Update Button UI if (state.showNsfw) { dom.btnNsfw.classList.add('btn-danger'); // Red for ALERT/NSFW dom.btnNsfw.classList.remove('btn-secondary'); dom.btnNsfw.innerHTML = ` NSFW: ON `; } else { dom.btnNsfw.classList.remove('btn-danger'); dom.btnNsfw.classList.add('btn-secondary'); dom.btnNsfw.innerHTML = ` NSFW: Off `; } render(); } function saveSource() { const url = dom.inputUrl.value.trim(); if (!url) return window.NotificationUtils.error('Please enter a valid URL'); localStorage.setItem(LS_KEY_MARKETPLACE_URL, url); state.url = url; dom.configPanel.classList.add('hidden'); window.NotificationUtils.success('Source updated'); loadData(); } async function loadData() { renderLoading(); try { const proxyUrl = `/api/proxy?url=${encodeURIComponent(state.url)}`; const [metaRes, installedRes] = await Promise.all([ fetch(proxyUrl).then(r => r.ok ? r.json() : null), fetch(INSTALLED_EXTENSIONS_API).then(r => r.json()) ]); if (!metaRes) throw new Error('Failed to fetch marketplace JSON'); state.metadata = metaRes.extensions || {}; state.installed = (installedRes.extensions || []).map(e => e.toLowerCase()); render(); } catch (error) { console.error(error); renderError(error.message); } } /* --- Rendering --- */ function render() { dom.content.innerHTML = ''; const typeFilter = dom.filterType.value; const langFilter = dom.filterLang.value; let items = []; // 1. Prepare items if (state.currentTab === 'marketplace') { items = Object.entries(state.metadata).map(([id, data]) => ({ id, ...data, isInstalled: state.installed.includes(id.toLowerCase()) })); } else { const metaItems = Object.entries(state.metadata) .filter(([id]) => state.installed.includes(id.toLowerCase())) .map(([id, data]) => ({ id, ...data, isInstalled: true })); items = [...metaItems]; state.installed.forEach(instId => { if (!items.find(i => i.id.toLowerCase() === instId)) { items.push({ id: instId, name: capitalize(instId), type: 'Local', description: 'Locally installed.', author: '?', lang: 'Local', isInstalled: true, isLocal: true }); } }); } // 2. Filter items = items.filter(item => { // NSFW Filter logic if (!state.showNsfw && item.nsfw) return false; // Type Filter if (typeFilter !== 'All' && item.type !== typeFilter && item.type !== 'Local') return false; // Lang Filter if (langFilter !== 'All') { const itemLang = (item.lang || 'en').toLowerCase(); // Si es image-board, ignoramos el filtro de idioma (ya que no tienen idioma) // O podemos decidir que siempre pasen, o que solo pasen si el filtro es "All". // Para simplificar: Si es image-board, pasa el filtro de idioma automáticamente. if (item.type !== 'image-board' && itemLang !== 'multi' && itemLang !== langFilter.toLowerCase()) return false; } return true; }); const grouped = groupBy(items, 'type'); const types = Object.keys(grouped).sort(); if (types.length === 0) return renderEmptyState(); types.forEach(type => { const section = document.createElement('div'); section.className = 'mp-section'; const title = document.createElement('h2'); title.className = 'category-title'; title.innerText = formatType(type); const grid = document.createElement('div'); grid.className = 'mp-grid'; grouped[type].forEach(ext => grid.appendChild(createCard(ext))); section.appendChild(title); section.appendChild(grid); dom.content.appendChild(section); }); } function createCard(ext) { const card = document.createElement('div'); card.className = `mp-card ${ext.nsfw ? 'card-nsfw' : ''}`; // Icon logic const iconSrc = ext.icon || '/public/assets/waifuboards.ico'; // Button Logic let btnClass = 'btn-primary'; let btnText = 'Install'; if (ext.isInstalled) { btnClass = 'btn-danger'; btnText = 'Uninstall'; } else if (ext.broken) { btnClass = 'btn-secondary'; btnText = 'Broken'; } // Language Pill Logic let langHtml = ''; // Solo mostramos lenguaje si NO es un image-board if (ext.type !== 'image-board') { const langCode = (ext.lang || 'EN').toLowerCase(); const langClass = `lang-${langCode}`; const langLabel = langCode === 'multi' ? 'MULTI' : langCode.toUpperCase(); langHtml = `${langLabel}`; } card.innerHTML = `
${ext.description || 'No description provided.'}
Configure a source URL to start.
No extensions found.
Loading...
Error: ${msg}