diff --git a/desktop/src/scripts/marketplace.js b/desktop/src/scripts/marketplace.js index a3598ac..f83a6c4 100644 --- a/desktop/src/scripts/marketplace.js +++ b/desktop/src/scripts/marketplace.js @@ -1,242 +1,314 @@ -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 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'; -const marketplaceContent = document.getElementById('marketplace-content'); -const filterSelect = document.getElementById('extension-filter'); -const updateAllBtn = document.getElementById('btn-update-all'); +// DOM Elements +const dom = { + content: document.getElementById('marketplace-content'), + configPanel: document.getElementById('config-panel'), + inputUrl: document.getElementById('source-url-input'), -const modal2 = document.getElementById('customModal'); -const modalTitle = document.getElementById('modalTitle'); -const modalMessage = document.getElementById('modalMessage'); -const modalConfirmBtn = document.getElementById('modalConfirmButton'); -const modalCloseBtn = document.getElementById('modalCloseButton'); + // Filters & Toggles + filterType: document.getElementById('filter-type'), + filterLang: document.getElementById('filter-lang'), + btnNsfw: document.getElementById('btn-toggle-nsfw'), -let marketplaceMetadata = {}; -let installedExtensions = []; -let currentTab = 'marketplace'; + // Buttons + btnConfig: document.getElementById('btn-configure'), + btnSaveSource: document.getElementById('btn-save-source'), + btnResetSource: document.getElementById('btn-reset-source'), + btnUpdateAll: document.getElementById('btn-update-all'), -async function loadMarketplace() { - showSkeletons(); + // 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(MARKETPLACE_JSON_URL).then(res => res.json()), - fetch(INSTALLED_EXTENSIONS_API).then(res => res.json()) + fetch(proxyUrl).then(r => r.ok ? r.json() : null), + fetch(INSTALLED_EXTENSIONS_API).then(r => r.json()) ]); - marketplaceMetadata = metaRes.extensions; - installedExtensions = (installedRes.extensions || []).map(e => e.toLowerCase()); + if (!metaRes) throw new Error('Failed to fetch marketplace JSON'); - initTabs(); - renderGroupedView(); + state.metadata = metaRes.extensions || {}; + state.installed = (installedRes.extensions || []).map(e => e.toLowerCase()); - if (filterSelect) { - filterSelect.addEventListener('change', () => renderGroupedView()); - } - - if (updateAllBtn) { - updateAllBtn.onclick = handleUpdateAll; - } + render(); } catch (error) { - console.error('Error loading marketplace:', error); - marketplaceContent.innerHTML = `
Error al cargar el marketplace.
`; + console.error(error); + renderError(error.message); } } -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; +/* --- Rendering --- */ +function render() { + dom.content.innerHTML = ''; + const typeFilter = dom.filterType.value; + const langFilter = dom.filterLang.value; - if (updateAllBtn) { - if (currentTab === 'installed') { - updateAllBtn.classList.remove('hidden'); - } else { - updateAllBtn.classList.add('hidden'); - } - } + let items = []; - renderGroupedView(); - }; - }); -} - -async function handleUpdateAll() { - const originalText = updateAllBtn.innerText; - try { - updateAllBtn.disabled = true; - updateAllBtn.innerText = 'Updating...'; - - const res = await fetch(UPDATE_EXTENSIONS_API, { method: 'POST' }); - if (!res.ok) throw new Error('Update failed'); - - const data = await res.json(); - - if (data.updated && data.updated.length > 0) { - - const list = data.updated.join(', '); - window.NotificationUtils.success(`Updated: ${list}`); - - await loadMarketplace(); - } else { - window.NotificationUtils.info('Everything is up to date.'); - } - } catch (error) { - console.error('Update All Error:', error); - window.NotificationUtils.error('Failed to perform bulk update.'); - } finally { - updateAllBtn.disabled = false; - updateAllBtn.innerText = originalText; - } -} - -function renderGroupedView() { - marketplaceContent.innerHTML = ''; - const activeFilter = filterSelect.value; - const groups = {}; - - let listToRender = []; - - if (currentTab === 'marketplace') { - for (const [id, data] of Object.entries(marketplaceMetadata)) { - listToRender.push({ - id, - ...data, - isInstalled: installedExtensions.includes(id.toLowerCase()) - }); - } + // 1. Prepare items + if (state.currentTab === 'marketplace') { + items = Object.entries(state.metadata).map(([id, data]) => ({ + id, ...data, isInstalled: state.installed.includes(id.toLowerCase()) + })); } else { - for (const [id, data] of Object.entries(marketplaceMetadata)) { - if (installedExtensions.includes(id.toLowerCase())) { - listToRender.push({ id, ...data, isInstalled: true }); - } - } + const metaItems = Object.entries(state.metadata) + .filter(([id]) => state.installed.includes(id.toLowerCase())) + .map(([id, data]) => ({ 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), + items = [...metaItems]; + + state.installed.forEach(instId => { + if (!items.find(i => i.id.toLowerCase() === instId)) { + items.push({ + id: instId, + name: capitalize(instId), type: 'Local', - author: 'Unknown', - isInstalled: true + description: 'Locally installed.', + author: '?', + lang: 'Local', + isInstalled: true, + isLocal: true }); } }); } - listToRender.forEach(ext => { - const type = ext.type || 'Other'; - if (activeFilter !== 'All' && type !== activeFilter) return; - if (!groups[type]) groups[type] = []; - groups[type].push(ext); + // 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 sortedTypes = Object.keys(groups).sort(); + const grouped = groupBy(items, 'type'); + const types = Object.keys(grouped).sort(); - if (sortedTypes.length === 0) { - marketplaceContent.innerHTML = `

