Merge claude/2
This commit is contained in:
commit
4242159217
@ -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(/</g, '<'); }
|
||||
|
||||
_block(key, value, canDelete) {
|
||||
const fieldId = `config-${key}`;
|
||||
return `
|
||||
<div class="domain-building-block">
|
||||
<div class="domain-header">
|
||||
<input type="url" id="${fieldId}" name="${key}" value="${this._esc(value)}"
|
||||
class="form-control catalog-input" placeholder="https://catalog.example.org"
|
||||
oninput="window.validateCatalogFormat && window.validateCatalogFormat(this)">
|
||||
<button type="button" class="delete-domain-btn ${!canDelete ? 'disabled' : ''}"
|
||||
onclick="window.deleteCatalog('${key}', this)"
|
||||
title="${canDelete ? 'Remove catalog' : 'Only the last catalog can be removed'}"
|
||||
${!canDelete ? 'disabled' : ''}>
|
||||
<span class="delete-icon">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="config-category">
|
||||
<div class="domains-wrapper">
|
||||
<div class="domains-header">
|
||||
<div>
|
||||
<h3>${displaySubcategory}</h3>
|
||||
<p class="category-description">${subcategoryDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domains-divider"></div>
|
||||
<div class="traefik-warning-banner">
|
||||
<div class="traefik-warning-content">
|
||||
<span class="traefik-warning-icon">🛈</span>
|
||||
<div class="traefik-warning-text">
|
||||
<strong>The official catalog is always included.</strong> Add third-party catalog URLs to browse more apps — third-party sources are <em>unverified</em> (you are trusting that host). Each is served with a signed <code>/<channel>/index.json</code>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domain-building-blocks">
|
||||
${blocks}
|
||||
</div>
|
||||
<div class="domain-actions">
|
||||
<button type="button" id="add-catalog-btn" class="btn ${isMax ? 'btn-secondary' : 'btn-primary'}"
|
||||
onclick="window.addCatalog()" ${isMax ? 'disabled' : ''}>
|
||||
<span class="add-icon">${isMax ? '✓' : '+'}</span>${isMax ? 'Maximum Catalogs Reached' : 'Add Catalog'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer spacer-lg"></div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 = `
|
||||
<div class="domain-building-block">
|
||||
<div class="domain-header">
|
||||
<input type="url" id="${fieldId}" name="${key}" value="" class="form-control catalog-input"
|
||||
placeholder="https://catalog.example.org"
|
||||
oninput="window.validateCatalogFormat && window.validateCatalogFormat(this)">
|
||||
<button type="button" class="delete-domain-btn" onclick="window.deleteCatalog('${key}', this)">
|
||||
<span class="delete-icon">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
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 = `<span class="add-icon">${isMax ? '✓' : '+'}</span>${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;
|
||||
};
|
||||
@ -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) {
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user