feat: add soft delete and media tab for user attachments with admin restore
This commit is contained in:
@@ -82,61 +82,9 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.attachment-lightbox {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attachment-lightbox.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.attachment-lightbox img {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.attachment-lightbox-close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.attachment-gallery-item {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.attachment-lightbox-close {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,7 +867,7 @@ button.comment-form-submit:hover {
|
||||
.context-menu,
|
||||
.dp-toast-host,
|
||||
.feed-fab,
|
||||
.attachment-lightbox {
|
||||
.lightbox-overlay {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
img[data-lightbox] {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.lightbox-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
cursor: zoom-out;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.lightbox-overlay.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.lightbox-overlay .lightbox-image {
|
||||
max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.lightbox-close {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
.media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: var(--space-md);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.media-tile {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.media-tile .gallery-thumb,
|
||||
.media-tile .gallery-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media-tile .gallery-video {
|
||||
background: #000;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.media-tile .non-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.media-tile .non-image .icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.media-tile .non-image .name {
|
||||
font-size: 0.6875rem;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.media-tile .non-image .size {
|
||||
font-size: 0.625rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.media-tile-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-sm);
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), transparent 45%, transparent 70%, rgba(0, 0, 0, 0.55));
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.media-tile:hover .media-tile-overlay,
|
||||
.media-tile:focus-within .media-tile-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.media-tile-overlay > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.media-source-link {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--white);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.media-source-link:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.media-delete-btn {
|
||||
margin-left: auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: var(--white);
|
||||
font-size: 0.9375rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.media-delete-btn:hover {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.media-tile.media-removing {
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.media-tile-meta {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm);
|
||||
font-size: 0.6875rem;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.media-uploader {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.media-uploader:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.media-deleted-at {
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.media-tile-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.media-tile-actions .admin-inline-form {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.media-tile-actions .admin-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.media-tile-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.media-tile-overlay,
|
||||
.media-tile.media-removing {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { Tabs } from "./Tabs.js";
|
||||
import { DeviiTerminal } from "./DeviiTerminal.js";
|
||||
import { ZipDownloader } from "./ZipDownloader.js";
|
||||
import { ProjectForker } from "./ProjectForker.js";
|
||||
import { MediaGallery } from "./MediaGallery.js";
|
||||
import WindowManager from "./components/WindowManager.js";
|
||||
import { ContainerTerminalManager } from "./ContainerTerminalManager.js";
|
||||
|
||||
@@ -31,7 +32,8 @@ class Application {
|
||||
this.dialog = document.createElement("dp-dialog");
|
||||
this.contextMenu = document.createElement("dp-context-menu");
|
||||
this.toast = document.createElement("dp-toast");
|
||||
document.body.append(this.dialog, this.contextMenu, this.toast);
|
||||
this.lightbox = document.createElement("dp-lightbox");
|
||||
document.body.append(this.dialog, this.contextMenu, this.toast, this.lightbox);
|
||||
this.modals = new ModalManager();
|
||||
this.forms = new FormManager();
|
||||
this.votes = new VoteManager();
|
||||
@@ -58,6 +60,7 @@ class Application {
|
||||
this.devii = new DeviiTerminal();
|
||||
this.zipDownloader = new ZipDownloader();
|
||||
this.projectForker = new ProjectForker();
|
||||
this.mediaGallery = new MediaGallery();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ export class ContentEnhancer {
|
||||
initContentRenderer() {
|
||||
document.querySelectorAll("[data-render]").forEach((el) => {
|
||||
contentRenderer.applyTo(el);
|
||||
el.querySelectorAll("img:not(.avatar-img)").forEach((img) => {
|
||||
img.dataset.lightbox = "";
|
||||
});
|
||||
});
|
||||
contentRenderer.highlightAll();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Http } from "./Http.js";
|
||||
|
||||
export class MediaGallery {
|
||||
constructor() {
|
||||
document.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-media-delete]");
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
this.remove(btn);
|
||||
});
|
||||
}
|
||||
|
||||
async remove(btn) {
|
||||
const uid = btn.dataset.mediaDelete;
|
||||
const tile = btn.closest("[data-media-tile]");
|
||||
try {
|
||||
await Http.send(`/media/${uid}/delete`);
|
||||
if (tile) {
|
||||
tile.classList.add("media-removing");
|
||||
setTimeout(() => tile.remove(), 220);
|
||||
}
|
||||
if (window.app && window.app.toast) {
|
||||
window.app.toast.show("Media deleted");
|
||||
}
|
||||
} catch (err) {
|
||||
if (window.app && window.app.toast) {
|
||||
window.app.toast.show(err.message, { type: "error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.MediaGallery = MediaGallery;
|
||||
@@ -0,0 +1,71 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { Component } from "./Component.js";
|
||||
|
||||
export class AppLightbox extends Component {
|
||||
connectedCallback() {
|
||||
if (this._built) {
|
||||
return;
|
||||
}
|
||||
this._built = true;
|
||||
this.lastFocus = null;
|
||||
this.bodyOverflow = "";
|
||||
this.build();
|
||||
this.bind();
|
||||
}
|
||||
|
||||
build() {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "lightbox-overlay";
|
||||
overlay.setAttribute("role", "dialog");
|
||||
overlay.setAttribute("aria-modal", "true");
|
||||
overlay.innerHTML =
|
||||
'<button type="button" class="lightbox-close" aria-label="Close">×</button>' +
|
||||
'<img class="lightbox-image" src="" alt="">';
|
||||
this.appendChild(overlay);
|
||||
|
||||
this.overlay = overlay;
|
||||
this.image = overlay.querySelector(".lightbox-image");
|
||||
this.closeBtn = overlay.querySelector(".lightbox-close");
|
||||
}
|
||||
|
||||
bind() {
|
||||
this.overlay.addEventListener("click", (e) => {
|
||||
if (e.target !== this.image) this.close();
|
||||
});
|
||||
this.closeBtn.addEventListener("click", () => this.close());
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && this.overlay.classList.contains("visible")) {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
document.addEventListener("click", (e) => {
|
||||
const thumb = e.target.closest("img[data-lightbox]");
|
||||
if (!thumb || thumb.closest("a")) return;
|
||||
e.preventDefault();
|
||||
this.open(thumb.dataset.full || thumb.currentSrc || thumb.src, { alt: thumb.alt });
|
||||
});
|
||||
}
|
||||
|
||||
open(src, options) {
|
||||
const opts = options || {};
|
||||
if (!src) return;
|
||||
this.image.src = src;
|
||||
this.image.alt = opts.alt || "";
|
||||
this.lastFocus = document.activeElement;
|
||||
this.bodyOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
this.overlay.classList.add("visible");
|
||||
setTimeout(() => this.closeBtn.focus(), 20);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.overlay.classList.remove("visible");
|
||||
this.image.src = "";
|
||||
document.body.style.overflow = this.bodyOverflow;
|
||||
if (this.lastFocus && this.lastFocus.focus) this.lastFocus.focus();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("dp-lightbox", AppLightbox);
|
||||
@@ -7,4 +7,5 @@ import "./AppUpload.js";
|
||||
import "./AppToast.js";
|
||||
import "./AppDialog.js";
|
||||
import "./AppContextMenu.js";
|
||||
import "./AppLightbox.js";
|
||||
import "./ContainerTerminal.js";
|
||||
|
||||
Reference in New Issue
Block a user