diff --git a/containers/libreportal/frontend/components/apps/overview/css/overview.css b/containers/libreportal/frontend/components/apps/overview/css/overview.css index 0dca797..12670a2 100644 --- a/containers/libreportal/frontend/components/apps/overview/css/overview.css +++ b/containers/libreportal/frontend/components/apps/overview/css/overview.css @@ -194,6 +194,38 @@ padding: 0 6px; } .ov-toolbar-actions { display: flex; gap: 8px; } +/* Select-all, mirroring the Tasks toolbar: pushed to the right of the chips so + the picker column and its master control line up on the same edge. */ +.ov-select-all { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-secondary); + cursor: pointer; + white-space: nowrap; +} +.ov-select-all input { + width: 15px; + height: 15px; + accent-color: var(--accent); + cursor: pointer; +} +/* Trailing picker. No fixed width reserved: an unselectable row renders no + checkbox at all, so holding a column open would reintroduce the gutter this + replaced. */ +.ov-pick { + display: inline-flex; + align-items: center; + margin-left: 4px; +} +.ov-pick-box { + width: 15px; + height: 15px; + accent-color: var(--accent); + cursor: pointer; +} /* ---- Updates: expandable rows ------------------------------------------- */ /* Block (not the inherited .updater-row 3-col grid) so the head sits on top and diff --git a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js index dae77d6..07b539a 100644 --- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js +++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js @@ -331,6 +331,21 @@ class OverviewManager { this.selected.clear(); this._applyTab('updates'); return; + case 'select-all': { + // Acts on what is actually selectable and in view — the filter chips + // can narrow the list, and "select all" that quietly picked rows the + // user cannot see would be a worse answer than none. + const boxes = Array.from(document.querySelectorAll('#overview-view .ov-pick-box')); + const on = !!(oa.checked); + for (const b of boxes) { + b.checked = on; + const slug = b.dataset.app; + if (!slug) continue; + if (on) this.selected.add(slug); else this.selected.delete(slug); + } + this._syncSelectionBar(); + break; + } case 'update-selected': if (this.updater) this.updater.applySelected([...this.selected]); this.selected.clear(); @@ -606,6 +621,15 @@ class OverviewManager { return [...this.selected]; } + // True when every selectable row currently in view is picked — drives the + // master checkbox's checked state on render. + _allPicked() { + const up = this.updater; + if (!up) return false; + const avail = up.apps.filter((a) => a.update_available).map((a) => a.name); + return avail.length > 0 && avail.every((n) => this.selected.has(n)); + } + // Update the bar in place. Re-rendering the whole tab on every tick would // rebuild the checkboxes underneath the pointer and lose focus mid-selection. _syncSelectionBar() { @@ -655,6 +679,10 @@ class OverviewManager { ${up.renderAutoCheckLine()}
${chip('all', 'All', up.apps.length)}${chip('updates', 'Updates', nUpd)}${chip('newer', 'Newer', nNew)}${chip('security', 'Security', nSec)}
+ ${nUpd ? `` : ''}
${nUpd ? this.renderSelectionBar() : ''}
${rows}
`; @@ -765,20 +793,25 @@ class OverviewManager { const updBtn = a.update_available ? `` : (a.newer_version - ? `` + ? `` : ''); // Only rows with something to apply are selectable. A checkbox on a row // that is already current would offer a choice with no outcome, and // "selected 6, updated 2" is a worse answer than not offering the 4. + // Trailing, and absent rather than blank when the row is not selectable. + // It used to lead the row with an `ov-pick-empty` spacer holding the column + // open — so with nothing updatable, every row carried a left gutter for a + // checkbox that was never coming. Matches the Tasks list, where the picker + // sits last and unselectable rows simply have none. const pick = a.update_available ? `` - : ''; + : ''; return `
`; diff --git a/containers/libreportal/frontend/components/updater/css/updater.css b/containers/libreportal/frontend/components/updater/css/updater.css index 0d49ad0..0d97150 100644 --- a/containers/libreportal/frontend/components/updater/css/updater.css +++ b/containers/libreportal/frontend/components/updater/css/updater.css @@ -184,6 +184,17 @@ color: rgb(var(--page-rgb, var(--accent-rgb))); } .updater-btn-primary:hover { background: rgba(var(--page-rgb, var(--accent-rgb)), 0.3); } +/* Green, for moving to a newer release line. Distinct from the primary (accent) + Update so the two actions on a row never read as the same thing. + Pastel #86efac text on a saturated green rather than --status-success itself, + matching .app-tag.installed-tag: the raw #28a745 is too dark to read at button + size on dark themes. */ +.updater-btn-success { + background: rgba(var(--status-success-rgb), 0.30); + border-color: rgba(var(--status-success-rgb), 0.65); + color: #86efac; +} +.updater-btn-success:hover { background: rgba(var(--status-success-rgb), 0.45); } /* ---- Hints / empty states ---- */ .updater-hint { padding: 12px 15px; border-radius: 11px; margin-bottom: 14px; font-size: 0.84rem; diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index 41b20bc..97194fd 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -339,12 +339,15 @@ class UpdaterPage { const a = this.apps.find((x) => x.name === app) || {}; const from = a.channel || a.current_version || 'the current version'; const to = version || a.newer_version || 'the newest release'; + // Display name, not the slug: the dialog now leads with the app's icon, and + // "Upgrade matrix to…" beside the Matrix logo reads as a different thing. + const label = (window.getAppDisplayName ? window.getAppDisplayName(app) : null) || a.displayName || app; const body = `
-

${this.escape(app)} will move from ${this.escape(from)} +

${this.escape(label)} will move from ${this.escape(from)} to ${this.escape(to)}, one release at a time.

Every step takes its own recovery snapshot first, then waits for - ${this.escape(app)} to confirm it is serving that version with no migration outstanding. + ${this.escape(label)} to confirm it is serving that version with no migration outstanding. If any step fails it is rolled back and the upgrade stops there, leaving the app on the last version that verified.

This can take a long time — each release runs its own database @@ -353,8 +356,9 @@ class UpdaterPage { const go = () => this.dispatch('updater_upgrade', { app, version: version || '' }, `Upgrading ${app} to ${to}, one release at a time…`); if (window.showConfirmation) { - window.showConfirmation(`Upgrade ${app} to ${to}?`, '', go, 'Start upgrade', 'Cancel', 'warning', false, '', body); - } else if (window.confirm(`Upgrade ${app} from ${from} to ${to}, one release at a time?`)) { + window.showConfirmation(`Upgrade ${label} to ${to}?`, '', go, 'Start upgrade', 'Cancel', 'warning', false, '', body, + `/core/icons/apps/${app}.svg`); + } else if (window.confirm(`Upgrade ${label} from ${from} to ${to}, one release at a time?`)) { go(); } } diff --git a/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js b/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js index 3db27d9..a696621 100755 --- a/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js +++ b/containers/libreportal/frontend/core/overlays/js/confirmation-dialog.js @@ -31,13 +31,14 @@ class ConfirmationDialog { } - 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', messageHtml = '') { + 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', messageHtml = '', iconUrl = '') { this.callback = onConfirm; // Build dialog content this.dialog.innerHTML = `

+ ${iconUrl ? `
` : ''}

${this.escapeHtml(title)}

@@ -142,12 +143,12 @@ function initConfirmationDialog() { // initConfirmationDialog() will be called centrally // Global function -window.showConfirmation = (title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml) => { +window.showConfirmation = (title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml, iconUrl) => { // Ensure dialog is initialized initConfirmationDialog(); if (confirmationDialog) { - confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml); + confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml, iconUrl); } else { // Fallback to native confirm if (confirm(message)) { diff --git a/containers/libreportal/frontend/core/theme/css/base.css b/containers/libreportal/frontend/core/theme/css/base.css index 4bd6f5a..a065e65 100755 --- a/containers/libreportal/frontend/core/theme/css/base.css +++ b/containers/libreportal/frontend/core/theme/css/base.css @@ -901,8 +901,33 @@ html[data-theme="nebula"]::after { border-bottom: 1px solid var(--border-color, #444); } +/* Icon holder, when a caller names an app. Styled here rather than reusing + .app-card-icon from apps.css: this dialog is global and appears on pages that + never load the apps stylesheet, where borrowing that class would render an + unstyled image. */ +.confirmation-app-icon { + width: 40px; + height: 40px; + min-width: 40px; + padding: 7px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(var(--text-rgb), 0.1); + border: 1px solid rgba(var(--text-rgb), 0.2); +} +.confirmation-app-icon img { + width: 100%; + height: 100%; + object-fit: contain; +} + .confirmation-header h3 { margin: 0; + /* Takes the slack so the close button stays pinned right once an icon leads + the header — the row is space-between, which would otherwise centre it. */ + flex: 1; color: var(--text-primary); color: var(--text-primary, #fff); font-size: 16px;