490 lines
13 KiB
JavaScript
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
|
|
});
|
|
})();
|