mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
Compare commits
5 Commits
fix/lacros
...
763df0fee4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
763df0fee4 | ||
|
|
2447b131b5 | ||
|
|
88c4faa9ac | ||
|
|
4c2c7c0d17 | ||
|
|
34a55b5a55 |
@@ -83,6 +83,9 @@
|
||||
[data-theme="dark"] .hover\:bg-gray-200:hover { background-color: #4b5563; }
|
||||
[data-theme="dark"] .hover\:text-gray-700:hover { color: #e5e7eb; }
|
||||
[data-theme="dark"] .hover\:border-gray-300:hover { border-color: #6b7280; }
|
||||
[data-theme="dark"] .bg-red-100 { background-color: #450a0a; }
|
||||
[data-theme="dark"] .text-red-700 { color: #fca5a5; }
|
||||
[data-theme="dark"] .hover\:bg-red-200:hover { background-color: #7f1d1d; }
|
||||
|
||||
/* Base styles */
|
||||
* {
|
||||
@@ -663,6 +666,39 @@ button.bg-white {
|
||||
color: var(--color-purple-text);
|
||||
}
|
||||
|
||||
/* Filter Pill Toggle States */
|
||||
.filter-pill {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.filter-pill[data-active="true"] {
|
||||
background-color: var(--color-info-bg);
|
||||
border-color: var(--color-info);
|
||||
color: var(--color-info);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-pill[data-active="true"]:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Category Filter Pills */
|
||||
.category-filter-pill {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.category-filter-pill[data-active="true"] {
|
||||
background-color: var(--color-info-bg);
|
||||
border-color: var(--color-info);
|
||||
color: var(--color-info);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 0 0 1px var(--color-info);
|
||||
}
|
||||
|
||||
/* Section Headers with Subtle Gradients */
|
||||
.section-header {
|
||||
background: linear-gradient(135deg, rgb(255 255 255 / 90%) 0%, rgb(249 250 251 / 90%) 100%);
|
||||
|
||||
@@ -863,6 +863,43 @@ window.currentPluginConfig = null;
|
||||
let currentOnDemandPluginId = null;
|
||||
let hasLoadedOnDemandStatus = false;
|
||||
|
||||
// Store filter/sort state
|
||||
const storeFilterState = {
|
||||
sort: localStorage.getItem('storeSort') || 'a-z',
|
||||
filterVerified: false,
|
||||
filterNew: false,
|
||||
filterInstalled: null, // null = all, true = installed only, false = not installed only
|
||||
filterAuthors: [],
|
||||
filterCategories: [],
|
||||
|
||||
persist() {
|
||||
localStorage.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 = localStorage.getItem('installedSort') || 'a-z';
|
||||
|
||||
// Shared on-demand status store (mirrors Alpine store when available)
|
||||
window.__onDemandStore = window.__onDemandStore || {
|
||||
loading: true,
|
||||
@@ -976,7 +1013,7 @@ 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');
|
||||
renderInstalledPlugins(window.__pendingInstalledPlugins);
|
||||
sortAndRenderInstalledPlugins(window.__pendingInstalledPlugins);
|
||||
window.__pendingInstalledPlugins = null;
|
||||
}
|
||||
if (window.__pendingStorePlugins) {
|
||||
@@ -1105,13 +1142,15 @@ 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('#installed-plugins-grid') ||
|
||||
document.getElementById('installed-plugins-grid')) {
|
||||
if (target.id === 'plugins-content' ||
|
||||
target.querySelector('[data-plugins-loaded]')) {
|
||||
console.log('HTMX swap detected for plugins, initializing...');
|
||||
// Reset initialization flag to allow re-initialization after HTMX swap
|
||||
// Reset initialization flags 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
|
||||
@@ -1172,7 +1211,10 @@ 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') {
|
||||
@@ -1227,7 +1269,7 @@ function loadInstalledPlugins(forceRefresh = false) {
|
||||
}));
|
||||
pluginLog('[CACHE] Dispatched pluginsUpdated event from cache');
|
||||
// Still render to ensure UI is updated
|
||||
renderInstalledPlugins(pluginLoadCache.data);
|
||||
sortAndRenderInstalledPlugins(pluginLoadCache.data);
|
||||
return Promise.resolve(pluginLoadCache.data);
|
||||
}
|
||||
|
||||
@@ -1286,7 +1328,7 @@ function loadInstalledPlugins(forceRefresh = false) {
|
||||
});
|
||||
}
|
||||
|
||||
renderInstalledPlugins(installedPlugins);
|
||||
sortAndRenderInstalledPlugins(installedPlugins);
|
||||
|
||||
// Update count
|
||||
const countEl = document.getElementById('installed-count');
|
||||
@@ -1327,6 +1369,24 @@ 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) {
|
||||
@@ -1682,6 +1742,8 @@ 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');
|
||||
@@ -1691,7 +1753,7 @@ async function runUpdateAllPlugins() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.dataset.running === 'true') {
|
||||
if (updateAllRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1702,7 +1764,7 @@ async function runUpdateAllPlugins() {
|
||||
}
|
||||
|
||||
const originalContent = button.innerHTML;
|
||||
button.dataset.running = 'true';
|
||||
updateAllRunning = true;
|
||||
button.disabled = true;
|
||||
button.classList.add('opacity-60', 'cursor-wait');
|
||||
|
||||
@@ -1712,7 +1774,11 @@ async function runUpdateAllPlugins() {
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
const plugin = plugins[i];
|
||||
const pluginId = plugin.id;
|
||||
button.innerHTML = `<i class="fas fa-sync fa-spin mr-2"></i>Updating ${i + 1}/${plugins.length}...`;
|
||||
// Re-fetch button in case DOM was replaced by HTMX swap
|
||||
const btn = document.getElementById('update-all-plugins-btn');
|
||||
if (btn) {
|
||||
btn.innerHTML = `<i class="fas fa-sync fa-spin mr-2"></i>Updating ${i + 1}/${plugins.length}...`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v3/plugins/update', {
|
||||
@@ -1752,10 +1818,13 @@ async function runUpdateAllPlugins() {
|
||||
console.error('Bulk plugin update failed:', error);
|
||||
showNotification('Failed to update all plugins: ' + error.message, 'error');
|
||||
} finally {
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
button.classList.remove('opacity-60', 'cursor-wait');
|
||||
button.dataset.running = 'false';
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5037,7 +5106,7 @@ function handleUninstallSuccess(pluginId) {
|
||||
if (typeof installedPlugins !== 'undefined') {
|
||||
installedPlugins = updatedPlugins;
|
||||
}
|
||||
renderInstalledPlugins(updatedPlugins);
|
||||
sortAndRenderInstalledPlugins(updatedPlugins);
|
||||
showNotification(`Plugin uninstalled successfully`, 'success');
|
||||
|
||||
// Also refresh from server to ensure consistency
|
||||
@@ -5076,6 +5145,256 @@ 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;
|
||||
localStorage.setItem('installedSort', installedSort);
|
||||
const plugins = window.installedPlugins || [];
|
||||
if (plugins.length > 0) {
|
||||
sortAndRenderInstalledPlugins(plugins);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function applyStoreFiltersAndSort() {
|
||||
if (!pluginStoreCache) return;
|
||||
|
||||
let plugins = [...pluginStoreCache];
|
||||
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 = pluginStoreCache.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 = '<option value="">All Authors</option>' +
|
||||
authors.map(a => `<option value="${escapeHtml(a)}">${escapeHtml(a)}</option>`).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 =>
|
||||
`<button class="category-filter-pill badge badge-info cursor-pointer" data-category="${escapeHtml(cat)}" data-active="${storeFilterState.filterCategories.includes(cat)}">${escapeHtml(cat)}</button>`
|
||||
).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 });
|
||||
|
||||
@@ -5100,15 +5419,7 @@ function searchPluginStore(fetchCommitInfo = true) {
|
||||
console.error('plugin-store-grid element not found, cannot render cached plugins');
|
||||
// Don't return, let it fetch fresh data
|
||||
} else {
|
||||
renderPluginStore(pluginStoreCache);
|
||||
try {
|
||||
const countEl = document.getElementById('store-count');
|
||||
if (countEl) {
|
||||
countEl.innerHTML = `${pluginStoreCache.length} available`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not update store count:', e);
|
||||
}
|
||||
applyStoreFiltersAndSort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -5158,8 +5469,9 @@ function searchPluginStore(fetchCommitInfo = true) {
|
||||
pluginStoreCache = plugins;
|
||||
cacheTimestamp = Date.now();
|
||||
console.log('Cached plugin store data');
|
||||
populateFilterControls();
|
||||
}
|
||||
|
||||
|
||||
// Ensure plugin store grid exists before rendering
|
||||
const storeGrid = document.getElementById('plugin-store-grid');
|
||||
if (!storeGrid) {
|
||||
@@ -5168,17 +5480,20 @@ function searchPluginStore(fetchCommitInfo = true) {
|
||||
window.__pendingStorePlugins = plugins;
|
||||
return;
|
||||
}
|
||||
|
||||
renderPluginStore(plugins);
|
||||
|
||||
// Update count - safely check element exists
|
||||
try {
|
||||
const countEl = document.getElementById('store-count');
|
||||
if (countEl) {
|
||||
countEl.innerHTML = `${plugins.length} available`;
|
||||
// Route through filter/sort pipeline if cache is available, otherwise render directly
|
||||
if (pluginStoreCache) {
|
||||
applyStoreFiltersAndSort();
|
||||
} else {
|
||||
renderPluginStore(plugins);
|
||||
try {
|
||||
const countEl = document.getElementById('store-count');
|
||||
if (countEl) {
|
||||
countEl.innerHTML = `${plugins.length} available`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not update store count:', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not update store count:', e);
|
||||
}
|
||||
|
||||
// Ensure GitHub token collapse handler is attached after store is rendered
|
||||
@@ -5279,7 +5594,17 @@ function renderPluginStore(plugins) {
|
||||
return JSON.stringify(text || '');
|
||||
};
|
||||
|
||||
container.innerHTML = plugins.map(plugin => `
|
||||
// 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 && plugin.version !== installedVersion;
|
||||
return `
|
||||
<div class="plugin-card">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -5287,6 +5612,8 @@ function renderPluginStore(plugins) {
|
||||
<h4 class="font-semibold text-gray-900 text-base">${escapeHtml(plugin.name || plugin.id)}</h4>
|
||||
${plugin.verified ? '<span class="badge badge-success"><i class="fas fa-check-circle mr-1"></i>Verified</span>' : ''}
|
||||
${isNewPlugin(plugin.last_updated) ? '<span class="badge badge-info"><i class="fas fa-sparkles mr-1"></i>New</span>' : ''}
|
||||
${isInstalled ? '<span class="badge badge-success"><i class="fas fa-check mr-1"></i>Installed</span>' : ''}
|
||||
${hasUpdate ? '<span class="badge badge-warning"><i class="fas fa-arrow-up mr-1"></i>Update</span>' : ''}
|
||||
${plugin._source === 'custom_repository' ? `<span class="badge badge-accent" title="From: ${escapeHtml(plugin._repository_name || plugin._repository_url || 'Custom Repository')}"><i class="fas fa-bookmark mr-1"></i>Custom</span>` : ''}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-1.5 mb-3">
|
||||
@@ -5325,7 +5652,8 @@ function renderPluginStore(plugins) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Expose functions to window for onclick handlers
|
||||
|
||||
@@ -1375,7 +1375,7 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<!-- Custom v3 styles -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='v3/app.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='v3/app.css') }}?v=20260216b">
|
||||
</head>
|
||||
<body x-data="app()" class="bg-gray-50 min-h-screen">
|
||||
<!-- Header -->
|
||||
@@ -5013,7 +5013,7 @@
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/plugin-loader.js') }}" defer></script>
|
||||
|
||||
<!-- Legacy plugins_manager.js (for backward compatibility during migration) -->
|
||||
<script src="{{ url_for('static', filename='v3/plugins_manager.js') }}?v=20250116a" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/plugins_manager.js') }}?v=20260216b" defer></script>
|
||||
|
||||
<!-- Custom feeds table helper functions -->
|
||||
<script>
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
<h3 class="text-lg font-bold text-gray-900">Installed Plugins</h3>
|
||||
<span id="installed-count" class="text-sm text-gray-500 font-medium">0 installed</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="installed-sort" class="text-sm font-medium text-gray-700 whitespace-nowrap">
|
||||
<i class="fas fa-sort mr-1"></i>Sort:
|
||||
</label>
|
||||
<select id="installed-sort" class="text-sm px-3 py-1.5 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="a-z">A → Z</option>
|
||||
<option value="z-a">Z → A</option>
|
||||
<option value="enabled">Enabled First</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="installed-plugins-content" class="block">
|
||||
<div id="installed-plugins-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||
@@ -165,6 +175,68 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort & Filter Controls -->
|
||||
<div id="store-filter-bar" class="mb-4 space-y-3">
|
||||
<!-- Row 1: Sort + Quick Filters -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<!-- Sort Dropdown -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="store-sort" class="text-sm font-medium text-gray-700 whitespace-nowrap">
|
||||
<i class="fas fa-sort mr-1"></i>Sort:
|
||||
</label>
|
||||
<select id="store-sort" class="text-sm px-3 py-1.5 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="a-z">A → Z</option>
|
||||
<option value="z-a">Z → A</option>
|
||||
<option value="verified">Verified First</option>
|
||||
<option value="newest">Recently Updated</option>
|
||||
<option value="category">Category</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="h-6 w-px bg-gray-300"></div>
|
||||
|
||||
<!-- Quick Filter Toggles -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-sm font-medium text-gray-700">Filter:</span>
|
||||
<button id="filter-verified" type="button" class="filter-pill text-xs px-3 py-1.5 rounded-full border border-gray-300 bg-white hover:bg-gray-50 transition-colors" data-active="false">
|
||||
<i class="fas fa-check-circle mr-1"></i>Verified
|
||||
</button>
|
||||
<button id="filter-new" type="button" class="filter-pill text-xs px-3 py-1.5 rounded-full border border-gray-300 bg-white hover:bg-gray-50 transition-colors" data-active="false">
|
||||
<i class="fas fa-sparkles mr-1"></i>New
|
||||
</button>
|
||||
<button id="filter-installed" type="button" class="filter-pill text-xs px-3 py-1.5 rounded-full border border-gray-300 bg-white hover:bg-gray-50 transition-colors" data-active="false">
|
||||
<i class="fas fa-download mr-1"></i><span>All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="h-6 w-px bg-gray-300"></div>
|
||||
|
||||
<!-- Author Dropdown -->
|
||||
<select id="filter-author" class="text-sm px-3 py-1.5 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">All Authors</option>
|
||||
</select>
|
||||
|
||||
<!-- Clear Filters + Badge -->
|
||||
<button id="clear-filters-btn" type="button" class="hidden text-xs px-3 py-1.5 rounded-full bg-red-100 text-red-700 hover:bg-red-200 transition-colors font-medium">
|
||||
<i class="fas fa-times mr-1"></i>Clear Filters
|
||||
<span id="filter-count-badge" class="ml-1 inline-flex items-center justify-center bg-red-600 text-white rounded-full w-5 h-5 text-xs font-bold">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Category Pills (populated dynamically) -->
|
||||
<div id="filter-categories-container" class="hidden">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs font-medium text-gray-600 whitespace-nowrap">Categories:</span>
|
||||
<div id="filter-categories-pills" class="flex flex-wrap gap-1.5">
|
||||
<!-- Dynamically populated category pills -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="plugin-store-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||
<!-- Loading skeleton -->
|
||||
<div class="store-loading col-span-full">
|
||||
|
||||
Reference in New Issue
Block a user