@@ -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 = '
';
+ 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();
+ }
+ });
+})();
+