diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 570eb130..a032fa6b 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -896,47 +896,40 @@ window.currentPluginConfig = null; let pluginStoreCache = null; // Cache for plugin store to speed up subsequent loads let cacheTimestamp = null; const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds + let storeFilteredList = []; + + // ── Plugin Store Filter State ─────────────────────────────────────────── + const storeFilterState = { + sort: safeLocalStorage.getItem('storeSort') || 'a-z', + filterCategory: '', + filterInstalled: null, // null=all, true=installed, false=not-installed + searchQuery: '', + page: 1, + perPage: parseInt(safeLocalStorage.getItem('storePerPage')) || 12, + persist() { + safeLocalStorage.setItem('storeSort', this.sort); + safeLocalStorage.setItem('storePerPage', this.perPage); + }, + reset() { + this.sort = 'a-z'; + this.filterCategory = ''; + this.filterInstalled = null; + this.searchQuery = ''; + this.page = 1; + }, + activeCount() { + let n = 0; + if (this.searchQuery) n++; + if (this.filterInstalled !== null) n++; + if (this.filterCategory) n++; + if (this.sort !== 'a-z') n++; + return n; + } + }; let onDemandStatusInterval = null; let currentOnDemandPluginId = null; let hasLoadedOnDemandStatus = false; - // Store filter/sort state - const storeFilterState = { - sort: safeLocalStorage.getItem('storeSort') || 'a-z', - filterVerified: false, - filterNew: false, - filterInstalled: null, // null = all, true = installed only, false = not installed only - filterAuthors: [], - filterCategories: [], - - persist() { - safeLocalStorage.setItem('storeSort', this.sort); - }, - - reset() { - this.sort = 'a-z'; - this.filterVerified = false; - this.filterNew = false; - this.filterInstalled = null; - this.filterAuthors = []; - this.filterCategories = []; - this.persist(); - }, - - activeCount() { - let count = 0; - if (this.filterVerified) count++; - if (this.filterNew) count++; - if (this.filterInstalled !== null) count++; - count += this.filterAuthors.length; - count += this.filterCategories.length; - return count; - } - }; - - // Installed plugins sort state - let installedSort = safeLocalStorage.getItem('installedSort') || 'a-z'; - // Shared on-demand status store (mirrors Alpine store when available) window.__onDemandStore = window.__onDemandStore || { loading: true, @@ -1050,13 +1043,15 @@ window.initPluginsPage = function() { // If we fetched data before the DOM existed, render it now if (window.__pendingInstalledPlugins) { console.log('[RENDER] Applying pending installed plugins data'); - sortAndRenderInstalledPlugins(window.__pendingInstalledPlugins); + renderInstalledPlugins(window.__pendingInstalledPlugins); window.__pendingInstalledPlugins = null; } if (window.__pendingStorePlugins) { console.log('[RENDER] Applying pending plugin store data'); - renderPluginStore(window.__pendingStorePlugins); + pluginStoreCache = window.__pendingStorePlugins; + cacheTimestamp = Date.now(); window.__pendingStorePlugins = null; + applyStoreFiltersAndSort(); } initializePlugins(); @@ -1065,7 +1060,6 @@ window.initPluginsPage = function() { const refreshBtn = document.getElementById('refresh-plugins-btn'); const updateAllBtn = document.getElementById('update-all-plugins-btn'); const restartBtn = document.getElementById('restart-display-btn'); - const searchBtn = document.getElementById('search-plugins-btn'); const closeBtn = document.getElementById('close-plugin-config'); const closeOnDemandModalBtn = document.getElementById('close-on-demand-modal'); const cancelOnDemandBtn = document.getElementById('cancel-on-demand'); @@ -1093,10 +1087,13 @@ window.initPluginsPage = function() { document.getElementById('restart-display-btn').addEventListener('click', restartDisplay); console.log('[initPluginsPage] Attached restartDisplay listener'); } - if (searchBtn) { - searchBtn.replaceWith(searchBtn.cloneNode(true)); - document.getElementById('search-plugins-btn').addEventListener('click', searchPluginStore); - } + // Restore persisted store sort/perPage + const storeSortEl = document.getElementById('store-sort'); + if (storeSortEl) storeSortEl.value = storeFilterState.sort; + const storePpEl = document.getElementById('store-per-page'); + if (storePpEl) storePpEl.value = storeFilterState.perPage; + setupStoreFilterListeners(); + if (closeBtn) { closeBtn.replaceWith(closeBtn.cloneNode(true)); document.getElementById('close-plugin-config').addEventListener('click', closePluginConfigModal); @@ -1179,15 +1176,13 @@ function initializePluginPageWhenReady() { document.body.addEventListener('htmx:afterSwap', function(event) { const target = event.detail.target; // Check if plugins content was swapped in - if (target.id === 'plugins-content' || - target.querySelector('[data-plugins-loaded]')) { + if (target.id === 'plugins-content' || + target.querySelector('#installed-plugins-grid') || + document.getElementById('installed-plugins-grid')) { console.log('HTMX swap detected for plugins, initializing...'); - // Reset initialization flags to allow re-initialization after HTMX swap + // Reset initialization flag to allow re-initialization after HTMX swap window.pluginManager.initialized = false; window.pluginManager.initializing = false; - pluginsInitialized = false; - pluginLoadCache.data = null; - pluginLoadCache.promise = null; initTimer = setTimeout(attemptInit, 100); } }, { once: false }); // Allow multiple swaps @@ -1248,10 +1243,7 @@ function initializePlugins() { categorySelect._listenerSetup = true; categorySelect.addEventListener('change', searchPluginStore); } - - // Setup store sort/filter controls - setupStoreFilterListeners(); - + // Setup GitHub installation handlers console.log('[initializePlugins] About to call setupGitHubInstallHandlers...'); if (typeof setupGitHubInstallHandlers === 'function') { @@ -1306,7 +1298,7 @@ function loadInstalledPlugins(forceRefresh = false) { })); pluginLog('[CACHE] Dispatched pluginsUpdated event from cache'); // Still render to ensure UI is updated - sortAndRenderInstalledPlugins(pluginLoadCache.data); + renderInstalledPlugins(pluginLoadCache.data); return Promise.resolve(pluginLoadCache.data); } @@ -1365,7 +1357,7 @@ function loadInstalledPlugins(forceRefresh = false) { }); } - sortAndRenderInstalledPlugins(installedPlugins); + renderInstalledPlugins(installedPlugins); // Update count const countEl = document.getElementById('installed-count'); @@ -1406,24 +1398,6 @@ function refreshInstalledPlugins() { window.pluginManager.loadInstalledPlugins = loadInstalledPlugins; // Note: searchPluginStore will be exposed after its definition (see below) -function sortAndRenderInstalledPlugins(plugins) { - const sorted = [...plugins].sort((a, b) => { - const nameA = (a.name || a.id || '').toLowerCase(); - const nameB = (b.name || b.id || '').toLowerCase(); - switch (installedSort) { - case 'z-a': - return nameB.localeCompare(nameA); - case 'enabled': - if (a.enabled !== b.enabled) return a.enabled ? -1 : 1; - return nameA.localeCompare(nameB); - case 'a-z': - default: - return nameA.localeCompare(nameB); - } - }); - renderInstalledPlugins(sorted); -} - function renderInstalledPlugins(plugins) { const container = document.getElementById('installed-plugins-grid'); if (!container) { @@ -1496,6 +1470,7 @@ function renderInstalledPlugins(plugins) {