No extensions found for this criteria.

`; - return; - } + if (types.length === 0) return renderEmptyState(); - sortedTypes.forEach(type => { + types.forEach(type => { const section = document.createElement('div'); - section.className = 'category-group'; - + section.className = 'mp-section'; const title = document.createElement('h2'); - title.className = 'marketplace-section-title'; - title.innerText = type.replace('-', ' '); - + title.className = 'category-title'; + title.innerText = formatType(type); const grid = document.createElement('div'); - grid.className = 'marketplace-grid'; - - groups[type].forEach(ext => grid.appendChild(createCard(ext))); - + grid.className = 'mp-grid'; + grouped[type].forEach(ext => grid.appendChild(createCard(ext))); section.appendChild(title); section.appendChild(grid); - marketplaceContent.appendChild(section); + dom.content.appendChild(section); }); } function createCard(ext) { const card = document.createElement('div'); - card.className = `extension-card ${ext.nsfw ? 'nsfw-ext' : ''} ${ext.broken ? 'broken-ext' : ''}`; + card.className = `mp-card ${ext.nsfw ? 'card-nsfw' : ''}`; - const iconUrl = `https://www.google.com/s2/favicons?domain=${ext.domain}&sz=128`; + // Icon logic + const iconSrc = ext.icon || '/public/assets/waifuboards.ico'; + + // Button Logic + let btnClass = 'btn-primary'; + let btnText = 'Install'; - let buttonHtml = ''; if (ext.isInstalled) { - buttonHtml = ``; + btnClass = 'btn-danger'; + btnText = 'Uninstall'; } else if (ext.broken) { - buttonHtml = ``; - } else { - buttonHtml = ``; + 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.name}

- by ${ext.author || 'Unknown'} -

${ext.description || 'No description available.'}

-
- - ${ext.isInstalled ? 'Installed' : (ext.broken ? 'Broken' : 'Available')} - - ${ext.nsfw ? 'NSFW' : ''} +
+ +
+

${ext.name}

+ by ${ext.author || 'Unknown'}
- ${buttonHtml} + +
+ ${langHtml} + ${ext.isInstalled ? 'INSTALLED' : ''} + ${ext.nsfw ? 'NSFW' : ''} +
+ +

${ext.description || 'No description provided.'}

