Merge claude/2
This commit is contained in:
commit
06723e0995
@ -6,6 +6,7 @@
|
|||||||
# in priority order 1..9. The official catalog (from the Release Host) is always
|
# in priority order 1..9. The official catalog (from the Release Host) is always
|
||||||
# source #1 and is NOT listed here. Third-party catalogs are UNVERIFIED — you are
|
# source #1 and is NOT listed here. Third-party catalogs are UNVERIFIED — you are
|
||||||
# trusting that host. Edit these from the App Center's Catalog Sources block.
|
# trusting that host. Edit these from the App Center's Catalog Sources block.
|
||||||
|
CFG_CATALOG_OFFICIAL_ENABLED=true # Official Catalog - Include the official LibrePortal catalog (source #1) in the App Center. Disabling only hides its apps from browsing; system updates/hotfixes still use the official release host. [true:On|false:Off] **ADVANCED**
|
||||||
CFG_CATALOG_1= # Catalog 1 - Extra catalog base URL, e.g. https://catalog.example.org
|
CFG_CATALOG_1= # Catalog 1 - Extra catalog base URL, e.g. https://catalog.example.org
|
||||||
CFG_CATALOG_2= # Catalog 2 - Extra catalog base URL
|
CFG_CATALOG_2= # Catalog 2 - Extra catalog base URL
|
||||||
CFG_CATALOG_3= # Catalog 3 - Extra catalog base URL
|
CFG_CATALOG_3= # Catalog 3 - Extra catalog base URL
|
||||||
|
|||||||
@ -25,15 +25,48 @@ class CatalogManager {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The pinned official row (source #1). URL comes from the generated catalog
|
||||||
|
// data; the hidden CFG_CATALOG_OFFICIAL_ENABLED input carries its on/off state
|
||||||
|
// so it saves with the rest on "Save Config".
|
||||||
|
_officialRow(base, enabled) {
|
||||||
|
const host = base ? base.replace(/^https?:\/\//, '').replace(/\/$/, '') : 'official catalog';
|
||||||
|
const off = enabled === false;
|
||||||
|
return `
|
||||||
|
<div class="domain-building-block" data-catalog-official style="${off ? 'opacity:.55;' : ''}">
|
||||||
|
<div class="domain-header" style="align-items:center; gap:.6rem;">
|
||||||
|
<div style="flex:1; display:flex; align-items:center; gap:.6rem; min-width:0;">
|
||||||
|
<span style="display:inline-flex; align-items:center; padding:.15rem .55rem; border-radius:999px; background:rgba(46,204,113,.15); color:#2ecc71; font-weight:600; font-size:.8rem; white-space:nowrap;">✓ Official</span>
|
||||||
|
<span style="opacity:.85; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
|
||||||
|
${this._esc(host)}<span data-official-hint>${off ? ' <em style="opacity:.7;">(hidden from browse)</em>' : ''}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="CFG_CATALOG_OFFICIAL_ENABLED" id="config-CFG_CATALOG_OFFICIAL_ENABLED" value="${off ? 'false' : 'true'}">
|
||||||
|
<span data-official-action>${window.officialCatalogActionHTML(!off)}</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _officialBase() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/data/apps/generated/registry_catalog.json', { cache: 'no-store' });
|
||||||
|
if (r.ok) { const j = await r.json(); return (j.source && j.source.base) || ''; }
|
||||||
|
} catch (_) {}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
async renderCatalogsSection(configItems, displaySubcategory, subcategoryDescription) {
|
async renderCatalogsSection(configItems, displaySubcategory, subcategoryDescription) {
|
||||||
const keys = configItems.map(i => i.key).filter(k => k.startsWith('CFG_CATALOG_'));
|
const keys = configItems.map(i => i.key).filter(k => k.startsWith('CFG_CATALOG_') && k !== 'CFG_CATALOG_OFFICIAL_ENABLED');
|
||||||
const withContent = keys
|
const withContent = keys
|
||||||
.filter(k => ((configItems.find(i => i.key === k) || {}).value || '').trim() !== '')
|
.filter(k => ((configItems.find(i => i.key === k) || {}).value || '').trim() !== '')
|
||||||
.sort((a, b) => this._num(a) - this._num(b));
|
.sort((a, b) => this._num(a) - this._num(b));
|
||||||
const highest = withContent.length ? Math.max(...withContent.map(k => this._num(k))) : 0;
|
const highest = withContent.length ? Math.max(...withContent.map(k => this._num(k))) : 0;
|
||||||
const isMax = withContent.length >= 9;
|
const isMax = withContent.length >= 9;
|
||||||
|
|
||||||
let blocks = '';
|
const officialItem = configItems.find(i => i.key === 'CFG_CATALOG_OFFICIAL_ENABLED') || {};
|
||||||
|
const officialEnabled = String(officialItem.value == null ? 'true' : officialItem.value) !== 'false';
|
||||||
|
const officialBase = await this._officialBase();
|
||||||
|
|
||||||
|
let blocks = this._officialRow(officialBase, officialEnabled);
|
||||||
withContent.forEach(key => {
|
withContent.forEach(key => {
|
||||||
const value = (configItems.find(i => i.key === key) || {}).value || '';
|
const value = (configItems.find(i => i.key === key) || {}).value || '';
|
||||||
blocks += this._block(key, value, this._num(key) === highest);
|
blocks += this._block(key, value, this._num(key) === highest);
|
||||||
@ -53,7 +86,7 @@ class CatalogManager {
|
|||||||
<div class="traefik-warning-content">
|
<div class="traefik-warning-content">
|
||||||
<span class="traefik-warning-icon">🛈</span>
|
<span class="traefik-warning-icon">🛈</span>
|
||||||
<div class="traefik-warning-text">
|
<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>.
|
<strong>The official catalog is always included.</strong> Add third-party catalog URLs to browse more apps — third-party sources are <em>unverified</em>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -150,3 +183,44 @@ window.validateCatalogFormat = function (input) {
|
|||||||
input.title = ok ? '' : 'Catalog must be an http(s):// URL';
|
input.title = ok ? '' : 'Catalog must be an http(s):// URL';
|
||||||
return ok;
|
return ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- official catalog (source #1): shown pinned, removable only with a warning ---
|
||||||
|
window.officialCatalogActionHTML = function (enabled) {
|
||||||
|
return enabled
|
||||||
|
? `<button type="button" class="delete-domain-btn" title="Remove the official catalog from browsing" onclick="window.deleteOfficialCatalog()"><span class="delete-icon">×</span></button>`
|
||||||
|
: `<button type="button" class="btn btn-secondary" style="padding:.3rem .8rem;" onclick="window.enableOfficialCatalog()">Re-enable</button>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deleteOfficialCatalog = function () {
|
||||||
|
const apply = () => window._setOfficialCatalog(false);
|
||||||
|
if (window.showConfirmation) {
|
||||||
|
window.showConfirmation(
|
||||||
|
'Remove the official catalog?',
|
||||||
|
'This hides the official LibrePortal catalog’s apps from the App Center. Your system updates and hotfixes are NOT affected — they always use the official release host. You can re-enable it any time. Click Save Config to apply.',
|
||||||
|
apply, 'Remove from browsing', 'Cancel', 'danger', true,
|
||||||
|
'I understand this only hides official apps from browsing, not system updates'
|
||||||
|
);
|
||||||
|
} else if (window.confirm('Remove the official catalog from browsing?')) {
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.enableOfficialCatalog = function () { window._setOfficialCatalog(true); };
|
||||||
|
|
||||||
|
window._setOfficialCatalog = function (enabled) {
|
||||||
|
const input = document.getElementById('config-CFG_CATALOG_OFFICIAL_ENABLED');
|
||||||
|
if (input) input.value = enabled ? 'true' : 'false';
|
||||||
|
const row = document.querySelector('[data-catalog-official]');
|
||||||
|
if (row) {
|
||||||
|
row.style.opacity = enabled ? '' : '.55';
|
||||||
|
const action = row.querySelector('[data-official-action]');
|
||||||
|
if (action) action.innerHTML = window.officialCatalogActionHTML(enabled);
|
||||||
|
const hint = row.querySelector('[data-official-hint]');
|
||||||
|
if (hint) hint.innerHTML = enabled ? '' : ' <em style="opacity:.7;">(hidden from browse)</em>';
|
||||||
|
}
|
||||||
|
if (window.notificationSystem) {
|
||||||
|
window.notificationSystem.info(enabled
|
||||||
|
? 'Official catalog will be re-enabled on Save.'
|
||||||
|
: 'Official catalog will be hidden on Save.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -265,6 +265,20 @@
|
|||||||
grid-auto-rows: minmax(min-content, max-content);
|
grid-auto-rows: minmax(min-content, max-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Boxed content panel for a config section's body (the area after the divider,
|
||||||
|
before any action buttons). A darker inset inside the section card so the
|
||||||
|
options read as their own content section — matches the boxed look used
|
||||||
|
elsewhere. Applies to regular option grids and the block editors
|
||||||
|
(domains/catalog/whitelist). Scoped to the main config editor (not app-config). */
|
||||||
|
.config-category:not(.app-config) .config-fields,
|
||||||
|
.config-category:not(.app-config) .domain-building-blocks,
|
||||||
|
.config-category:not(.app-config) .whitelist-building-blocks {
|
||||||
|
background: rgba(0, 0, 0, 0.20);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive column counts — colocated here (and not in style.css)
|
/* Responsive column counts — colocated here (and not in style.css)
|
||||||
because the base 3-column rule above loads later than style.css,
|
because the base 3-column rule above loads later than style.css,
|
||||||
so the unscoped base would otherwise win the cascade against the
|
so the unscoped base would otherwise win the cascade against the
|
||||||
|
|||||||
@ -31,8 +31,8 @@ class ConfirmationDialog {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show(title, message, onConfirm, confirmText = 'Confirm', cancelText = 'Cancel', confirmClass = 'primary', showDataLossCheckbox = false) {
|
show(title, message, onConfirm, confirmText = 'Confirm', cancelText = 'Cancel', confirmClass = 'primary', showDataLossCheckbox = false, checkboxText = 'I understand I will lose all my data and it cannot be undone') {
|
||||||
|
|
||||||
this.callback = onConfirm;
|
this.callback = onConfirm;
|
||||||
|
|
||||||
// Build dialog content
|
// Build dialog content
|
||||||
@ -50,7 +50,7 @@ class ConfirmationDialog {
|
|||||||
<div class="confirmation-checkbox">
|
<div class="confirmation-checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="dataLossCheckbox" onchange="window.confirmationDialog.updateConfirmButton(this.checked)">
|
<input type="checkbox" id="dataLossCheckbox" onchange="window.confirmationDialog.updateConfirmButton(this.checked)">
|
||||||
<span>I understand I will lose all my data and it cannot be undone</span>
|
<span>${this.escapeHtml(checkboxText)}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@ -142,12 +142,12 @@ function initConfirmationDialog() {
|
|||||||
// initConfirmationDialog() will be called centrally
|
// initConfirmationDialog() will be called centrally
|
||||||
|
|
||||||
// Global function
|
// Global function
|
||||||
window.showConfirmation = (title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox) => {
|
window.showConfirmation = (title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText) => {
|
||||||
// Ensure dialog is initialized
|
// Ensure dialog is initialized
|
||||||
initConfirmationDialog();
|
initConfirmationDialog();
|
||||||
|
|
||||||
if (confirmationDialog) {
|
if (confirmationDialog) {
|
||||||
confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox);
|
confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to native confirm
|
// Fallback to native confirm
|
||||||
if (confirm(message)) {
|
if (confirm(message)) {
|
||||||
|
|||||||
@ -40,26 +40,29 @@ catalogExtraSourcesJson() {
|
|||||||
printf '%s' "$arr"
|
printf '%s' "$arr"
|
||||||
}
|
}
|
||||||
|
|
||||||
# The full ordered source list as a JSON array. Each: {idx,name,base,channel,official,trust}.
|
# The full ordered source list as a JSON array. Each:
|
||||||
|
# {idx,name,base,channel,official,trust,enabled}. The official source can be
|
||||||
|
# switched off for browsing via CFG_CATALOG_OFFICIAL_ENABLED (default on) —
|
||||||
|
# extras are always enabled (an empty slot is simply absent).
|
||||||
catalogSourcesJson() {
|
catalogSourcesJson() {
|
||||||
local off_base off_channel extras
|
local off_base off_channel extras off_enabled
|
||||||
off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}"
|
off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}"
|
||||||
off_channel="$(lpReleaseChannel)"
|
off_channel="$(lpReleaseChannel)"
|
||||||
|
off_enabled="true"; [[ "${CFG_CATALOG_OFFICIAL_ENABLED:-true}" == "false" ]] && off_enabled="false"
|
||||||
extras="$(catalogExtraSourcesJson)"
|
extras="$(catalogExtraSourcesJson)"
|
||||||
jq -cn --arg base "$off_base" --arg channel "$off_channel" --argjson extras "$extras" '
|
jq -cn --arg base "$off_base" --arg channel "$off_channel" --argjson oe "$off_enabled" --argjson extras "$extras" '
|
||||||
[ { idx: 1, name: "LibrePortal Official", base: $base, channel: $channel,
|
[ { idx: 1, name: "LibrePortal Official", base: $base, channel: $channel,
|
||||||
official: true, trust: "official" } ]
|
official: true, trust: "official", enabled: $oe } ]
|
||||||
+ ( $extras | map( . + { official: false, trust: "community" } ) )'
|
+ ( $extras | map( . + { official: false, trust: "community", enabled: true } ) )'
|
||||||
}
|
}
|
||||||
|
|
||||||
# One source per line (compact JSON), priority order — for `while read`. Every
|
# One ENABLED source per line (compact JSON), priority order — for `while read`.
|
||||||
# configured source is active (empty slots are skipped upstream).
|
catalogEnabledSources() { catalogSourcesJson | jq -c '.[] | select(.enabled)'; }
|
||||||
catalogEnabledSources() { catalogSourcesJson | jq -c '.[]'; }
|
|
||||||
|
|
||||||
# Human-readable listing for the CLI.
|
# Human-readable listing for the CLI.
|
||||||
catalogSourceList() {
|
catalogSourceList() {
|
||||||
catalogSourcesJson | jq -r '.[] |
|
catalogSourcesJson | jq -r '.[] |
|
||||||
"[\(.idx)] \(.name) — \(.base)/\(.channel) (\(if .official then "official ✓" else "unverified" end))"'
|
"[\(.idx)] \(.name) — \(.base)/\(.channel) (\(if .official then "official ✓" else "unverified" end)\(if .enabled then "" else ", off" end))"'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fetch a THIRD-PARTY catalog index. Deliberately NOT verified against the
|
# Fetch a THIRD-PARTY catalog index. Deliberately NOT verified against the
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user