${escapeHtml(plugin.name || plugin.id)}

+ ${plugin.is_starlark_app ? 'Starlark' : ''} ${plugin.verified ? 'Verified' : ''}
@@ -1707,18 +1682,37 @@ function handlePluginAction(event) { }); break; case 'uninstall': - waitForFunction('uninstallPlugin', 10, 50) - .then(uninstallFunc => { - uninstallFunc(pluginId); - }) - .catch(error => { - console.error('[EVENT DELEGATION]', error.message); - if (typeof showNotification === 'function') { - showNotification('Uninstall function not loaded. Please refresh the page.', 'error'); - } else { - alert('Uninstall function not loaded. Please refresh the page.'); - } - }); + if (pluginId.startsWith('starlark:')) { + // Starlark app uninstall uses dedicated endpoint + const starlarkAppId = pluginId.slice('starlark:'.length); + if (!confirm(`Uninstall Starlark app "${starlarkAppId}"?`)) break; + fetch(`/api/v3/starlark/apps/${encodeURIComponent(starlarkAppId)}`, {method: 'DELETE'}) + .then(r => r.json()) + .then(data => { + if (data.status === 'success') { + if (typeof showNotification === 'function') showNotification('Starlark app uninstalled', 'success'); + else alert('Starlark app uninstalled'); + if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins(); + else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins(); + } else { + alert('Uninstall failed: ' + (data.message || 'Unknown error')); + } + }) + .catch(err => alert('Uninstall failed: ' + err.message)); + } else { + waitForFunction('uninstallPlugin', 10, 50) + .then(uninstallFunc => { + uninstallFunc(pluginId); + }) + .catch(error => { + console.error('[EVENT DELEGATION]', error.message); + if (typeof showNotification === 'function') { + showNotification('Uninstall function not loaded. Please refresh the page.', 'error'); + } else { + alert('Uninstall function not loaded. Please refresh the page.'); + } + }); + } break; } } @@ -1779,8 +1773,6 @@ function startOnDemandStatusPolling() { window.loadOnDemandStatus = loadOnDemandStatus; -let updateAllRunning = false; - async function runUpdateAllPlugins() { console.log('[runUpdateAllPlugins] Button clicked, checking for updates...'); const button = document.getElementById('update-all-plugins-btn'); @@ -1790,7 +1782,7 @@ async function runUpdateAllPlugins() { return; } - if (updateAllRunning) { + if (button.dataset.running === 'true') { return; } @@ -1801,7 +1793,7 @@ async function runUpdateAllPlugins() { } const originalContent = button.innerHTML; - updateAllRunning = true; + button.dataset.running = 'true'; button.disabled = true; button.classList.add('opacity-60', 'cursor-wait'); @@ -1811,11 +1803,7 @@ async function runUpdateAllPlugins() { for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; const pluginId = plugin.id; - // Re-fetch button in case DOM was replaced by HTMX swap - const btn = document.getElementById('update-all-plugins-btn'); - if (btn) { - btn.innerHTML = `Updating ${i + 1}/${plugins.length}...`; - } + button.innerHTML = `Updating ${i + 1}/${plugins.length}...`; try { const response = await fetch('/api/v3/plugins/update', { @@ -1855,13 +1843,10 @@ async function runUpdateAllPlugins() { console.error('Bulk plugin update failed:', error); showNotification('Failed to update all plugins: ' + error.message, 'error'); } finally { - updateAllRunning = false; - const btn = document.getElementById('update-all-plugins-btn'); - if (btn) { - btn.innerHTML = originalContent; - btn.disabled = false; - btn.classList.remove('opacity-60', 'cursor-wait'); - } + button.innerHTML = originalContent; + button.disabled = false; + button.classList.remove('opacity-60', 'cursor-wait'); + button.dataset.running = 'false'; } } @@ -5143,7 +5128,7 @@ function handleUninstallSuccess(pluginId) { if (typeof installedPlugins !== 'undefined') { installedPlugins = updatedPlugins; } - sortAndRenderInstalledPlugins(updatedPlugins); + renderInstalledPlugins(updatedPlugins); showNotification(`Plugin uninstalled successfully`, 'success'); // Also refresh from server to ensure consistency @@ -5182,405 +5167,88 @@ function restartDisplay() { }); } -// --- Store Filter/Sort Functions --- - -function setupStoreFilterListeners() { - // Sort dropdown - const sortSelect = document.getElementById('store-sort'); - if (sortSelect && !sortSelect._listenerSetup) { - sortSelect._listenerSetup = true; - sortSelect.value = storeFilterState.sort; - sortSelect.addEventListener('change', () => { - storeFilterState.sort = sortSelect.value; - storeFilterState.persist(); - applyStoreFiltersAndSort(); - }); - } - - // Verified filter toggle - const verifiedBtn = document.getElementById('filter-verified'); - if (verifiedBtn && !verifiedBtn._listenerSetup) { - verifiedBtn._listenerSetup = true; - verifiedBtn.addEventListener('click', () => { - storeFilterState.filterVerified = !storeFilterState.filterVerified; - verifiedBtn.dataset.active = storeFilterState.filterVerified; - applyStoreFiltersAndSort(); - }); - } - - // New filter toggle - const newBtn = document.getElementById('filter-new'); - if (newBtn && !newBtn._listenerSetup) { - newBtn._listenerSetup = true; - newBtn.addEventListener('click', () => { - storeFilterState.filterNew = !storeFilterState.filterNew; - newBtn.dataset.active = storeFilterState.filterNew; - applyStoreFiltersAndSort(); - }); - } - - // Installed filter (cycles: All -> Installed -> Not Installed -> All) - const installedBtn = document.getElementById('filter-installed'); - if (installedBtn && !installedBtn._listenerSetup) { - installedBtn._listenerSetup = true; - installedBtn.addEventListener('click', () => { - const states = [null, true, false]; - const labels = ['All', 'Installed', 'Not Installed']; - const icons = ['fa-download', 'fa-check', 'fa-times']; - const current = states.indexOf(storeFilterState.filterInstalled); - const next = (current + 1) % states.length; - storeFilterState.filterInstalled = states[next]; - installedBtn.querySelector('span').textContent = labels[next]; - installedBtn.querySelector('i').className = `fas ${icons[next]} mr-1`; - installedBtn.dataset.active = String(states[next] !== null); - applyStoreFiltersAndSort(); - }); - } - - // Author dropdown - const authorSelect = document.getElementById('filter-author'); - if (authorSelect && !authorSelect._listenerSetup) { - authorSelect._listenerSetup = true; - authorSelect.addEventListener('change', () => { - storeFilterState.filterAuthors = authorSelect.value - ? [authorSelect.value] : []; - applyStoreFiltersAndSort(); - }); - } - - // Tag pills (event delegation on container) - // Category pills (event delegation on container) - const catsPills = document.getElementById('filter-categories-pills'); - if (catsPills && !catsPills._listenerSetup) { - catsPills._listenerSetup = true; - catsPills.addEventListener('click', (e) => { - const pill = e.target.closest('.category-filter-pill'); - if (!pill) return; - const cat = pill.dataset.category; - const idx = storeFilterState.filterCategories.indexOf(cat); - if (idx >= 0) { - storeFilterState.filterCategories.splice(idx, 1); - pill.dataset.active = 'false'; - } else { - storeFilterState.filterCategories.push(cat); - pill.dataset.active = 'true'; - } - applyStoreFiltersAndSort(); - }); - } - - // Clear filters button - const clearBtn = document.getElementById('clear-filters-btn'); - if (clearBtn && !clearBtn._listenerSetup) { - clearBtn._listenerSetup = true; - clearBtn.addEventListener('click', () => { - storeFilterState.reset(); - // Reset all UI elements - const sort = document.getElementById('store-sort'); - if (sort) sort.value = 'a-z'; - const vBtn = document.getElementById('filter-verified'); - if (vBtn) vBtn.dataset.active = 'false'; - const nBtn = document.getElementById('filter-new'); - if (nBtn) nBtn.dataset.active = 'false'; - const iBtn = document.getElementById('filter-installed'); - if (iBtn) { - iBtn.dataset.active = 'false'; - const span = iBtn.querySelector('span'); - if (span) span.textContent = 'All'; - const icon = iBtn.querySelector('i'); - if (icon) icon.className = 'fas fa-download mr-1'; - } - const auth = document.getElementById('filter-author'); - if (auth) auth.value = ''; - document.querySelectorAll('.category-filter-pill').forEach(p => { - p.dataset.active = 'false'; - }); - applyStoreFiltersAndSort(); - }); - } - - // Installed plugins sort dropdown - const installedSortSelect = document.getElementById('installed-sort'); - if (installedSortSelect && !installedSortSelect._listenerSetup) { - installedSortSelect._listenerSetup = true; - installedSortSelect.value = installedSort; - installedSortSelect.addEventListener('change', () => { - installedSort = installedSortSelect.value; - safeLocalStorage.setItem('installedSort', installedSort); - const plugins = window.installedPlugins || []; - if (plugins.length > 0) { - sortAndRenderInstalledPlugins(plugins); - } - }); - } -} - -function applyStoreFiltersAndSort(basePlugins) { - const source = basePlugins || pluginStoreCache; - if (!source) return; - - let plugins = [...source]; - const installedIds = new Set( - (window.installedPlugins || []).map(p => p.id) - ); - - // Apply filters - if (storeFilterState.filterVerified) { - plugins = plugins.filter(p => p.verified); - } - if (storeFilterState.filterNew) { - plugins = plugins.filter(p => isNewPlugin(p.last_updated)); - } - if (storeFilterState.filterInstalled === true) { - plugins = plugins.filter(p => installedIds.has(p.id)); - } else if (storeFilterState.filterInstalled === false) { - plugins = plugins.filter(p => !installedIds.has(p.id)); - } - if (storeFilterState.filterAuthors.length > 0) { - const authorSet = new Set(storeFilterState.filterAuthors); - plugins = plugins.filter(p => authorSet.has(p.author)); - } - if (storeFilterState.filterCategories.length > 0) { - const catSet = new Set(storeFilterState.filterCategories); - plugins = plugins.filter(p => catSet.has(p.category)); - } - - // Apply sort - switch (storeFilterState.sort) { - case 'a-z': - plugins.sort((a, b) => - (a.name || a.id).localeCompare(b.name || b.id)); - break; - case 'z-a': - plugins.sort((a, b) => - (b.name || b.id).localeCompare(a.name || a.id)); - break; - case 'verified': - plugins.sort((a, b) => - (b.verified ? 1 : 0) - (a.verified ? 1 : 0) || - (a.name || a.id).localeCompare(b.name || b.id)); - break; - case 'newest': - plugins.sort((a, b) => { - // Prefer static per-plugin last_updated over GitHub pushed_at (which is repo-wide) - const dateA = a.last_updated || a.last_updated_iso || ''; - const dateB = b.last_updated || b.last_updated_iso || ''; - return dateB.localeCompare(dateA); - }); - break; - case 'category': - plugins.sort((a, b) => - (a.category || '').localeCompare(b.category || '') || - (a.name || a.id).localeCompare(b.name || b.id)); - break; - } - - renderPluginStore(plugins); - - // Update result count - const countEl = document.getElementById('store-count'); - if (countEl) { - const total = source.length; - const shown = plugins.length; - countEl.innerHTML = shown < total - ? `${shown} of ${total} shown` - : `${total} available`; - } - - updateFilterCountBadge(); -} - -function populateFilterControls() { - if (!pluginStoreCache) return; - - // Collect unique authors - const authors = [...new Set( - pluginStoreCache.map(p => p.author).filter(Boolean) - )].sort(); - - const authorSelect = document.getElementById('filter-author'); - if (authorSelect) { - const currentVal = authorSelect.value; - authorSelect.innerHTML = '' + - authors.map(a => ``).join(''); - authorSelect.value = currentVal; - } - - // Collect unique categories sorted alphabetically - const categories = [...new Set( - pluginStoreCache.map(p => p.category).filter(Boolean) - )].sort(); - - const catsContainer = document.getElementById('filter-categories-container'); - const catsPills = document.getElementById('filter-categories-pills'); - if (catsContainer && catsPills && categories.length > 0) { - catsContainer.classList.remove('hidden'); - catsPills.innerHTML = categories.map(cat => - `` - ).join(''); - } -} - -function updateFilterCountBadge() { - const count = storeFilterState.activeCount(); - const clearBtn = document.getElementById('clear-filters-btn'); - const badge = document.getElementById('filter-count-badge'); - if (clearBtn) { - clearBtn.classList.toggle('hidden', count === 0); - } - if (badge) { - badge.textContent = count; - } -} - function searchPluginStore(fetchCommitInfo = true) { pluginLog('[STORE] Searching plugin store...', { fetchCommitInfo }); - - // Safely get search values (elements may not exist yet) - const searchInput = document.getElementById('plugin-search'); - const categorySelect = document.getElementById('plugin-category'); - const query = searchInput ? searchInput.value : ''; - const category = categorySelect ? categorySelect.value : ''; - // For filtered searches (user typing), we can use cache to avoid excessive API calls - // For initial load or refresh, always fetch fresh metadata - const isFilteredSearch = query || category; const now = Date.now(); const isCacheValid = pluginStoreCache && cacheTimestamp && (now - cacheTimestamp < CACHE_DURATION); - - // Only use cache for filtered searches that don't explicitly request fresh metadata - if (isFilteredSearch && isCacheValid && !fetchCommitInfo) { - console.log('Using cached plugin store data for filtered search'); - // Ensure plugin store grid exists before rendering + + // If cache is valid and we don't need fresh commit info, just re-filter + if (isCacheValid && !fetchCommitInfo) { + console.log('Using cached plugin store data'); const storeGrid = document.getElementById('plugin-store-grid'); - if (!storeGrid) { - console.error('plugin-store-grid element not found, cannot render cached plugins'); - // Don't return, let it fetch fresh data - } else { + if (storeGrid) { applyStoreFiltersAndSort(); return; } } - // Show loading state - safely check element exists + // Show loading state try { const countEl = document.getElementById('store-count'); - if (countEl) { - countEl.innerHTML = 'Loading...'; - } - } catch (e) { - console.warn('Could not update store count:', e); - } + if (countEl) countEl.innerHTML = 'Loading...'; + } catch (e) { /* ignore */ } showStoreLoading(true); let url = '/api/v3/plugins/store/list'; - const params = new URLSearchParams(); - if (query) params.append('query', query); - if (category) params.append('category', category); - // Always fetch fresh commit metadata unless explicitly disabled (for performance on repeated filtered searches) if (!fetchCommitInfo) { - params.append('fetch_commit_info', 'false'); - } - // Note: fetch_commit_info defaults to true on the server side to keep metadata fresh - - if (params.toString()) { - url += '?' + params.toString(); + url += '?fetch_commit_info=false'; } console.log('Store URL:', url); fetch(url) - .then(response => { - console.log('Store response:', response.status); - return response.json(); - }) + .then(response => response.json()) .then(data => { - console.log('Store data:', data); showStoreLoading(false); - + if (data.status === 'success') { const plugins = data.data.plugins || []; console.log('Store plugins count:', plugins.length); - - // Cache the results if no filters - if (!query && !category) { - pluginStoreCache = plugins; - cacheTimestamp = Date.now(); - console.log('Cached plugin store data'); - populateFilterControls(); - } - // Ensure plugin store grid exists before rendering + pluginStoreCache = plugins; + cacheTimestamp = Date.now(); + const storeGrid = document.getElementById('plugin-store-grid'); if (!storeGrid) { - // Defer rendering until plugin tab loads pluginLog('[STORE] plugin-store-grid not ready, deferring render'); window.__pendingStorePlugins = plugins; return; } - // Route through filter/sort pipeline — pass fresh plugins - // so server-filtered results (query/category) aren't ignored - applyStoreFiltersAndSort(plugins); - - // Ensure GitHub token collapse handler is attached after store is rendered - // The button might not exist until the store content is loaded - console.log('[STORE] Checking for attachGithubTokenCollapseHandler...', { - exists: typeof window.attachGithubTokenCollapseHandler, - checkGitHubAuthStatus: typeof window.checkGitHubAuthStatus - }); + // Update total count + try { + const countEl = document.getElementById('store-count'); + if (countEl) countEl.innerHTML = `${plugins.length} available`; + } catch (e) { /* ignore */ } + + applyStoreFiltersAndSort(); + + // Re-attach GitHub token collapse handler after store render if (window.attachGithubTokenCollapseHandler) { - // Use requestAnimationFrame for faster execution (runs on next frame, ~16ms) requestAnimationFrame(() => { - console.log('[STORE] Re-attaching GitHub token collapse handler after store render'); - try { - window.attachGithubTokenCollapseHandler(); - } catch (error) { - console.error('[STORE] Error attaching collapse handler:', error); - } - // Also check auth status to update UI (already checked earlier, but refresh to be sure) + try { window.attachGithubTokenCollapseHandler(); } catch (e) { /* ignore */ } if (window.checkGitHubAuthStatus) { - console.log('[STORE] Refreshing GitHub auth status after store render...'); - try { - window.checkGitHubAuthStatus(); - } catch (error) { - console.error('[STORE] Error calling checkGitHubAuthStatus:', error); - } - } else { - console.warn('[STORE] checkGitHubAuthStatus not available'); + try { window.checkGitHubAuthStatus(); } catch (e) { /* ignore */ } } }); - } else { - console.warn('[STORE] attachGithubTokenCollapseHandler not available'); } } else { showError('Failed to search plugin store: ' + data.message); try { const countEl = document.getElementById('store-count'); - if (countEl) { - countEl.innerHTML = 'Error loading'; - } - } catch (e) { - console.warn('Could not update store count:', e); - } + if (countEl) countEl.innerHTML = 'Error loading'; + } catch (e) { /* ignore */ } } }) .catch(error => { console.error('Error searching plugin store:', error); showStoreLoading(false); - let errorMsg = 'Error searching plugin store: ' + error.message; - if (error.message && error.message.includes('Failed to Fetch')) { - errorMsg += ' - Please try refreshing your browser.'; - } - showError(errorMsg); + showError('Error searching plugin store: ' + error.message); try { const countEl = document.getElementById('store-count'); - if (countEl) { - countEl.innerHTML = 'Error loading'; - } - } catch (e) { - console.warn('Could not update store count:', e); - } + if (countEl) countEl.innerHTML = 'Error loading'; + } catch (e) { /* ignore */ } }); } @@ -5591,6 +5259,257 @@ function showStoreLoading(show) { } } +// ── Plugin Store: Client-Side Filter/Sort/Pagination ──────────────────────── + +function isStorePluginInstalled(pluginId) { + return (window.installedPlugins || installedPlugins || []).some(p => p.id === pluginId); +} + +function applyStoreFiltersAndSort(skipPageReset) { + if (!pluginStoreCache) return; + const st = storeFilterState; + + let list = pluginStoreCache.slice(); + + // Text search + if (st.searchQuery) { + const q = st.searchQuery.toLowerCase(); + list = list.filter(plugin => { + const hay = [ + plugin.name, plugin.description, plugin.author, + plugin.id, plugin.category, + ...(plugin.tags || []) + ].filter(Boolean).join(' ').toLowerCase(); + return hay.includes(q); + }); + } + + // Category filter + if (st.filterCategory) { + const cat = st.filterCategory.toLowerCase(); + list = list.filter(plugin => (plugin.category || '').toLowerCase() === cat); + } + + // Installed filter + if (st.filterInstalled === true) { + list = list.filter(plugin => isStorePluginInstalled(plugin.id)); + } else if (st.filterInstalled === false) { + list = list.filter(plugin => !isStorePluginInstalled(plugin.id)); + } + + // Sort + list.sort((a, b) => { + const nameA = (a.name || a.id || '').toLowerCase(); + const nameB = (b.name || b.id || '').toLowerCase(); + switch (st.sort) { + case 'z-a': return nameB.localeCompare(nameA); + case 'category': { + const catCmp = (a.category || '').localeCompare(b.category || ''); + return catCmp !== 0 ? catCmp : nameA.localeCompare(nameB); + } + case 'author': { + const authCmp = (a.author || '').localeCompare(b.author || ''); + return authCmp !== 0 ? authCmp : nameA.localeCompare(nameB); + } + case 'newest': { + const dateA = a.last_updated ? new Date(a.last_updated).getTime() : 0; + const dateB = b.last_updated ? new Date(b.last_updated).getTime() : 0; + return dateB - dateA; // newest first + } + default: return nameA.localeCompare(nameB); + } + }); + + storeFilteredList = list; + if (!skipPageReset) st.page = 1; + + renderStorePage(); + updateStoreFilterUI(); +} + +function renderStorePage() { + const st = storeFilterState; + const total = storeFilteredList.length; + const totalPages = Math.max(1, Math.ceil(total / st.perPage)); + if (st.page > totalPages) st.page = totalPages; + + const start = (st.page - 1) * st.perPage; + const end = Math.min(start + st.perPage, total); + const pagePlugins = storeFilteredList.slice(start, end); + + // Results info + const info = total > 0 + ? `Showing ${start + 1}\u2013${end} of ${total} plugins` + : 'No plugins match your filters'; + const infoEl = document.getElementById('store-results-info'); + const infoElBot = document.getElementById('store-results-info-bottom'); + if (infoEl) infoEl.textContent = info; + if (infoElBot) infoElBot.textContent = info; + + // Pagination + renderStorePagination('store-pagination-top', totalPages, st.page); + renderStorePagination('store-pagination-bottom', totalPages, st.page); + + // Grid + renderPluginStore(pagePlugins); +} + +function renderStorePagination(containerId, totalPages, currentPage) { + const container = document.getElementById(containerId); + if (!container) return; + + if (totalPages <= 1) { container.innerHTML = ''; return; } + + const btnClass = 'px-3 py-1 text-sm rounded-md border transition-colors'; + const activeClass = 'bg-blue-600 text-white border-blue-600'; + const normalClass = 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100 cursor-pointer'; + const disabledClass = 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed'; + + let html = ''; + html += ``; + + const pages = []; + pages.push(1); + if (currentPage > 3) pages.push('...'); + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + pages.push(i); + } + if (currentPage < totalPages - 2) pages.push('...'); + if (totalPages > 1) pages.push(totalPages); + + pages.forEach(p => { + if (p === '...') { + html += ``; + } else { + html += ``; + } + }); + + html += ``; + + container.innerHTML = html; + + container.querySelectorAll('[data-store-page]').forEach(btn => { + btn.addEventListener('click', function() { + const p = parseInt(this.getAttribute('data-store-page')); + if (p >= 1 && p <= totalPages && p !== currentPage) { + storeFilterState.page = p; + renderStorePage(); + const grid = document.getElementById('plugin-store-grid'); + if (grid) grid.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); +} + +function updateStoreFilterUI() { + const st = storeFilterState; + const count = st.activeCount(); + + const badge = document.getElementById('store-active-filters'); + const clearBtn = document.getElementById('store-clear-filters'); + if (badge) { + badge.classList.toggle('hidden', count === 0); + badge.textContent = count + ' filter' + (count !== 1 ? 's' : '') + ' active'; + } + if (clearBtn) clearBtn.classList.toggle('hidden', count === 0); + + const instBtn = document.getElementById('store-filter-installed'); + if (instBtn) { + if (st.filterInstalled === true) { + instBtn.innerHTML = 'Installed'; + instBtn.classList.add('border-green-400', 'bg-green-50'); + instBtn.classList.remove('border-gray-300', 'bg-white', 'border-red-400', 'bg-red-50'); + } else if (st.filterInstalled === false) { + instBtn.innerHTML = 'Not Installed'; + instBtn.classList.add('border-red-400', 'bg-red-50'); + instBtn.classList.remove('border-gray-300', 'bg-white', 'border-green-400', 'bg-green-50'); + } else { + instBtn.innerHTML = 'All'; + instBtn.classList.add('border-gray-300', 'bg-white'); + instBtn.classList.remove('border-green-400', 'bg-green-50', 'border-red-400', 'bg-red-50'); + } + } +} + +function setupStoreFilterListeners() { + // Search with debounce + const searchEl = document.getElementById('plugin-search'); + if (searchEl && !searchEl._storeFilterInit) { + searchEl._storeFilterInit = true; + let debounce = null; + searchEl.addEventListener('input', function() { + clearTimeout(debounce); + debounce = setTimeout(() => { + storeFilterState.searchQuery = this.value.trim(); + applyStoreFiltersAndSort(); + }, 300); + }); + } + + // Category dropdown + const catEl = document.getElementById('plugin-category'); + if (catEl && !catEl._storeFilterInit) { + catEl._storeFilterInit = true; + catEl.addEventListener('change', function() { + storeFilterState.filterCategory = this.value; + applyStoreFiltersAndSort(); + }); + } + + // Sort dropdown + const sortEl = document.getElementById('store-sort'); + if (sortEl && !sortEl._storeFilterInit) { + sortEl._storeFilterInit = true; + sortEl.addEventListener('change', function() { + storeFilterState.sort = this.value; + storeFilterState.persist(); + applyStoreFiltersAndSort(); + }); + } + + // Installed toggle (cycle: all → installed → not-installed → all) + const instBtn = document.getElementById('store-filter-installed'); + if (instBtn && !instBtn._storeFilterInit) { + instBtn._storeFilterInit = true; + instBtn.addEventListener('click', function() { + const st = storeFilterState; + if (st.filterInstalled === null) st.filterInstalled = true; + else if (st.filterInstalled === true) st.filterInstalled = false; + else st.filterInstalled = null; + applyStoreFiltersAndSort(); + }); + } + + // Clear filters + const clearBtn = document.getElementById('store-clear-filters'); + if (clearBtn && !clearBtn._storeFilterInit) { + clearBtn._storeFilterInit = true; + clearBtn.addEventListener('click', function() { + storeFilterState.reset(); + const searchEl = document.getElementById('plugin-search'); + if (searchEl) searchEl.value = ''; + const catEl = document.getElementById('plugin-category'); + if (catEl) catEl.value = ''; + const sortEl = document.getElementById('store-sort'); + if (sortEl) sortEl.value = 'a-z'; + storeFilterState.persist(); + applyStoreFiltersAndSort(); + }); + } + + // Per-page selector + const ppEl = document.getElementById('store-per-page'); + if (ppEl && !ppEl._storeFilterInit) { + ppEl._storeFilterInit = true; + ppEl.addEventListener('change', function() { + storeFilterState.perPage = parseInt(this.value) || 12; + storeFilterState.persist(); + applyStoreFiltersAndSort(); + }); + } +} + // Expose searchPluginStore on window.pluginManager for Alpine.js integration window.searchPluginStore = searchPluginStore; window.pluginManager.searchPluginStore = searchPluginStore; @@ -5621,26 +5540,17 @@ function renderPluginStore(plugins) { return JSON.stringify(text || ''); }; - // Build installed lookup for badges - const installedMap = new Map(); - (window.installedPlugins || []).forEach(p => { - installedMap.set(p.id, p.version || ''); - }); - container.innerHTML = plugins.map(plugin => { - const isInstalled = installedMap.has(plugin.id); - const installedVersion = installedMap.get(plugin.id); - const hasUpdate = isInstalled && plugin.version && installedVersion && isNewerVersion(plugin.version, installedVersion); + const installed = isStorePluginInstalled(plugin.id); return `
-
+

${escapeHtml(plugin.name || plugin.id)}

${plugin.verified ? 'Verified' : ''} - ${isNewPlugin(plugin.last_updated) ? 'New' : ''} - ${isInstalled ? 'Installed' : ''} - ${hasUpdate ? 'Update' : ''} + ${installed ? 'Installed' : ''} + ${isNewPlugin(plugin.last_updated) ? 'New' : ''} ${plugin._source === 'custom_repository' ? `Custom` : ''}
@@ -5660,26 +5570,25 @@ function renderPluginStore(plugins) { ` : ''} -
+
-
-
-
- `; +
`; }).join(''); } @@ -5701,12 +5610,9 @@ window.installPlugin = function(pluginId, branch = null) { .then(data => { showNotification(data.message, data.status); if (data.status === 'success') { - // Refresh both installed plugins and store + // Refresh installed plugins list, then re-render store to update badges loadInstalledPlugins(); - // Delay store refresh slightly to ensure DOM is ready - setTimeout(() => { - searchPluginStore(); - }, 100); + setTimeout(() => applyStoreFiltersAndSort(true), 500); } }) .catch(error => { @@ -6391,20 +6297,6 @@ function formatCommit(commit, branch) { return 'Latest'; } -// Check if storeVersion is strictly newer than installedVersion (semver-aware) -function isNewerVersion(storeVersion, installedVersion) { - const parse = (v) => (v || '').replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0); - const a = parse(storeVersion); - const b = parse(installedVersion); - const len = Math.max(a.length, b.length); - for (let i = 0; i < len; i++) { - const diff = (a[i] || 0) - (b[i] || 0); - if (diff > 0) return true; - if (diff < 0) return false; - } - return false; -} - // Check if plugin is new (updated within last 7 days) function isNewPlugin(lastUpdated) { if (!lastUpdated) return false; @@ -7666,3 +7558,576 @@ setTimeout(function() { }, 500); }, 200); +// ─── Starlark Apps Integration ────────────────────────────────────────────── + +(function() { + 'use strict'; + + let starlarkSectionVisible = false; + let starlarkFullCache = null; // All apps from server + let starlarkFilteredList = []; // After filters applied + let starlarkDataLoaded = false; + + // ── Filter State ──────────────────────────────────────────────────────── + const starlarkFilterState = { + sort: safeLocalStorage.getItem('starlarkSort') || 'a-z', + filterInstalled: null, // null=all, true=installed, false=not-installed + filterAuthor: '', + filterCategory: '', + searchQuery: '', + page: 1, + perPage: parseInt(safeLocalStorage.getItem('starlarkPerPage')) || 24, + persist() { + safeLocalStorage.setItem('starlarkSort', this.sort); + safeLocalStorage.setItem('starlarkPerPage', this.perPage); + }, + reset() { + this.sort = 'a-z'; + this.filterInstalled = null; + this.filterAuthor = ''; + this.filterCategory = ''; + this.searchQuery = ''; + this.page = 1; + }, + activeCount() { + let n = 0; + if (this.searchQuery) n++; + if (this.filterInstalled !== null) n++; + if (this.filterAuthor) n++; + if (this.filterCategory) n++; + if (this.sort !== 'a-z') n++; + return n; + } + }; + + // ── Helpers ───────────────────────────────────────────────────────────── + function escapeHtml(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + function isStarlarkInstalled(appId) { + // Check window.installedPlugins (populated by loadInstalledPlugins) + if (window.installedPlugins && Array.isArray(window.installedPlugins)) { + return window.installedPlugins.some(p => p.id === 'starlark:' + appId); + } + return false; + } + + // ── Section Toggle + Init ─────────────────────────────────────────────── + function initStarlarkSection() { + const toggleBtn = document.getElementById('toggle-starlark-section'); + if (toggleBtn && !toggleBtn._starlarkInit) { + toggleBtn._starlarkInit = true; + toggleBtn.addEventListener('click', function() { + starlarkSectionVisible = !starlarkSectionVisible; + const content = document.getElementById('starlark-section-content'); + const icon = document.getElementById('starlark-section-icon'); + if (content) content.classList.toggle('hidden', !starlarkSectionVisible); + if (icon) { + icon.classList.toggle('fa-chevron-down', !starlarkSectionVisible); + icon.classList.toggle('fa-chevron-up', starlarkSectionVisible); + } + this.querySelector('span').textContent = starlarkSectionVisible ? 'Hide' : 'Show'; + if (starlarkSectionVisible) { + loadStarlarkStatus(); + if (!starlarkDataLoaded) fetchStarlarkApps(); + } + }); + } + + // Restore persisted sort/perPage + const sortEl = document.getElementById('starlark-sort'); + if (sortEl) sortEl.value = starlarkFilterState.sort; + const ppEl = document.getElementById('starlark-per-page'); + if (ppEl) ppEl.value = starlarkFilterState.perPage; + + setupStarlarkFilterListeners(); + + const uploadBtn = document.getElementById('starlark-upload-btn'); + if (uploadBtn && !uploadBtn._starlarkInit) { + uploadBtn._starlarkInit = true; + uploadBtn.addEventListener('click', function() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.star'; + input.onchange = function(e) { + if (e.target.files.length > 0) uploadStarlarkFile(e.target.files[0]); + }; + input.click(); + }); + } + } + + // ── Status ────────────────────────────────────────────────────────────── + function loadStarlarkStatus() { + fetch('/api/v3/starlark/status') + .then(r => r.json()) + .then(data => { + const banner = document.getElementById('starlark-pixlet-status'); + if (!banner) return; + if (data.pixlet_available) { + banner.innerHTML = `
+ Pixlet available${data.pixlet_version ? ' (' + escapeHtml(data.pixlet_version) + ')' : ''} — ${data.installed_apps || 0} app(s) installed +
`; + } else { + banner.innerHTML = `
+ Pixlet not installed. + +
`; + } + }) + .catch(err => console.error('Starlark status error:', err)); + } + + // ── Bulk Fetch All Apps ───────────────────────────────────────────────── + function fetchStarlarkApps() { + const grid = document.getElementById('starlark-apps-grid'); + if (grid) { + grid.innerHTML = `
+
+ ${Array(10).fill('
').join('')} +
+
`; + } + + fetch('/api/v3/starlark/repository/browse') + .then(r => r.json()) + .then(data => { + if (data.status !== 'success') { + if (grid) grid.innerHTML = `
${escapeHtml(data.message || 'Failed to load')}
`; + return; + } + + starlarkFullCache = data.apps || []; + starlarkDataLoaded = true; + + // Populate category dropdown + const catSelect = document.getElementById('starlark-category'); + if (catSelect) { + catSelect.innerHTML = ''; + (data.categories || []).forEach(cat => { + const opt = document.createElement('option'); + opt.value = cat; + opt.textContent = cat; + catSelect.appendChild(opt); + }); + } + + // Populate author dropdown + const authSelect = document.getElementById('starlark-filter-author'); + if (authSelect) { + authSelect.innerHTML = ''; + (data.authors || []).forEach(author => { + const opt = document.createElement('option'); + opt.value = author; + opt.textContent = author; + authSelect.appendChild(opt); + }); + } + + const countEl = document.getElementById('starlark-apps-count'); + if (countEl) countEl.textContent = `${data.count} apps`; + + if (data.rate_limit) { + console.log(`[Starlark] GitHub rate limit: ${data.rate_limit.remaining}/${data.rate_limit.limit} remaining` + (data.cached ? ' (cached)' : '')); + } + + applyStarlarkFiltersAndSort(); + }) + .catch(err => { + console.error('Starlark browse error:', err); + if (grid) grid.innerHTML = '
Error loading apps
'; + }); + } + + // ── Apply Filters + Sort ──────────────────────────────────────────────── + function applyStarlarkFiltersAndSort(skipPageReset) { + if (!starlarkFullCache) return; + const st = starlarkFilterState; + + let list = starlarkFullCache.slice(); + + // Text search + if (st.searchQuery) { + const q = st.searchQuery.toLowerCase(); + list = list.filter(app => { + const hay = [app.name, app.summary, app.desc, app.author, app.id, app.category] + .filter(Boolean).join(' ').toLowerCase(); + return hay.includes(q); + }); + } + + // Category filter + if (st.filterCategory) { + const cat = st.filterCategory.toLowerCase(); + list = list.filter(app => (app.category || '').toLowerCase() === cat); + } + + // Author filter + if (st.filterAuthor) { + list = list.filter(app => app.author === st.filterAuthor); + } + + // Installed filter + if (st.filterInstalled === true) { + list = list.filter(app => isStarlarkInstalled(app.id)); + } else if (st.filterInstalled === false) { + list = list.filter(app => !isStarlarkInstalled(app.id)); + } + + // Sort + list.sort((a, b) => { + const nameA = (a.name || a.id || '').toLowerCase(); + const nameB = (b.name || b.id || '').toLowerCase(); + switch (st.sort) { + case 'z-a': return nameB.localeCompare(nameA); + case 'category': { + const catCmp = (a.category || '').localeCompare(b.category || ''); + return catCmp !== 0 ? catCmp : nameA.localeCompare(nameB); + } + case 'author': { + const authCmp = (a.author || '').localeCompare(b.author || ''); + return authCmp !== 0 ? authCmp : nameA.localeCompare(nameB); + } + default: return nameA.localeCompare(nameB); // a-z + } + }); + + starlarkFilteredList = list; + if (!skipPageReset) st.page = 1; + + renderStarlarkPage(); + updateStarlarkFilterUI(); + } + + // ── Render Current Page ───────────────────────────────────────────────── + function renderStarlarkPage() { + const st = starlarkFilterState; + const total = starlarkFilteredList.length; + const totalPages = Math.max(1, Math.ceil(total / st.perPage)); + if (st.page > totalPages) st.page = totalPages; + + const start = (st.page - 1) * st.perPage; + const end = Math.min(start + st.perPage, total); + const pageApps = starlarkFilteredList.slice(start, end); + + // Results info + const info = total > 0 + ? `Showing ${start + 1}\u2013${end} of ${total} apps` + : 'No apps match your filters'; + const infoEl = document.getElementById('starlark-results-info'); + const infoElBot = document.getElementById('starlark-results-info-bottom'); + if (infoEl) infoEl.textContent = info; + if (infoElBot) infoElBot.textContent = info; + + // Pagination + renderStarlarkPagination('starlark-pagination-top', totalPages, st.page); + renderStarlarkPagination('starlark-pagination-bottom', totalPages, st.page); + + // Grid + const grid = document.getElementById('starlark-apps-grid'); + renderStarlarkApps(pageApps, grid); + } + + // ── Pagination Controls ───────────────────────────────────────────────── + function renderStarlarkPagination(containerId, totalPages, currentPage) { + const container = document.getElementById(containerId); + if (!container) return; + + if (totalPages <= 1) { container.innerHTML = ''; return; } + + const btnClass = 'px-3 py-1 text-sm rounded-md border transition-colors'; + const activeClass = 'bg-blue-600 text-white border-blue-600'; + const normalClass = 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100 cursor-pointer'; + const disabledClass = 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed'; + + let html = ''; + + // Prev + html += ``; + + // Page numbers with ellipsis + const pages = []; + pages.push(1); + if (currentPage > 3) pages.push('...'); + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + pages.push(i); + } + if (currentPage < totalPages - 2) pages.push('...'); + if (totalPages > 1) pages.push(totalPages); + + pages.forEach(p => { + if (p === '...') { + html += ``; + } else { + html += ``; + } + }); + + // Next + html += ``; + + container.innerHTML = html; + + // Event delegation for page buttons + container.querySelectorAll('[data-starlark-page]').forEach(btn => { + btn.addEventListener('click', function() { + const p = parseInt(this.getAttribute('data-starlark-page')); + if (p >= 1 && p <= totalPages && p !== currentPage) { + starlarkFilterState.page = p; + renderStarlarkPage(); + // Scroll to top of grid + const grid = document.getElementById('starlark-apps-grid'); + if (grid) grid.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); + } + + // ── Card Rendering ────────────────────────────────────────────────────── + function renderStarlarkApps(apps, grid) { + if (!grid) return; + if (!apps || apps.length === 0) { + grid.innerHTML = '

No Starlark apps found

'; + return; + } + + grid.innerHTML = apps.map(app => { + const installed = isStarlarkInstalled(app.id); + return ` +
+
+
+
+

${escapeHtml(app.name || app.id)}

+ Starlark + ${installed ? 'Installed' : ''} +
+
+ ${app.author ? `

${escapeHtml(app.author)}

` : ''} + ${app.category ? `

${escapeHtml(app.category)}

` : ''} +
+

${escapeHtml(app.summary || app.desc || 'No description')}

+
+
+
+ + +
+
`; + }).join(''); + } + + // ── Filter UI Updates ─────────────────────────────────────────────────── + function updateStarlarkFilterUI() { + const st = starlarkFilterState; + const count = st.activeCount(); + + const badge = document.getElementById('starlark-active-filters'); + const clearBtn = document.getElementById('starlark-clear-filters'); + if (badge) { + badge.classList.toggle('hidden', count === 0); + badge.textContent = count + ' filter' + (count !== 1 ? 's' : '') + ' active'; + } + if (clearBtn) clearBtn.classList.toggle('hidden', count === 0); + + // Update installed toggle button text + const instBtn = document.getElementById('starlark-filter-installed'); + if (instBtn) { + if (st.filterInstalled === true) { + instBtn.innerHTML = 'Installed'; + instBtn.classList.add('border-green-400', 'bg-green-50'); + instBtn.classList.remove('border-gray-300', 'bg-white', 'border-red-400', 'bg-red-50'); + } else if (st.filterInstalled === false) { + instBtn.innerHTML = 'Not Installed'; + instBtn.classList.add('border-red-400', 'bg-red-50'); + instBtn.classList.remove('border-gray-300', 'bg-white', 'border-green-400', 'bg-green-50'); + } else { + instBtn.innerHTML = 'All'; + instBtn.classList.add('border-gray-300', 'bg-white'); + instBtn.classList.remove('border-green-400', 'bg-green-50', 'border-red-400', 'bg-red-50'); + } + } + } + + // ── Event Listeners ───────────────────────────────────────────────────── + function setupStarlarkFilterListeners() { + // Search with debounce + const searchEl = document.getElementById('starlark-search'); + if (searchEl && !searchEl._starlarkInit) { + searchEl._starlarkInit = true; + let debounce = null; + searchEl.addEventListener('input', function() { + clearTimeout(debounce); + debounce = setTimeout(() => { + starlarkFilterState.searchQuery = this.value.trim(); + applyStarlarkFiltersAndSort(); + }, 300); + }); + } + + // Category dropdown + const catEl = document.getElementById('starlark-category'); + if (catEl && !catEl._starlarkInit) { + catEl._starlarkInit = true; + catEl.addEventListener('change', function() { + starlarkFilterState.filterCategory = this.value; + applyStarlarkFiltersAndSort(); + }); + } + + // Sort dropdown + const sortEl = document.getElementById('starlark-sort'); + if (sortEl && !sortEl._starlarkInit) { + sortEl._starlarkInit = true; + sortEl.addEventListener('change', function() { + starlarkFilterState.sort = this.value; + starlarkFilterState.persist(); + applyStarlarkFiltersAndSort(); + }); + } + + // Author dropdown + const authEl = document.getElementById('starlark-filter-author'); + if (authEl && !authEl._starlarkInit) { + authEl._starlarkInit = true; + authEl.addEventListener('change', function() { + starlarkFilterState.filterAuthor = this.value; + applyStarlarkFiltersAndSort(); + }); + } + + // Installed toggle (cycle: all → installed → not-installed → all) + const instBtn = document.getElementById('starlark-filter-installed'); + if (instBtn && !instBtn._starlarkInit) { + instBtn._starlarkInit = true; + instBtn.addEventListener('click', function() { + const st = starlarkFilterState; + if (st.filterInstalled === null) st.filterInstalled = true; + else if (st.filterInstalled === true) st.filterInstalled = false; + else st.filterInstalled = null; + applyStarlarkFiltersAndSort(); + }); + } + + // Clear filters + const clearBtn = document.getElementById('starlark-clear-filters'); + if (clearBtn && !clearBtn._starlarkInit) { + clearBtn._starlarkInit = true; + clearBtn.addEventListener('click', function() { + starlarkFilterState.reset(); + // Reset UI elements + const searchEl = document.getElementById('starlark-search'); + if (searchEl) searchEl.value = ''; + const catEl = document.getElementById('starlark-category'); + if (catEl) catEl.value = ''; + const sortEl = document.getElementById('starlark-sort'); + if (sortEl) sortEl.value = 'a-z'; + const authEl = document.getElementById('starlark-filter-author'); + if (authEl) authEl.value = ''; + starlarkFilterState.persist(); + applyStarlarkFiltersAndSort(); + }); + } + + // Per-page selector + const ppEl = document.getElementById('starlark-per-page'); + if (ppEl && !ppEl._starlarkInit) { + ppEl._starlarkInit = true; + ppEl.addEventListener('change', function() { + starlarkFilterState.perPage = parseInt(this.value) || 24; + starlarkFilterState.persist(); + applyStarlarkFiltersAndSort(); + }); + } + } + + // ── Install / Upload / Pixlet ─────────────────────────────────────────── + window.installStarlarkApp = function(appId) { + if (!confirm(`Install Starlark app "${appId}" from Tronbyte repository?`)) return; + + fetch('/api/v3/starlark/repository/install', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({app_id: appId}) + }) + .then(r => r.json()) + .then(data => { + if (data.status === 'success') { + alert(`Installed: ${data.message || appId}`); + // Refresh installed plugins list + if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins(); + else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins(); + // Re-render current page to update installed badges + setTimeout(() => applyStarlarkFiltersAndSort(true), 500); + } else { + alert(`Install failed: ${data.message || 'Unknown error'}`); + } + }) + .catch(err => { + console.error('Install error:', err); + alert('Install failed: ' + err.message); + }); + }; + + window.installPixlet = function() { + if (!confirm('Download and install Pixlet binary? This may take a few minutes.')) return; + + fetch('/api/v3/starlark/install-pixlet', {method: 'POST'}) + .then(r => r.json()) + .then(data => { + if (data.status === 'success') { + alert(data.message || 'Pixlet installed!'); + loadStarlarkStatus(); + } else { + alert('Pixlet install failed: ' + (data.message || 'Unknown error')); + } + }) + .catch(err => alert('Pixlet install failed: ' + err.message)); + }; + + function uploadStarlarkFile(file) { + const formData = new FormData(); + formData.append('file', file); + + const appId = file.name.replace('.star', ''); + formData.append('app_id', appId); + formData.append('name', appId.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())); + + fetch('/api/v3/starlark/upload', {method: 'POST', body: formData}) + .then(r => r.json()) + .then(data => { + if (data.status === 'success') { + alert(`Uploaded: ${data.app_id}`); + if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins(); + else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins(); + setTimeout(() => applyStarlarkFiltersAndSort(true), 500); + } else { + alert('Upload failed: ' + (data.message || 'Unknown error')); + } + }) + .catch(err => alert('Upload failed: ' + err.message)); + } + + // ── Bootstrap ─────────────────────────────────────────────────────────── + const origInit = window.initializePlugins; + window.initializePlugins = function() { + if (origInit) origInit(); + initStarlarkSection(); + }; + + document.addEventListener('DOMContentLoaded', initStarlarkSection); + document.addEventListener('htmx:afterSwap', function(e) { + if (e.detail && e.detail.target && e.detail.target.id === 'plugins-content') { + initStarlarkSection(); + } + }); +})(); +