diff --git a/containers/libreportal/frontend/components/admin/core/css/admin.css b/containers/libreportal/frontend/components/admin/core/css/admin.css index 310f270..bccd41f 100644 --- a/containers/libreportal/frontend/components/admin/core/css/admin.css +++ b/containers/libreportal/frontend/components/admin/core/css/admin.css @@ -84,10 +84,52 @@ .sys-images-toolbar { display: flex; align-items: center; - justify-content: flex-end; - gap: 14px; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px 14px; margin: 10px 0 6px; } +.sys-images-actions { + display: flex; + align-items: center; + gap: 14px; +} +/* Storage → Images: All / In use / Unused segmented filter. */ +.sys-img-filter { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px; + background: rgba(var(--text-rgb), 0.05); + border: 1px solid rgba(var(--text-rgb), 0.10); + border-radius: 9px; +} +.sys-img-filter-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + border: 0; + background: transparent; + color: var(--text-secondary); + font-size: 11px; + font-weight: 500; + line-height: 1.4; + border-radius: 7px; + cursor: pointer; + transition: background 0.18s ease, color 0.18s ease; +} +.sys-img-filter-btn:hover { color: var(--text-primary); } +.sys-img-filter-btn.is-active { + background: rgba(var(--accent-rgb), 0.16); + color: var(--accent); +} +.sys-img-filter-n { + font-size: 10px; + font-variant-numeric: tabular-nums; + opacity: 0.65; +} +.sys-img-filter-btn.is-active .sys-img-filter-n { opacity: 0.9; } .sys-images-list { margin-top: 4px; } .sys-image-item .task-header { cursor: default; } .sys-img-icon { color: rgba(var(--text-rgb), 0.6); flex-shrink: 0; } diff --git a/containers/libreportal/frontend/components/admin/system/js/system-storage-page.js b/containers/libreportal/frontend/components/admin/system/js/system-storage-page.js index 3b634d2..5671122 100644 --- a/containers/libreportal/frontend/components/admin/system/js/system-storage-page.js +++ b/containers/libreportal/frontend/components/admin/system/js/system-storage-page.js @@ -24,6 +24,7 @@ class SystemStoragePage { this._timer = null; this._expanded = new Set(); // app names whose folder breakdown is open this._selectedImageIds = new Set(); // images ticked for bulk delete + this._imageFilter = 'all'; // images list filter: all | inuse | unused this._deleting = false; // a remove-images task is in flight this._onClick = this._onClick.bind(this); this._onChange = this._onChange.bind(this); @@ -67,12 +68,25 @@ class SystemStoragePage { // Remove a single image. const del = e.target.closest('[data-image-delete]'); if (del) { this._deleteImages([del.getAttribute('data-image-delete')]); return; } - // Clear All / Delete Selected. + // Filter chip (All / In use / Unused) — re-render the list in place, + // dropping any selection that belongs to the now-hidden group. + const filt = e.target.closest('[data-image-filter]'); + if (filt) { + const f = filt.getAttribute('data-image-filter'); + if (f && f !== this._imageFilter) { + this._imageFilter = f; + this._selectedImageIds.clear(); + this._rerenderImages(); + } + return; + } + // Clear All / Delete Selected. With no explicit selection, act on the + // rows currently shown (i.e. honour the active filter). const clr = e.target.closest('[data-images-clear]'); if (clr) { const ids = this._selectedImageIds.size ? [...this._selectedImageIds] - : ((this.data && this.data.image_list) || []).map(im => im.id); + : this._visibleImages().map(im => im.id); this._deleteImages(ids); return; } @@ -354,7 +368,7 @@ class SystemStoragePage { dockerSection = `