+ +
+ +
`; - const btn = card.querySelector('.extension-action-button'); + const btn = card.querySelector('button'); if (!ext.broken || ext.isInstalled) { - btn.onclick = () => ext.isInstalled ? promptUninstall(ext) : handleInstall(ext); + btn.onclick = () => ext.isInstalled ? confirmUninstall(ext) : installExtension(ext); } return card; } -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 { - modalConfirmBtn.classList.add('hidden'); - } - modalCloseBtn.onclick = hideModal; - modal2.classList.remove('hidden'); -} - -function hideModal() { modal2.classList.add('hidden'); } - -async function handleInstall(ext) { +/* --- Actions & Helpers --- */ +async function installExtension(ext) { + if (!ext.entry) return window.NotificationUtils.error('Invalid extension entry point'); try { + window.NotificationUtils.info(`Installing ${ext.name}...`); 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(); - window.NotificationUtils.success(`${ext.name} installed!`); - } - } catch (e) { window.NotificationUtils.error('Install failed.'); } + window.NotificationUtils.success(`${ext.name} Installed!`); + state.installed.push(ext.id.toLowerCase()); + render(); + } else throw new Error('Install failed'); + } catch (e) { window.NotificationUtils.error(`Failed to install ${ext.name}`); } } -function promptUninstall(ext) { - showModal('Confirm', `Uninstall ${ext.name}?`, true, () => handleUninstall(ext)); +function confirmUninstall(ext) { + showModal('Uninstall Extension', `Remove ${ext.name}?`, true, () => uninstallExtension(ext)); } -async function handleUninstall(ext) { +async function uninstallExtension(ext) { try { const res = await fetch('/api/extensions/uninstall', { method: 'POST', @@ -244,19 +316,53 @@ async function handleUninstall(ext) { body: JSON.stringify({ fileName: ext.id + '.js' }) }); if (res.ok) { - installedExtensions = installedExtensions.filter(id => id !== ext.id.toLowerCase()); - renderGroupedView(); - window.NotificationUtils.info(`${ext.name} uninstalled.`); + state.installed = state.installed.filter(id => id !== ext.id.toLowerCase()); + window.NotificationUtils.success(`${ext.name} removed.`); + render(); } - } catch (e) { window.NotificationUtils.error('Uninstall failed.'); } + } catch (e) { window.NotificationUtils.error('Uninstall failed'); } } -function showSkeletons() { - marketplaceContent.innerHTML = ` -
- ${Array(3).fill('
').join('')} +async function handleUpdateAll() { + const btn = dom.btnUpdateAll; + const oldText = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = 'Updating...'; + try { + const res = await fetch(UPDATE_EXTENSIONS_API, { method: 'POST' }); + const data = await res.json(); + if (data.updated?.length) { + window.NotificationUtils.success(`Updated ${data.updated.length} extensions.`); + loadData(); + } else window.NotificationUtils.info('All up to date.'); + } catch (e) { window.NotificationUtils.error('Bulk update failed.'); } + finally { btn.disabled = false; btn.innerHTML = oldText; } +} + +function renderWelcomeState() { + dom.content.innerHTML = ` +
+
🔌
+

Configure Source

+

Configure a source URL to start.

+
`; } -document.addEventListener('DOMContentLoaded', loadMarketplace); \ No newline at end of file +function renderEmptyState() { + dom.content.innerHTML = `
🔍

No extensions found.

`; +} +function renderLoading() { dom.content.innerHTML = '

Loading...

'; } +function renderError(msg) { dom.content.innerHTML = `

Error: ${msg}

`; } +function showModal(title, msg, hasConfirm, onConfirm) { + dom.modalTitle.innerText = title; dom.modalMsg.innerText = msg; dom.modal.classList.remove('hidden'); + if (hasConfirm) { dom.modalConfirm.classList.remove('hidden'); dom.modalConfirm.onclick = () => { hideModal(); if (onConfirm) onConfirm(); }; } + else dom.modalConfirm.classList.add('hidden'); +} +function hideModal() { dom.modal.classList.add('hidden'); } +function groupBy(arr, key) { return arr.reduce((acc, i) => ((acc[i[key] || 'Other'] = acc[i[key] || 'Other'] || []).push(i), acc), {}); } +function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } +function formatType(t) { return t.replace(/-/g, ' ').toUpperCase(); } + +document.addEventListener('DOMContentLoaded', init); \ No newline at end of file diff --git a/desktop/views/css/marketplace.css b/desktop/views/css/marketplace.css index b415b38..1873cb3 100644 --- a/desktop/views/css/marketplace.css +++ b/desktop/views/css/marketplace.css @@ -1,390 +1,267 @@ -.hero-spacer { - height: var(--nav-height); - width: 100%; +:root { + --bg-base: #0b0b0b; + --bg-card: rgba(255, 255, 255, 0.03); + --bg-card-hover: rgba(255, 255, 255, 0.08); + --border-subtle: rgba(255, 255, 255, 0.08); + --color-primary: #8b5cf6; + --color-primary-glow: rgba(139, 92, 246, 0.5); + --color-text-main: #ffffff; + --color-text-muted: #a1a1aa; + --nav-height: 70px; } -.marketplace-subtitle { - font-size: 1.1rem; - color: var(--color-text-secondary); -} - -.filter-controls { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.filter-label { - font-size: 0.9rem; - color: var(--color-text-secondary); - font-weight: 600; -} - -.filter-select { - padding: 0.6rem 2rem 0.6rem 1.25rem; - border-radius: 999px; - background: var(--color-bg-elevated-hover); - color: var(--color-text-primary); - border: 1px solid rgba(255,255,255,0.1); - appearance: none; - font-weight: 600; - - background-image: url('data:image/svg+xml;utf8,'); - background-repeat: no-repeat; - background-position: right 0.75rem center; - background-size: 1em; - cursor: pointer; - transition: all 0.2s; -} - -.filter-select:hover { - border-color: var(--color-primary); - box-shadow: 0 0 8px var(--color-primary-glow); -} - -.marketplace-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 1rem; - padding-top: 1rem; -} - -.extension-card { - background: var(--bg-surface); - border: 1px solid rgba(255,255,255,0.05); - border-radius: var(--radius-md); - padding: 1rem; - display: flex; - flex-direction: column; - align-items: flex-start; - transition: all 0.2s; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); - min-height: 140px; - position: relative; - overflow: hidden; -} - -.extension-card:hover { - background: var(--color-bg-elevated-hover); - transform: translateY(-4px); - box-shadow: 0 8px 25px rgba(0,0,0,0.5); -} - -.card-content-wrapper { - flex-grow: 1; - margin-bottom: 0.5rem; -} - -.extension-icon { - width: 50px; - height: 50px; - border-radius: 8px; - object-fit: contain; - margin-bottom: 0.75rem; - border: 2px solid var(--color-primary); - background-color: var(--color-bg-base); - flex-shrink: 0; - box-shadow: 0 0 10px var(--color-primary-glow); -} - -.extension-name { - font-size: 1.1rem; - font-weight: 700; +body { + background-color: var(--bg-base); + color: var(--color-text-main); + font-family: system-ui, -apple-system, sans-serif; margin: 0; - color: var(--color-text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: 100%; + overflow-x: hidden; } -.extension-status-badge { - font-size: 0.75rem; - font-weight: 600; - padding: 0.15rem 0.5rem; - border-radius: 999px; - margin-top: 0.4rem; - display: inline-block; - letter-spacing: 0.5px; +.nav-spacer { height: var(--nav-height); width: 100%; } + +.content-container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem 1.5rem 5rem; } -.badge-installed { - background: rgba(34, 197, 94, 0.2); - color: #4ade80; - border: 1px solid rgba(34, 197, 94, 0.3); -} +/* --- Header & Config --- */ +.mp-header { margin-bottom: 3rem; } -.badge-local { - background: rgba(251, 191, 36, 0.2); - color: #fcd34d; - border: 1px solid rgba(251, 191, 36, 0.3); -} - -.extension-action-button { - width: 100%; - padding: 0.6rem 1rem; - border-radius: 999px; - font-weight: 700; - font-size: 0.9rem; - border: none; - cursor: pointer; - transition: background 0.2s, transform 0.2s, box-shadow 0.2s; - margin-top: auto; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.btn-install { - background: var(--color-primary); - color: white; -} -.btn-install:hover { - background: #a78bfa; - transform: scale(1.02); - box-shadow: 0 0 15px var(--color-primary-glow); -} - -.btn-uninstall { - background: #dc2626; - color: white; - margin-top: auto; -} - -.btn-uninstall:hover { - background: #ef4444; - transform: scale(1.02); - box-shadow: 0 0 15px rgba(220, 38, 38, 0.4); -} - -.extension-card.skeleton { +.header-top { display: flex; - flex-direction: column; - align-items: flex-start; justify-content: space-between; - - box-shadow: none; - transform: none; -} -.extension-card.skeleton:hover { - background: var(--bg-surface); - box-shadow: none; - transform: none; -} -.skeleton-icon { - width: 50px; - height: 50px; - border-radius: 8px; - margin-bottom: 0.75rem; - - border: 2px solid rgba(255,255,255,0.05); -} -.skeleton-text.title-skeleton { - height: 1.1em; - margin-bottom: 0.25rem; -} -.skeleton-text.text-skeleton { - height: 0.7em; - margin-bottom: 0; -} -.skeleton-button { - width: 100%; - height: 32px; - border-radius: 999px; - margin-top: auto; -} - -.section-title { - font-size: 1.8rem; - font-weight: 800; - color: var(--color-text-primary); -} - -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.85); - backdrop-filter: blur(5px); - display: flex; - justify-content: center; align-items: center; - z-index: 5000; - opacity: 0; - pointer-events: none; - transition: opacity 0.3s ease-in-out; -} - -.modal-overlay:not(.hidden) { - opacity: 1; - pointer-events: auto; -} - -.modal-content { - background: var(--bg-surface); - color: var(--color-text-primary); - padding: 2.5rem; - border-radius: var(--radius-lg); - width: 90%; - max-width: 450px; - box-shadow: 0 15px 50px rgba(0, 0, 0, 0.8), 0 0 20px var(--color-primary-glow); - border: 1px solid rgba(255,255,255,0.1); - transform: translateY(-50px); - transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.3s ease-in-out; - opacity: 0; -} - -.modal-overlay:not(.hidden) .modal-content { - transform: translateY(0); - opacity: 1; -} - -#modalTitle { - font-size: 1.6rem; - font-weight: 800; - margin-top: 0; - color: var(--color-primary); - border-bottom: 2px solid rgba(255,255,255,0.05); - padding-bottom: 0.75rem; - margin-bottom: 1.5rem; -} - -#modalMessage { - font-size: 1rem; - line-height: 1.6; - color: var(--color-text-secondary); margin-bottom: 2rem; } -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 1rem; -} - -.modal-button { - - padding: 0.6rem 1.5rem; - border-radius: 999px; - font-weight: 700; - font-size: 0.9rem; - border: none; - cursor: pointer; - transition: background 0.2s, transform 0.2s; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.modal-button.btn-install { - background: var(--color-primary); - color: white; -} -.modal-button.btn-install:hover { - background: #a78bfa; - transform: scale(1.02); -} - -.modal-button.btn-uninstall { - background: #dc2626; - color: white; -} -.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; +.page-title { + font-size: 2.5rem; font-weight: 800; - margin: 2rem 0 1rem 0; - color: var(--color-text-primary); + margin: 0; + background: linear-gradient(to right, #fff, #a1a1aa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* Buttons */ +.btn-primary, .btn-secondary, .btn-danger { + padding: 0.6rem 1.2rem; + border-radius: 8px; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; display: flex; align-items: center; - gap: 0.5rem; - text-transform: capitalize; + gap: 8px; + transition: all 0.2s ease; + border: 1px solid transparent; } -.marketplace-section-title::before { - content: ''; - display: inline-block; - width: 4px; - height: 20px; +.btn-primary { background: var(--color-primary); - border-radius: 2px; + color: white; + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.2); +} +.btn-primary:hover { filter: brightness(1.1); transform: translateY(-1px); } + +.btn-secondary { + background: rgba(255,255,255,0.05); + color: white; + border-color: var(--border-subtle); +} +.btn-secondary:hover { background: rgba(255,255,255,0.1); } + +.btn-danger { + background: rgba(220, 38, 38, 0.2); + color: #f87171; + border-color: rgba(220, 38, 38, 0.3); +} +.btn-danger:hover { background: rgba(220, 38, 38, 0.3); } + +/* Config Panel */ +.config-panel { + background: rgba(20, 20, 25, 0.95); + border: 1px solid var(--border-subtle); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 2rem; + animation: slideDown 0.3s ease-out; +} +.config-panel.hidden { display: none; } +@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } + +.config-desc { color: var(--color-text-muted); font-size: 0.9rem; margin-top: 0.5rem; } +.input-group { display: flex; gap: 10px; margin: 1rem 0; } +.input-group input { + flex: 1; + background: rgba(0,0,0,0.3); + border: 1px solid var(--border-subtle); + border-radius: 8px; + padding: 0.8rem 1rem; + color: white; + font-family: inherit; +} +.input-group input:focus { outline: none; border-color: var(--color-primary); } +.btn-text { background: none; border: none; color: var(--color-text-muted); text-decoration: underline; cursor: pointer; font-size: 0.85rem; } + +/* Tabs */ +.tabs-wrapper { + display: flex; + gap: 2rem; + border-bottom: 1px solid var(--border-subtle); + margin-bottom: 2rem; +} +.tab-btn { + background: none; border: none; + color: var(--color-text-muted); + font-size: 1.1rem; + font-weight: 700; + padding: 0.8rem 0; + cursor: pointer; + position: relative; + transition: color 0.2s; +} +.tab-btn.active { color: white; } +.tab-btn.active::after { + content: ''; position: absolute; bottom: -1px; left: 0; width: 100%; height: 2px; + background: var(--color-primary); box-shadow: 0 0 10px var(--color-primary-glow); } -.category-group { - margin-bottom: 3rem; +/* Toolbar */ +.toolbar { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: flex-end; justify-content: space-between; margin-bottom: 2rem; } +.filter-group { display: flex; gap: 1.5rem; } +.select-wrapper { display: flex; flex-direction: column; gap: 6px; } +.select-wrapper label { font-size: 0.75rem; font-weight: 700; color: var(--color-text-muted); text-transform: uppercase; } +.select-wrapper select { + background: var(--bg-card); + border: 1px solid var(--border-subtle); + color: white; + padding: 0.6rem 2.5rem 0.6rem 1rem; + border-radius: 8px; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 16px; + font-weight: 600; + min-width: 140px; +} +.select-wrapper select:hover { background-color: var(--bg-card-hover); } + +/* --- Grid & Cards --- */ +.category-title { + font-size: 1.4rem; font-weight: 700; margin: 2.5rem 0 1rem; + color: white; display: flex; align-items: center; gap: 0.8rem; +} +.category-title::before { content: ''; display: block; width: 6px; height: 6px; background: var(--color-primary); border-radius: 50%; box-shadow: 0 0 10px var(--color-primary); } + +.mp-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.mp-card { + background: var(--bg-card); + border: 1px solid var(--border-subtle); + border-radius: 16px; + padding: 1.2rem; + display: flex; + flex-direction: column; + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); + position: relative; + overflow: hidden; +} + +.mp-card:hover { + transform: translateY(-5px); + background: var(--bg-card-hover); + border-color: rgba(255,255,255,0.2); + box-shadow: 0 10px 30px rgba(0,0,0,0.3); +} + +.card-header { display: flex; gap: 1rem; margin-bottom: 1rem; } +.card-icon { + width: 56px; height: 56px; border-radius: 12px; + background: #000; object-fit: contain; + border: 1px solid var(--border-subtle); +} +.card-info { flex: 1; min-width: 0; } +.card-title { font-size: 1.1rem; font-weight: 700; margin: 0 0 4px 0; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.card-author { font-size: 0.85rem; color: var(--color-text-muted); } + +.card-desc { + font-size: 0.9rem; color: #ccc; line-height: 1.5; + margin-bottom: 1.2rem; flex: 1; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; +} + +.card-meta { display: flex; gap: 0.5rem; margin-bottom: 1.2rem; flex-wrap: wrap; } +.pill { + font-size: 0.7rem; font-weight: 700; padding: 2px 8px; border-radius: 4px; + border: 1px solid transparent; text-transform: uppercase; +} + +/* Specific Pills */ +.pill.lang-multi { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border-color: rgba(59, 130, 246, 0.3); } +.pill.lang-es { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border-color: rgba(245, 158, 11, 0.3); } +.pill.lang-en { background: rgba(16, 185, 129, 0.15); color: #34d399; border-color: rgba(16, 185, 129, 0.3); } +.pill.lang-jp { background: rgba(236, 72, 153, 0.15); color: #f472b6; border-color: rgba(236, 72, 153, 0.3); } + +.pill.status-installed { background: rgba(139, 92, 246, 0.15); color: #a78bfa; border-color: rgba(139, 92, 246, 0.3); } +.pill.nsfw { background: rgba(220, 38, 38, 0.1); color: #ef4444; border-color: rgba(220, 38, 38, 0.3); } + +.card-actions { margin-top: auto; } +.btn-card { width: 100%; justify-content: center; } + +/* Empty States */ +.empty-state { + text-align: center; padding: 4rem 1rem; + background: var(--bg-card); border-radius: 16px; border: 1px dashed var(--border-subtle); +} +.empty-icon { width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5; } +.empty-text { font-size: 1.2rem; color: var(--color-text-muted); } + +/* Modal */ +.modal-overlay { + position: fixed; top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.8); backdrop-filter: blur(5px); + display: flex; align-items: center; justify-content: center; z-index: 5000; +} +.modal-overlay.hidden { display: none; } +.modal-box { + background: #18181b; border: 1px solid var(--border-subtle); + padding: 2rem; border-radius: 16px; width: 90%; max-width: 450px; + box-shadow: 0 20px 50px rgba(0,0,0,0.7); +} +.modal-box h3 { margin-top: 0; color: white; } +.modal-box p { color: #ccc; line-height: 1.5; } +.modal-footer { display: flex; justify-content: flex-end; gap: 1rem; margin-top: 2rem; } + +/* Añadir al final del CSS existente */ + +.mp-card.card-nsfw { + border-color: rgba(220, 38, 38, 0.2); + background: rgba(220, 38, 38, 0.03); +} + +.mp-card.card-nsfw:hover { + background: rgba(220, 38, 38, 0.08); + border-color: rgba(220, 38, 38, 0.4); +} + +.select-wrapper select option { + background-color: #18181b; /* Color oscuro sólido (Zinc-900) */ + color: #ffffff; /* Texto blanco */ + padding: 10px; /* Espaciado (si el navegador lo permite) */ +} + +/* Opcional: Para navegadores basados en Webkit que permitan algo de estilo en hover */ +.select-wrapper select option:checked, +.select-wrapper select option:hover { + background-color: var(--color-primary); + color: white; } \ No newline at end of file diff --git a/desktop/views/marketplace.html b/desktop/views/marketplace.html index fcf1a05..f9d10d1 100644 --- a/desktop/views/marketplace.html +++ b/desktop/views/marketplace.html @@ -8,48 +8,94 @@ - + -
-
-
-
-
- - -
+ -
- - -
-
- -
-
+ + + +
+ + +
+ +
+
+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ + +