diff --git a/desktop/src/api/extensions/extensions.controller.ts b/desktop/src/api/extensions/extensions.controller.ts
index 6b97931..c62a4eb 100644
--- a/desktop/src/api/extensions/extensions.controller.ts
+++ b/desktop/src/api/extensions/extensions.controller.ts
@@ -37,24 +37,33 @@ export async function getExtensionSettings(req: ExtensionNameRequest, reply: Fas
}
export async function installExtension(req: any, reply: FastifyReply) {
- const { fileName } = req.body;
+ const { url } = req.body;
- if (!fileName || !fileName.endsWith('.js')) {
- return reply.code(400).send({ error: "Invalid extension fileName provided" });
+ if (!url || typeof url !== 'string' || !url.endsWith('.js')) {
+ return reply.code(400).send({ error: "Invalid extension URL provided" });
}
try {
+ const fileName = url.split('/').pop();
- const downloadUrl = `https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/${fileName}`
+ if (!fileName) {
+ return reply.code(400).send({ error: "Could not determine file name from URL" });
+ }
- await saveExtensionFile(fileName, downloadUrl);
+ await saveExtensionFile(fileName, url);
req.server.log.info(`Extension installed: ${fileName}`);
- return reply.code(200).send({ success: true, message: `Extension ${fileName} installed successfully.` });
+ return reply.code(200).send({
+ success: true,
+ message: `Extension ${fileName} installed successfully.`,
+ });
} catch (error) {
- req.server.log.error(`Failed to install extension ${fileName}:`, error);
- return reply.code(500).send({ success: false, error: `Failed to install extension ${fileName}.` });
+ req.server.log.error(`Failed to install extension from ${url}:`, error);
+ return reply.code(500).send({
+ success: false,
+ error: "Failed to install extension.",
+ });
}
}
diff --git a/desktop/src/scripts/marketplace.js b/desktop/src/scripts/marketplace.js
index 6180eb9..2e784fe 100644
--- a/desktop/src/scripts/marketplace.js
+++ b/desktop/src/scripts/marketplace.js
@@ -1,422 +1,221 @@
-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 ORIGINAL_MARKETPLACE_URL = 'https://git.waifuboard.app/ItsSkaiya/WaifuBoard-Extensions/raw/branch/main/marketplace.json';
+const MARKETPLACE_JSON_URL = `/api/proxy?url=${encodeURIComponent(ORIGINAL_MARKETPLACE_URL)}`;
const INSTALLED_EXTENSIONS_API = '/api/extensions';
-const extensionsGrid = document.getElementById('extensions-grid');
+const marketplaceContent = document.getElementById('marketplace-content');
const filterSelect = document.getElementById('extension-filter');
-let allExtensionsData = [];
-
-const customModal = document.getElementById('customModal');
+const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
+const modalConfirmBtn = document.getElementById('modalConfirmButton');
+const modalCloseBtn = document.getElementById('modalCloseButton');
-function getRawUrl(filename) {
+let marketplaceMetadata = {};
+let installedExtensions = [];
+let currentTab = 'marketplace';
- 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) {
+async function loadMarketplace() {
+ showSkeletons();
try {
- const res = await fetch(url);
- if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
- const text = await res.text();
+ const [metaRes, installedRes] = await Promise.all([
+ fetch(MARKETPLACE_JSON_URL).then(res => res.json()),
+ fetch(INSTALLED_EXTENSIONS_API).then(res => res.json())
+ ]);
- 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}`);
- }
+ marketplaceMetadata = metaRes.extensions;
+ installedExtensions = (installedRes.extensions || []).map(e => e.toLowerCase());
+
+ initTabs();
+ renderGroupedView();
+
+ if (filterSelect) {
+ filterSelect.addEventListener('change', () => renderGroupedView());
}
-
- 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' };
+ } catch (error) {
+ console.error('Error loading marketplace:', error);
+ marketplaceContent.innerHTML = `
Error al cargar el marketplace.
`;
}
}
-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);
+function initTabs() {
+ const tabs = document.querySelectorAll('.tab-button');
+ tabs.forEach(tab => {
+ tab.onclick = () => {
+ tabs.forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+ currentTab = tab.dataset.tab;
+ renderGroupedView();
};
-
- newConfirmButton.onclick = () => closeModal(true);
- newCloseButton.onclick = () => closeModal(false);
-
- customModal.classList.remove('hidden');
});
}
-function renderExtensionCard(extension, isInstalled, isLocalOnly = false) {
+function renderGroupedView() {
+ marketplaceContent.innerHTML = '';
+ const activeFilter = filterSelect.value;
+ const groups = {};
- const extensionName = formatExtensionName(extension.fileName || extension.name);
- const extensionType = extension.type || 'Unknown';
+ let listToRender = [];
- let iconUrl;
+ if (currentTab === 'marketplace') {
- if (extension.baseUrl && extension.baseUrl !== 'Local Install') {
-
- iconUrl = `https://www.google.com/s2/favicons?domain=${extension.baseUrl}&sz=128`;
+ for (const [id, data] of Object.entries(marketplaceMetadata)) {
+ listToRender.push({
+ id,
+ ...data,
+ isInstalled: installedExtensions.includes(id.toLowerCase())
+ });
+ }
} else {
- const displayName = extensionName.replace(/\s/g, '+');
- iconUrl = `https://ui-avatars.com/api/?name=${displayName}&background=1f2937&color=fff&length=1`;
+ for (const [id, data] of Object.entries(marketplaceMetadata)) {
+ if (installedExtensions.includes(id.toLowerCase())) {
+ listToRender.push({ id, ...data, isInstalled: true });
+ }
+ }
+
+ installedExtensions.forEach(id => {
+ const existsInMeta = Object.keys(marketplaceMetadata).some(k => k.toLowerCase() === id);
+ if (!existsInMeta) {
+ listToRender.push({
+ id: id,
+ name: id.charAt(0).toUpperCase() + id.slice(1),
+ type: 'Local',
+ author: 'Unknown',
+ isInstalled: true
+ });
+ }
+ });
}
+ listToRender.forEach(ext => {
+ const type = ext.type || 'Other';
+
+ if (activeFilter !== 'All' && type !== activeFilter) return;
+
+ if (!groups[type]) groups[type] = [];
+ groups[type].push(ext);
+ });
+
+ const sortedTypes = Object.keys(groups).sort();
+
+ if (sortedTypes.length === 0) {
+ marketplaceContent.innerHTML = `No extensions found for this criteria.
`;
+ return;
+ }
+
+ sortedTypes.forEach(type => {
+ const section = document.createElement('div');
+ section.className = 'category-group';
+
+ const title = document.createElement('h2');
+ title.className = 'marketplace-section-title';
+ title.innerText = type.replace('-', ' ');
+
+ const grid = document.createElement('div');
+ grid.className = 'marketplace-grid';
+
+ groups[type].forEach(ext => grid.appendChild(createCard(ext)));
+
+ section.appendChild(title);
+ section.appendChild(grid);
+ marketplaceContent.appendChild(section);
+ });
+}
+
+function createCard(ext) {
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;
+ card.className = `extension-card ${ext.nsfw ? 'nsfw-ext' : ''} ${ext.broken ? 'broken-ext' : ''}`;
- let buttonHtml;
- let badgeHtml = '';
+ const iconUrl = `https://www.google.com/s2/favicons?domain=${ext.domain}&sz=128`
- if (isInstalled) {
-
- if (isLocalOnly) {
- badgeHtml = 'Local';
- } else {
- badgeHtml = 'Installed';
- }
- buttonHtml = `
-
- `;
+ let buttonHtml = '';
+ if (ext.isInstalled) {
+ buttonHtml = ``;
+ } else if (ext.broken) {
+ buttonHtml = ``;
} else {
-
- buttonHtml = `
-
- `;
+ buttonHtml = ``;
}
card.innerHTML = `
-
+
-
${extensionName}
- ${badgeHtml}
+
${ext.name}
+
by ${ext.author || 'Unknown'}
+
${ext.description || 'No description available.'}
+
+
+ ${ext.isInstalled ? 'Installed' : (ext.broken ? 'Broken' : 'Available')}
+
+ ${ext.nsfw ? 'NSFW' : ''}
+
${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
- );
- }
- });
+ const btn = card.querySelector('.extension-action-button');
+ if (!ext.broken || ext.isInstalled) {
+ btn.onclick = () => ext.isInstalled ? promptUninstall(ext) : handleInstall(ext);
}
- 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);
+ return 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');
+function showModal(title, message, showConfirm = false, onConfirm = null) {
+ modalTitle.innerText = title;
+ modalMessage.innerText = message;
+ if (showConfirm) {
+ modalConfirmBtn.classList.remove('hidden');
+ modalConfirmBtn.onclick = () => { hideModal(); if (onConfirm) onConfirm(); };
} else {
- navbar.classList.remove('scrolled');
+ modalConfirmBtn.classList.add('hidden');
}
-});
\ No newline at end of file
+ modalCloseBtn.onclick = hideModal;
+ modal.classList.remove('hidden');
+}
+
+function hideModal() { modal.classList.add('hidden'); }
+
+async function handleInstall(ext) {
+ try {
+ const res = await fetch('/api/extensions/install', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url: ext.entry })
+ });
+ if (res.ok) {
+ installedExtensions.push(ext.id.toLowerCase());
+ renderGroupedView();
+ showModal('Success', `${ext.name} installed!`);
+ }
+ } catch (e) { showModal('Error', 'Install failed.'); }
+}
+
+function promptUninstall(ext) {
+ showModal('Confirm', `Uninstall ${ext.name}?`, true, () => handleUninstall(ext));
+}
+
+async function handleUninstall(ext) {
+ try {
+ const res = await fetch('/api/extensions/uninstall', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ fileName: ext.id + '.js' })
+ });
+ if (res.ok) {
+ installedExtensions = installedExtensions.filter(id => id !== ext.id.toLowerCase());
+ renderGroupedView();
+ }
+ } catch (e) { showModal('Error', 'Uninstall failed.'); }
+}
+
+function showSkeletons() {
+ marketplaceContent.innerHTML = `
+
+ ${Array(3).fill('
').join('')}
+
+ `;
+}
+
+document.addEventListener('DOMContentLoaded', loadMarketplace);
\ No newline at end of file
diff --git a/desktop/views/css/marketplace.css b/desktop/views/css/marketplace.css
index 02d5f7f..b415b38 100644
--- a/desktop/views/css/marketplace.css
+++ b/desktop/views/css/marketplace.css
@@ -292,4 +292,99 @@
.modal-button.btn-uninstall:hover {
background: #ef4444;
transform: scale(1.02);
+}
+
+.extension-author {
+ font-size: 0.8rem;
+ color: var(--color-text-secondary);
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.extension-tags {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.badge-available {
+ background: rgba(59, 130, 246, 0.2);
+ color: #60a5fa;
+ border: 1px solid rgba(59, 130, 246, 0.3);
+}
+
+.nsfw-ext {
+ border-color: rgba(220, 38, 38, 0.3);
+}
+
+.broken-ext {
+ filter: grayscale(0.8);
+ opacity: 0.7;
+ border: 1px dashed #ef4444; /* Borde rojo discontinuo */
+}
+
+.broken-ext:hover {
+ transform: none; /* Evitamos que se mueva al pasar el ratón si está rota */
+}
+
+/* Estilos para los Tabs */
+.tabs-container {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+ padding-bottom: 0.5rem;
+}
+
+.tab-button {
+ background: none;
+ border: none;
+ color: var(--color-text-secondary);
+ font-size: 1.1rem;
+ font-weight: 700;
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+ transition: all 0.3s;
+ position: relative;
+}
+
+.tab-button.active {
+ color: var(--color-primary);
+}
+
+.tab-button.active::after {
+ content: '';
+ position: absolute;
+ bottom: -0.6rem;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background: var(--color-primary);
+ border-radius: 999px;
+ box-shadow: 0 0 10px var(--color-primary-glow);
+}
+
+/* TÃtulos de Secciones en Marketplace */
+.marketplace-section-title {
+ font-size: 1.4rem;
+ font-weight: 800;
+ margin: 2rem 0 1rem 0;
+ color: var(--color-text-primary);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ text-transform: capitalize;
+}
+
+.marketplace-section-title::before {
+ content: '';
+ display: inline-block;
+ width: 4px;
+ height: 20px;
+ background: var(--color-primary);
+ border-radius: 2px;
+}
+
+.category-group {
+ margin-bottom: 3rem;
}
\ No newline at end of file
diff --git a/desktop/views/marketplace.html b/desktop/views/marketplace.html
index 69933c6..3a18cf1 100644
--- a/desktop/views/marketplace.html
+++ b/desktop/views/marketplace.html
@@ -10,13 +10,13 @@
-
-
-

-
WaifuBoard
-
+
+
+

+
WaifuBoard
+
@@ -36,9 +36,9 @@
-
+
-
+
@@ -90,47 +90,28 @@
-
-
-
-
-
+
+
+
@@ -143,22 +124,10 @@
-
-
-
+