diff --git a/containers/libreportal/frontend/components/admin/config/js/catalog-manager.js b/containers/libreportal/frontend/components/admin/config/js/catalog-manager.js new file mode 100644 index 0000000..64626d4 --- /dev/null +++ b/containers/libreportal/frontend/components/admin/config/js/catalog-manager.js @@ -0,0 +1,152 @@ +// Catalog Sources - block editor for CFG_CATALOG_1..9 (extra App Center catalog +// URLs). Mirrors the domains block UI (reuses its CSS) but with URL inputs and +// no DNS checks. Values are saved by the generic config-form (any CFG_* input), +// and configUpdateBatch rebuilds registry_catalog.json when a CFG_CATALOG_* +// changes. The official catalog is source #1 and is not editable here. +class CatalogManager { + _num(key) { const m = key.match(/CFG_CATALOG_(\d+)/); return m ? parseInt(m[1], 10) : 0; } + _esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"').replace(/ +
+ + +
+ `; + } + + async renderCatalogsSection(configItems, displaySubcategory, subcategoryDescription) { + const keys = configItems.map(i => i.key).filter(k => k.startsWith('CFG_CATALOG_')); + const withContent = keys + .filter(k => ((configItems.find(i => i.key === k) || {}).value || '').trim() !== '') + .sort((a, b) => this._num(a) - this._num(b)); + const highest = withContent.length ? Math.max(...withContent.map(k => this._num(k))) : 0; + const isMax = withContent.length >= 9; + + let blocks = ''; + withContent.forEach(key => { + const value = (configItems.find(i => i.key === key) || {}).value || ''; + blocks += this._block(key, value, this._num(key) === highest); + }); + + return ` +
+
+
+
+

${displaySubcategory}

+

${subcategoryDescription}

+
+
+
+
+
+ 🛈 +
+ The official catalog is always included. Add third-party catalog URLs to browse more apps — third-party sources are unverified (you are trusting that host). Each is served with a signed /<channel>/index.json. +
+
+
+
+ ${blocks} +
+
+ +
+
+
+
`; + } +} + +// --- standalone block add/remove (mirrors the domain globals) ---------------- +function _catalogBlocks() { + return Array.from(document.querySelectorAll('.domain-building-block')) + .filter(b => b.querySelector('input[name^="CFG_CATALOG_"]')); +} + +window.addCatalog = function () { + const blocks = _catalogBlocks(); + const nums = blocks.map(b => { + const m = b.querySelector('input').name.match(/CFG_CATALOG_(\d+)/); return m ? parseInt(m[1], 10) : 0; + }); + if (nums.length >= 9) return; + const next = nums.length ? Math.max(...nums) + 1 : 1; + const key = `CFG_CATALOG_${next}`; + const fieldId = `config-${key}`; + const html = ` +
+
+ + +
+
`; + const container = document.querySelector('.domain-building-blocks'); + if (!container) return; + const tmp = document.createElement('div'); + tmp.innerHTML = html; + const block = tmp.firstElementChild; + container.appendChild(block); + const input = block.querySelector('input'); + if (input) input.focus(); + window.updateCatalogButtons(); +}; + +window.deleteCatalog = function (key, btn) { + const block = btn.closest('.domain-building-block'); + if (!block) return; + const input = block.querySelector('input'); + if (input) input.value = ''; + block.remove(); + window.updateCatalogButtons(); +}; + +window.updateCatalogButtons = function () { + const blocks = _catalogBlocks(); + const nums = blocks.map(b => { + const m = b.querySelector('input').name.match(/CFG_CATALOG_(\d+)/); return m ? parseInt(m[1], 10) : 0; + }); + const highest = nums.length ? Math.max(...nums) : 0; + blocks.forEach(b => { + const del = b.querySelector('.delete-domain-btn'); + const n = parseInt((b.querySelector('input').name.match(/CFG_CATALOG_(\d+)/) || [])[1] || '0', 10); + const canDelete = n === highest; + if (del) { + del.disabled = !canDelete; + del.className = `delete-domain-btn ${!canDelete ? 'disabled' : ''}`; + } + }); + const addBtn = document.getElementById('add-catalog-btn'); + if (addBtn) { + const isMax = blocks.length >= 9; + addBtn.disabled = isMax; + addBtn.className = `btn ${isMax ? 'btn-secondary' : 'btn-primary'}`; + addBtn.innerHTML = `${isMax ? '✓' : '+'}${isMax ? 'Maximum Catalogs Reached' : 'Add Catalog'}`; + } +}; + +// Light URL validation — the backend re-validates. Just a red border hint. +window.validateCatalogFormat = function (input) { + const v = (input.value || '').trim(); + const ok = v === '' || /^https?:\/\/[^\s/]+/i.test(v); + input.style.borderColor = ok ? '' : '#dc3545'; + input.title = ok ? '' : 'Catalog must be an http(s):// URL'; + return ok; +}; diff --git a/containers/libreportal/frontend/components/admin/config/js/config-manager.js b/containers/libreportal/frontend/components/admin/config/js/config-manager.js index b342a0f..857dddd 100755 --- a/containers/libreportal/frontend/components/admin/config/js/config-manager.js +++ b/containers/libreportal/frontend/components/admin/config/js/config-manager.js @@ -5,6 +5,7 @@ if (typeof window.ConfigManager === 'undefined') { constructor() { this.core = new ConfigCore(); this.domainManager = new DomainManager(); + this.catalogManager = new CatalogManager(); this.whitelistManager = new IPWhitelistManager(); this.renderer = new ConfigRenderer(); this.sidebar = new ConfigSidebar(); @@ -287,15 +288,20 @@ if (typeof window.ConfigManager === 'undefined') { // Special handling for domains section var isDomains = subcategoryName.includes('domains') || subcategoryName.includes('network_domains'); - + + // Special handling for the App Catalogs block (CFG_CATALOG_*) + var isCatalogs = subcategoryName === 'general_catalogs' || subcategoryName.includes('catalogs'); + // Special handling for IP whitelist section var isWhitelist = subcategoryName === 'network_whitelist' || subcategoryName.includes('whitelist'); - + var resultHTML = ''; - + if (isDomains) { // Render domains section with special handling resultHTML = await this.domainManager.renderDomainsSection(configItems, displaySubcategory, subcategoryDescription); + } else if (isCatalogs) { + resultHTML = await this.catalogManager.renderCatalogsSection(configItems, displaySubcategory, subcategoryDescription); } else if (isWhitelist) { resultHTML = await this.whitelistManager.renderWhitelistSection(configItems, displaySubcategory, subcategoryDescription); } else if (enabledKey) { diff --git a/containers/libreportal/frontend/core/boot/js/system-loader.js b/containers/libreportal/frontend/core/boot/js/system-loader.js index a7a1cfc..5701d2a 100755 --- a/containers/libreportal/frontend/core/boot/js/system-loader.js +++ b/containers/libreportal/frontend/core/boot/js/system-loader.js @@ -43,6 +43,7 @@ class SystemLoader { '/components/admin/config/js/toggle-manager.js', '/components/admin/config/js/config-core.js', '/components/admin/config/js/domain-manager.js', + '/components/admin/config/js/catalog-manager.js', '/components/admin/config/js/ip-whitelist-manager.js', '/components/admin/config/js/config-renderer.js', '/components/admin/config/js/config-sidebar.js',