diff --git a/configs/general/general_catalogs b/configs/general/general_catalogs index 95f3c47..a4ca06c 100644 --- a/configs/general/general_catalogs +++ b/configs/general/general_catalogs @@ -6,6 +6,7 @@ # 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 # 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_2= # Catalog 2 - Extra catalog base URL CFG_CATALOG_3= # Catalog 3 - Extra catalog base URL diff --git a/containers/libreportal/frontend/components/admin/config/js/catalog-manager.js b/containers/libreportal/frontend/components/admin/config/js/catalog-manager.js index 64626d4..0f0e4a9 100644 --- a/containers/libreportal/frontend/components/admin/config/js/catalog-manager.js +++ b/containers/libreportal/frontend/components/admin/config/js/catalog-manager.js @@ -25,15 +25,48 @@ class CatalogManager { `; } + // 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 ` +
+
+
+ βœ“ Official + + ${this._esc(host)}${off ? ' (hidden from browse)' : ''} + +
+ + ${window.officialCatalogActionHTML(!off)} +
+
`; + } + + 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) { - 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 .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 = ''; + 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 => { const value = (configItems.find(i => i.key === key) || {}).value || ''; blocks += this._block(key, value, this._num(key) === highest); @@ -53,7 +86,7 @@ class CatalogManager {
πŸ›ˆ
- 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. + The official catalog is always included. Add third-party catalog URLs to browse more apps β€” third-party sources are unverified.
@@ -150,3 +183,44 @@ window.validateCatalogFormat = function (input) { input.title = ok ? '' : 'Catalog must be an http(s):// URL'; return ok; }; + +// --- official catalog (source #1): shown pinned, removable only with a warning --- +window.officialCatalogActionHTML = function (enabled) { + return enabled + ? `` + : ``; +}; + +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 ? '' : ' (hidden from browse)'; + } + if (window.notificationSystem) { + window.notificationSystem.info(enabled + ? 'Official catalog will be re-enabled on Save.' + : 'Official catalog will be hidden on Save.'); + } +}; diff --git a/containers/libreportal/frontend/core/forms/css/config.css b/containers/libreportal/frontend/core/forms/css/config.css index 4c81bba..11cba56 100644 --- a/containers/libreportal/frontend/core/forms/css/config.css +++ b/containers/libreportal/frontend/core/forms/css/config.css @@ -265,6 +265,20 @@ 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) because the base 3-column rule above loads later than style.css, so the unscoped base would otherwise win the cascade against the diff --git a/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js b/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js index c826a77..0f9a1b1 100755 --- a/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js +++ b/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js @@ -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; // Build dialog content @@ -50,7 +50,7 @@ class ConfirmationDialog {
` : ''} @@ -142,12 +142,12 @@ function initConfirmationDialog() { // initConfirmationDialog() will be called centrally // 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 initConfirmationDialog(); - + if (confirmationDialog) { - confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox); + confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText); } else { // Fallback to native confirm if (confirm(message)) { diff --git a/scripts/catalog/catalog_sources.sh b/scripts/catalog/catalog_sources.sh index 1bd91dc..6b8ebc0 100644 --- a/scripts/catalog/catalog_sources.sh +++ b/scripts/catalog/catalog_sources.sh @@ -40,26 +40,29 @@ catalogExtraSourcesJson() { 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() { - local off_base off_channel extras + local off_base off_channel extras off_enabled off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}" off_channel="$(lpReleaseChannel)" + off_enabled="true"; [[ "${CFG_CATALOG_OFFICIAL_ENABLED:-true}" == "false" ]] && off_enabled="false" 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, - official: true, trust: "official" } ] - + ( $extras | map( . + { official: false, trust: "community" } ) )' + official: true, trust: "official", enabled: $oe } ] + + ( $extras | map( . + { official: false, trust: "community", enabled: true } ) )' } -# One source per line (compact JSON), priority order β€” for `while read`. Every -# configured source is active (empty slots are skipped upstream). -catalogEnabledSources() { catalogSourcesJson | jq -c '.[]'; } +# One ENABLED source per line (compact JSON), priority order β€” for `while read`. +catalogEnabledSources() { catalogSourcesJson | jq -c '.[] | select(.enabled)'; } # Human-readable listing for the CLI. catalogSourceList() { 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