Compare commits
2 Commits
325f8f3be8
...
67970a84e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67970a84e9 | ||
|
|
2510b1a52f |
@ -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
|
||||
|
||||
@ -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()}
|
||||
<div class="updater-toolbar ov-toolbar">
|
||||
<div class="ov-chips">${chip('all', 'All', up.apps.length)}${chip('updates', 'Updates', nUpd)}${chip('newer', 'Newer', nNew)}${chip('security', 'Security', nSec)}</div>
|
||||
${nUpd ? `<label class="ov-select-all" title="Select every updatable app in view">
|
||||
<input type="checkbox" data-overview-action="select-all"${this._allPicked() ? ' checked' : ''}>
|
||||
<span>Select all</span>
|
||||
</label>` : ''}
|
||||
</div>
|
||||
${nUpd ? this.renderSelectionBar() : ''}
|
||||
<div class="updater-list ov-updates-list">${rows}</div>`;
|
||||
@ -765,20 +793,25 @@ class OverviewManager {
|
||||
const updBtn = a.update_available
|
||||
? `<button class="updater-btn updater-btn-primary" data-updater-action="update" data-app="${slug}">Update</button>`
|
||||
: (a.newer_version
|
||||
? `<button class="updater-btn ov-upgrade-btn" data-updater-action="upgrade" data-app="${slug}" data-version="${esc(a.newer_version)}" title="Move to the ${esc(a.newer_version)} release line — you will be asked to confirm first">Upgrade to ${esc(a.newer_version)}</button>`
|
||||
? `<button class="updater-btn updater-btn-success" data-updater-action="upgrade" data-app="${slug}" data-version="${esc(a.newer_version)}" title="Move to the ${esc(a.newer_version)} release line — you will be asked to confirm first">Upgrade</button>`
|
||||
: '');
|
||||
// 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
|
||||
? `<span class="ov-pick"><input type="checkbox" class="ov-pick-box" data-overview-action="select" data-app="${slug}"${this.selected.has(a.name) ? ' checked' : ''} aria-label="Select ${name} for update"></span>`
|
||||
: '<span class="ov-pick ov-pick-empty" aria-hidden="true"></span>';
|
||||
: '';
|
||||
return `<div class="updater-row ov-row" data-app="${slug}">
|
||||
<div class="updater-row-head ov-row-head" data-overview-action="toggle" data-app="${slug}" role="button" tabindex="0" aria-expanded="false" aria-controls="ov-detail-${slug}">
|
||||
${pick}<img class="ov-row-icon" src="/core/icons/apps/${slug}.svg" alt="" onerror="this.style.display='none'">
|
||||
<img class="ov-row-icon" src="/core/icons/apps/${slug}.svg" alt="" onerror="this.style.display='none'">
|
||||
<span class="updater-row-name">${name}</span> ${status} ${sev} ${pol} ${newer} ${stale}
|
||||
<span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span>
|
||||
<span class="ov-row-actions">${updBtn}<button class="ov-details-btn" data-overview-action="toggle" data-app="${slug}" tabindex="-1" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"></polyline></svg><span>Details</span></button></span>
|
||||
<span class="ov-row-actions">${updBtn}<button class="ov-details-btn" data-overview-action="toggle" data-app="${slug}" tabindex="-1" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"></polyline></svg><span>Details</span></button>${pick}</span>
|
||||
</div>
|
||||
<div class="updater-row-details ov-row-details" id="ov-detail-${slug}" role="region" aria-label="${name} details" hidden></div>
|
||||
</div>`;
|
||||
|
||||
@ -1355,6 +1355,18 @@
|
||||
background: rgba(var(--text-rgb), 0.05);
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.02em;
|
||||
/* The chip is the last thing in .task-info and .task-actions starts straight
|
||||
after it, so without this the backup id and the Restore button touch. */
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* Roomier rows than the Tasks list they borrow .task-* from. .task-header ships
|
||||
4px of vertical padding, which is right for a dense log of tasks and too tight
|
||||
here: these rows carry a 28px app icon, so the content sat hard against the
|
||||
card edges. Scoped to the snapshot item so the Tasks page keeps its density. */
|
||||
.backup-snapshot-item .task-header {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.backup-snapshot-tag {
|
||||
display: inline-block;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = `
|
||||
<div class="updater-detail-section">
|
||||
<p><strong>${this.escape(app)}</strong> will move from <strong>${this.escape(from)}</strong>
|
||||
<p><strong>${this.escape(label)}</strong> will move from <strong>${this.escape(from)}</strong>
|
||||
to <strong>${this.escape(to)}</strong>, one release at a time.</p>
|
||||
<p class="updater-detail-meta">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.</p>
|
||||
<p class="updater-detail-meta">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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = `
|
||||
<div class="confirmation-header">
|
||||
${iconUrl ? `<div class="confirmation-app-icon"><img src="${this.escapeHtml(iconUrl)}" alt="" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg'"></div>` : ''}
|
||||
<h3>${this.escapeHtml(title)}</h3>
|
||||
<button class="confirmation-close" onclick="window.confirmationDialog.hide()">×</button>
|
||||
</div>
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user