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()}
-
${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 = `
@@ -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;