gallery_server_client_s190js/public/js/gallerySelection.js
2026-10-26 11:00:14 +01:00

490 lines
13 KiB
JavaScript

// Selezione multipla persistente rispetto alla virtualizzazione.
(() => {
"use strict";
let mode = null;
const selected = new Map();
let longPressTimer = null;
let longPressPointer = null;
let suppressClickUntil = 0;
const keyOf = (item) => String(item?.id ?? "");
function isSelected(type, item) {
return mode === type && selected.has(keyOf(item));
}
function updateTile(tile, type, item) {
const active = isSelected(type, item);
tile.classList.toggle("selection-selected", active);
tile.setAttribute("aria-selected", String(active));
}
function updateAllTiles() {
document.querySelectorAll("[data-selection-id]").forEach((tile) => {
const type = tile.dataset.selectionType;
const active =
mode === type && selected.has(tile.dataset.selectionId);
tile.classList.toggle("selection-selected", active);
tile.setAttribute("aria-selected", String(active));
});
}
function updateUI() {
const active = mode !== null && selected.size > 0;
document.body.classList.toggle("selection-mode", active);
document.body.classList.toggle(
"selection-photos",
active && mode === "photos"
);
document.body.classList.toggle(
"selection-activities",
active && mode === "activities"
);
const bar = document.getElementById("selectionBar");
const count = document.getElementById("selectionCount");
const actions = document.getElementById("selectionActionsMenu");
if (bar) bar.hidden = !active;
if (count) {
count.textContent = active
? `${selected.size} selezionati`
: "";
}
if (actions) actions.hidden = true;
updateAllTiles();
}
function begin(type, item) {
if (!item || !keyOf(item)) return;
if (mode !== type) {
selected.clear();
mode = type;
}
selected.set(keyOf(item), item);
updateUI();
}
function toggle(type, item) {
if (!item || !keyOf(item)) return;
if (mode !== type) {
begin(type, item);
return;
}
const key = keyOf(item);
if (selected.has(key)) {
selected.delete(key);
} else {
selected.set(key, item);
}
if (selected.size === 0) mode = null;
updateUI();
}
function clear() {
selected.clear();
mode = null;
updateUI();
}
let downloadRunning = false;
async function downloadSelectedPhotos() {
if (downloadRunning) return;
const notify = (message, options = {}) => {
if (window.operationBanner) {
window.operationBanner.show(message, options);
} else {
alert(message);
}
};
if (mode !== "photos") {
notify("Il download è disponibile solo per le foto.", { type: "error" });
return;
}
const items = [...selected.values()].filter(
item => item && item.id != null && String(item.id).trim() !== ""
);
const ids = [...new Set(items.map(item => String(item.id)))];
if (!ids.length) {
notify("Nessuna foto selezionata.", { type: "error" });
return;
}
const token = localStorage.getItem("token");
if (!token) {
notify("Sessione non valida. Accedi nuovamente.", { type: "error" });
return;
}
const button = document.getElementById("selectionDownloadBtn");
const originalButtonHTML = button?.innerHTML;
downloadRunning = true;
if (button) {
button.disabled = true;
button.textContent = "Preparazione...";
}
notify(`Preparazione del download di ${ids.length} foto...`, {
type: "download",
busy: true,
sticky: true
});
try {
const response = await fetch("/photos/download", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ ids })
});
if (!response.ok) {
let message = "Download non riuscito.";
try {
const data = await response.json();
if (data?.error) message = data.error;
} catch {}
throw new Error(message);
}
notify("Download in corso: ricezione del file...", {
type: "download",
busy: true,
sticky: true
});
const blob = await response.blob();
const disposition = response.headers.get("Content-Disposition") || "";
let filename = ids.length === 1
? String(items.find(item => String(item.id) === ids[0])?.name || "foto")
: "foto-selezionate.zip";
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
const plainMatch = disposition.match(/filename="?([^";]+)"?/i);
if (utf8Match) {
try { filename = decodeURIComponent(utf8Match[1]); } catch {}
} else if (plainMatch) {
filename = plainMatch[1];
}
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 60000);
notify(`Download avviato: ${filename}`, {
type: "success",
duration: 5000
});
} catch (err) {
notify(err?.message || "Errore durante il download.", {
type: "error",
duration: 7000
});
} finally {
downloadRunning = false;
if (button) {
button.disabled = false;
if (originalButtonHTML !== undefined) {
button.innerHTML = originalButtonHTML;
}
}
}
}
let softToggleRunning = false;
async function toggleSelectedPhotos() {
if (softToggleRunning) return;
if (mode !== "photos") {
alert("Questa azione è disponibile solo per le foto.");
return;
}
const items = [...selected.values()].filter(
item => item && item.id != null && String(item.id) !== ""
);
if (!items.length) {
alert("Nessuna foto selezionata.");
return;
}
if (!confirm(
`Invertire lo stato cestino di ${items.length} foto selezionate?\n\n` +
"Le foto nel cestino saranno ripristinate; le altre saranno cestinate."
)) return;
const token = localStorage.getItem("token");
if (!token) {
alert("Sessione non valida. Accedi nuovamente.");
return;
}
softToggleRunning = true;
const button = document.getElementById("selectionToggleSoftBtn");
if (button) {
button.disabled = true;
button.textContent = "Elaborazione…";
}
let succeeded = 0;
const failed = [];
try {
// Sequenziale: ogni foto viene inviata una sola volta.
// Non ripetere automaticamente le richieste: l'endpoint è un toggle.
for (const item of items) {
try {
const response = await fetch(
`/photos/${encodeURIComponent(String(item.id))}/toggle-soft`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`
}
}
);
let result = null;
try {
result = await response.json();
} catch {
// La risposta può non contenere JSON.
}
if (!response.ok || result?.ok !== true) {
failed.push(String(item.id));
console.error(
"[GallerySelection] Toggle soft fallito:",
item.id,
response.status,
result
);
continue;
}
succeeded++;
} catch (error) {
failed.push(String(item.id));
console.error(
"[GallerySelection] Errore toggle soft:",
item.id,
error
);
}
}
if (succeeded > 0) {
clear();
window.refreshGallery?.();
}
if (failed.length) {
alert(
`Operazione terminata.\n` +
`Riuscite: ${succeeded}\n` +
`Fallite: ${failed.length}\n\n` +
`ID non riusciti: ${failed.join(", ")}\n\n` +
(succeeded > 0
? "La galleria è stata aggiornata. Verifica gli ID non riusciti prima di riprovare."
: "Nessuna modifica confermata; la selezione è rimasta attiva.")
);
} else {
alert(`Operazione completata: ${succeeded} foto aggiornate.`);
}
} finally {
softToggleRunning = false;
if (button) {
button.disabled = false;
button.textContent = "Soft delete";
}
}
}
function bindTile(tile, type, item, openItem) {
const key = keyOf(item);
tile.dataset.selectionId = key;
tile.dataset.selectionType = type;
tile.setAttribute("aria-selected", "false");
updateTile(tile, type, item);
tile.addEventListener("pointerdown", (event) => {
if (mode !== null) return;
if (event.pointerType === "mouse" && event.button !== 0) return;
clearTimeout(longPressTimer);
longPressPointer = {
id: event.pointerId,
x: event.clientX,
y: event.clientY
};
longPressTimer = setTimeout(() => {
begin(type, item);
suppressClickUntil = Date.now() + 1000;
longPressPointer = null;
}, 550);
});
tile.addEventListener("pointermove", (event) => {
if (!longPressPointer ||
longPressPointer.id !== event.pointerId) return;
const dx = event.clientX - longPressPointer.x;
const dy = event.clientY - longPressPointer.y;
if (Math.hypot(dx, dy) > 12) {
clearTimeout(longPressTimer);
longPressPointer = null;
}
});
const stopPress = () => {
clearTimeout(longPressTimer);
longPressPointer = null;
};
tile.addEventListener("pointerup", stopPress);
tile.addEventListener("pointercancel", stopPress);
tile.addEventListener("contextmenu", (event) => {
event.preventDefault();
event.stopPropagation();
});
tile.addEventListener("click", async (event) => {
if (Date.now() < suppressClickUntil) {
event.preventDefault();
event.stopPropagation();
suppressClickUntil = 0;
return;
}
if (mode !== null) {
event.preventDefault();
event.stopPropagation();
toggle(type, item);
return;
}
await openItem?.();
});
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("selectionCancel")?.addEventListener(
"click", clear
);
const actionsButton = document.getElementById("selectionActionsBtn");
const actionsMenu = document.getElementById("selectionActionsMenu");
document.getElementById("selectionDownloadBtn")?.addEventListener(
"click",
async () => {
if (actionsMenu) actionsMenu.hidden = true;
await downloadSelectedPhotos();
}
);
document.getElementById("selectionToggleSoftBtn")?.addEventListener(
"click",
async () => {
if (actionsMenu) actionsMenu.hidden = true;
await toggleSelectedPhotos();
}
);
document.getElementById("selectionOtherBtn")?.addEventListener(
"click",
() => {
if (actionsMenu) actionsMenu.hidden = true;
alert("Altre azioni saranno disponibili in seguito.");
}
);
actionsButton?.addEventListener("click", () => {
if (!actionsMenu) return;
if (!actionsMenu.hidden) {
actionsMenu.hidden = true;
actionsMenu.classList.remove("opens-up");
actionsButton.setAttribute("aria-expanded", "false");
return;
}
// Sceglie il lato con più spazio, soprattutto su schermi Android.
actionsMenu.classList.remove("opens-up");
actionsMenu.hidden = false;
const rect = actionsButton.getBoundingClientRect();
const menuHeight = Math.min(
actionsMenu.scrollHeight || 112,
window.innerHeight - 24
);
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
if (spaceBelow < menuHeight + 12 && spaceAbove > spaceBelow) {
actionsMenu.classList.add("opens-up");
}
actionsButton.setAttribute("aria-expanded", "true");
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || !actionsMenu || actionsMenu.hidden) {
return;
}
actionsMenu.hidden = true;
actionsMenu.classList.remove("opens-up");
actionsButton?.setAttribute("aria-expanded", "false");
actionsButton?.focus();
});
document.addEventListener("click", (event) => {
if (actionsMenu &&
!actionsMenu.hidden &&
!event.target.closest("#selectionActionsBtn") &&
!event.target.closest("#selectionActionsMenu")) {
actionsMenu.hidden = true;
actionsMenu.classList.remove("opens-up");
actionsButton?.setAttribute("aria-expanded", "false");
}
});
});
window.GallerySelection = Object.freeze({
bindTile,
clear,
isSelected,
getMode: () => mode,
getItems: () => [...selected.values()],
getCount: () => selected.size
});
})();