const GITEA_INSTANCE = 'https://git.waifuboard.app'; const REPO_OWNER = 'ItsSkaiya'; const REPO_NAME = 'WaifuBoard-Extensions'; let DETECTED_BRANCH = 'main'; const API_URL_BASE = `${GITEA_INSTANCE}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/contents`; const INSTALLED_EXTENSIONS_API = '/api/extensions'; const extensionsGrid = document.getElementById('extensions-grid'); const filterSelect = document.getElementById('extension-filter'); let allExtensionsData = []; const customModal = document.getElementById('customModal'); const modalTitle = document.getElementById('modalTitle'); const modalMessage = document.getElementById('modalMessage'); function getRawUrl(filename) { const targetUrl = `${GITEA_INSTANCE}/${REPO_OWNER}/${REPO_NAME}/raw/branch/main/${filename}`; const encodedUrl = encodeURIComponent(targetUrl); return `/api/proxy?url=${encodedUrl}`; } function updateExtensionState(fileName, installed) { const ext = allExtensionsData.find(e => e.fileName === fileName); if (!ext) return; ext.isInstalled = installed; ext.isLocal = installed && ext.isLocal; filterAndRenderExtensions(filterSelect?.value || 'All'); } function formatExtensionName(fileName) { return fileName.replace('.js', '') .replace(/([a-z])([A-Z])/g, '$1 $2') .replace(/^[a-z]/, (char) => char.toUpperCase()); } function getIconUrl(extensionDetails) { return extensionDetails; } async function getExtensionDetails(url) { try { const res = await fetch(url); if (!res.ok) throw new Error(`Fetch failed: ${res.status}`); const text = await res.text(); const regex = /(?:this\.|const\s+|let\s+|var\s+)?baseUrl\s*=\s*(["'`])(.*?)\1/i; const match = text.match(regex); let finalHostname = null; if (match && match[2]) { let rawUrl = match[2].trim(); if (!rawUrl.startsWith('http')) rawUrl = 'https://' + rawUrl; try { const urlObj = new URL(rawUrl); finalHostname = urlObj.hostname; } catch(e) { console.warn(`Could not parse baseUrl: ${rawUrl}`); } } const classMatch = text.match(/class\s+(\w+)/); const name = classMatch ? classMatch[1] : null; let type = 'Image'; if (text.includes('type = "book-board"') || text.includes("type = 'book-board'")) type = 'Book'; else if (text.includes('type = "anime-board"') || text.includes("type = 'anime-board'")) type = 'Anime'; return { baseUrl: finalHostname, name, type }; } catch (e) { return { baseUrl: null, name: null, type: 'Unknown' }; } } function showCustomModal(title, message, isConfirm = false) { return new Promise(resolve => { modalTitle.textContent = title; modalMessage.textContent = message; const currentConfirmButton = document.getElementById('modalConfirmButton'); const currentCloseButton = document.getElementById('modalCloseButton'); const newConfirmButton = currentConfirmButton.cloneNode(true); currentConfirmButton.parentNode.replaceChild(newConfirmButton, currentConfirmButton); const newCloseButton = currentCloseButton.cloneNode(true); currentCloseButton.parentNode.replaceChild(newCloseButton, currentCloseButton); if (isConfirm) { newConfirmButton.classList.remove('hidden'); newConfirmButton.textContent = 'Confirm'; newCloseButton.textContent = 'Cancel'; } else { newConfirmButton.classList.add('hidden'); newCloseButton.textContent = 'Close'; } const closeModal = (confirmed) => { customModal.classList.add('hidden'); resolve(confirmed); }; newConfirmButton.onclick = () => closeModal(true); newCloseButton.onclick = () => closeModal(false); customModal.classList.remove('hidden'); }); } function renderExtensionCard(extension, isInstalled, isLocalOnly = false) { const extensionName = formatExtensionName(extension.fileName || extension.name); const extensionType = extension.type || 'Unknown'; let iconUrl; if (extension.baseUrl && extension.baseUrl !== 'Local Install') { iconUrl = `https://www.google.com/s2/favicons?domain=${extension.baseUrl}&sz=128`; } else { const displayName = extensionName.replace(/\s/g, '+'); iconUrl = `https://ui-avatars.com/api/?name=${displayName}&background=1f2937&color=fff&length=1`; } const card = document.createElement('div'); card.className = `extension-card grid-item extension-type-${extensionType.toLowerCase()}`; card.dataset.path = extension.fileName || extension.name; card.dataset.type = extensionType; let buttonHtml; let badgeHtml = ''; if (isInstalled) { if (isLocalOnly) { badgeHtml = 'Local'; } else { badgeHtml = 'Installed'; } buttonHtml = ` `; } else { buttonHtml = ` `; } card.innerHTML = ` ${extensionName} Icon

${extensionName}

${badgeHtml}
${buttonHtml} `; const installButton = card.querySelector('[data-action="install"]'); const uninstallButton = card.querySelector('[data-action="uninstall"]'); if (installButton) { installButton.addEventListener('click', async () => { try { const response = await fetch('/api/extensions/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName: extension.fileName }), }); const result = await response.json(); if (response.ok) { updateExtensionState(extension.fileName, true); await showCustomModal( 'Installation Successful', `${extensionName} has been successfully installed.`, false ); } else { await showCustomModal( 'Installation Failed', `Installation failed: ${result.error || 'Unknown error.'}`, false ); } } catch (error) { await showCustomModal( 'Installation Failed', `Network error during installation.`, false ); } }); } if (uninstallButton) { uninstallButton.addEventListener('click', async () => { const confirmed = await showCustomModal( 'Confirm Uninstallation', `Are you sure you want to uninstall ${extensionName}?`, true ); if (!confirmed) return; try { const response = await fetch('/api/extensions/uninstall', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName: extension.fileName }), }); const result = await response.json(); if (response.ok) { updateExtensionState(extension.fileName, false); await showCustomModal( 'Uninstallation Successful', `${extensionName} has been successfully uninstalled.`, false ); } else { await showCustomModal( 'Uninstallation Failed', `Uninstallation failed: ${result.error || 'Unknown error.'}`, false ); } } catch (error) { await showCustomModal( 'Uninstallation Failed', `Network error during uninstallation.`, false ); } }); } extensionsGrid.appendChild(card); } async function getInstalledExtensions() { console.log(`Fetching installed extensions from: ${INSTALLED_EXTENSIONS_API}`); try { const response = await fetch(INSTALLED_EXTENSIONS_API); if (!response.ok) { console.error(`Error fetching installed extensions. Status: ${response.status}`); return new Set(); } const data = await response.json(); if (!data.extensions || !Array.isArray(data.extensions)) { console.error("Invalid response format from /api/extensions: 'extensions' array missing or incorrect."); return new Set(); } const installedFileNames = data.extensions .map(name => `${name.toLowerCase()}.js`); return new Set(installedFileNames); } catch (error) { console.error('Network or JSON parsing error during fetch of installed extensions:', error); return new Set(); } } function filterAndRenderExtensions(filterType) { extensionsGrid.innerHTML = ''; if (!allExtensionsData || allExtensionsData.length === 0) { console.log('No extension data to filter.'); return; } const filteredExtensions = allExtensionsData.filter(ext => filterType === 'All' || ext.type === filterType || (ext.isLocal && filterType === 'Local') ); filteredExtensions.forEach(ext => { renderExtensionCard(ext, ext.isInstalled, ext.isLocal); }); if (filteredExtensions.length === 0) { extensionsGrid.innerHTML = `

No extensions found for the selected filter (${filterType}).

`; } } async function loadMarketplace() { extensionsGrid.innerHTML = ''; for (let i = 0; i < 6; i++) { extensionsGrid.innerHTML += `
`; } try { const [availableExtensionsRaw, installedExtensionsSet] = await Promise.all([ fetch(API_URL_BASE).then(res => { if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); return res.json(); }), getInstalledExtensions() ]); const availableExtensionsJs = availableExtensionsRaw.filter(ext => ext.type === 'file' && ext.name.endsWith('.js')); const detailPromises = []; const marketplaceFileNames = new Set(availableExtensionsJs.map(ext => ext.name.toLowerCase())); for (const ext of availableExtensionsJs) { const downloadUrl = getRawUrl(ext.name); const detailsPromise = getExtensionDetails(downloadUrl).then(details => ({ ...ext, ...details, fileName: ext.name, isInstalled: installedExtensionsSet.has(ext.name.toLowerCase()), isLocal: false, })); detailPromises.push(detailsPromise); } const extensionsWithDetails = await Promise.all(detailPromises); installedExtensionsSet.forEach(installedName => { if (!marketplaceFileNames.has(installedName)) { const localExt = { name: formatExtensionName(installedName), fileName: installedName, type: 'Local', isInstalled: true, isLocal: true, baseUrl: 'Local Install', }; extensionsWithDetails.push(localExt); } }); extensionsWithDetails.sort((a, b) => { if (a.isInstalled !== b.isInstalled) { return b.isInstalled - a.isInstalled; } const nameA = a.name || ''; const nameB = b.name || ''; return nameA.localeCompare(nameB); }); allExtensionsData = extensionsWithDetails; if (filterSelect) { filterSelect.addEventListener('change', (event) => { filterAndRenderExtensions(event.target.value); }); } filterAndRenderExtensions('All'); } catch (error) { console.error('Error loading the marketplace:', error); extensionsGrid.innerHTML = `
🚨 Error loading extensions.

Could not connect to the extension repository or local endpoint. Detail: ${error.message}

`; allExtensionsData = []; } } customModal.addEventListener('click', (e) => { if (e.target === customModal || e.target.tagName === 'BUTTON') { customModal.classList.add('hidden'); } }); document.addEventListener('DOMContentLoaded', loadMarketplace); window.addEventListener('scroll', () => { const navbar = document.getElementById('navbar'); if (window.scrollY > 0) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } });