// 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 }); })();