384 lines
11 KiB
JavaScript
384 lines
11 KiB
JavaScript
(() => {
|
||
"use strict";
|
||
|
||
const button = document.getElementById("uploadPhotoBtn");
|
||
|
||
if (!button) {
|
||
console.warn("[photoUpload] Pulsante upload non trovato");
|
||
return;
|
||
}
|
||
|
||
let uploading = false;
|
||
|
||
function authHeaders() {
|
||
const token = localStorage.getItem("token");
|
||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||
}
|
||
|
||
async function readResponse(response) {
|
||
const contentType = response.headers.get("content-type") || "";
|
||
|
||
if (contentType.includes("application/json")) {
|
||
return response.json();
|
||
}
|
||
|
||
const text = await response.text();
|
||
throw new Error(text || `Errore HTTP ${response.status}`);
|
||
}
|
||
|
||
async function api(url, options = {}) {
|
||
const response = await fetch(url, {
|
||
credentials: "include",
|
||
...options,
|
||
headers: {
|
||
Accept: "application/json",
|
||
...authHeaders(),
|
||
...(options.headers || {}),
|
||
},
|
||
});
|
||
|
||
const result = await readResponse(response);
|
||
|
||
if (!response.ok || !result.ok) {
|
||
throw new Error(result.error || `Errore HTTP ${response.status}`);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function validFolder(value) {
|
||
return Boolean(value) &&
|
||
value !== "." &&
|
||
value !== ".." &&
|
||
!value.includes("/") &&
|
||
!value.includes("\\") &&
|
||
!value.includes("\0");
|
||
}
|
||
|
||
function getStatus() {
|
||
let status = document.getElementById("photoUploadClientStatus");
|
||
|
||
if (!status) {
|
||
status = document.createElement("span");
|
||
status.id = "photoUploadClientStatus";
|
||
status.setAttribute("role", "status");
|
||
status.setAttribute("aria-live", "polite");
|
||
status.style.display = "none";
|
||
document.body.appendChild(status);
|
||
}
|
||
|
||
status.style.display = "none";
|
||
return status;
|
||
}
|
||
|
||
function chooseFile() {
|
||
return new Promise((resolve) => {
|
||
const input = document.createElement("input");
|
||
input.type = "file";
|
||
input.accept = "image/*";
|
||
input.multiple = true;
|
||
input.style.display = "none";
|
||
document.body.appendChild(input);
|
||
|
||
let finished = false;
|
||
|
||
function finish(files) {
|
||
if (finished) return;
|
||
finished = true;
|
||
input.remove();
|
||
window.removeEventListener("focus", onFocus);
|
||
resolve(files);
|
||
}
|
||
|
||
function onFocus() {
|
||
setTimeout(() => {
|
||
if (!input.files?.length) finish([]);
|
||
}, 500);
|
||
}
|
||
|
||
input.addEventListener("change", () => {
|
||
finish(Array.from(input.files || []));
|
||
}, { once: true });
|
||
|
||
window.addEventListener("focus", onFocus, { once: true });
|
||
input.click();
|
||
});
|
||
}
|
||
|
||
function createDialog() {
|
||
const dialog = document.createElement("dialog");
|
||
dialog.setAttribute("aria-labelledby", "photoUploadTitle");
|
||
dialog.style.cssText = [
|
||
"width:min(92vw,420px)",
|
||
"max-width:420px",
|
||
"border:0",
|
||
"border-radius:12px",
|
||
"padding:22px",
|
||
"box-shadow:0 8px 36px #0004",
|
||
"color:inherit",
|
||
].join(";");
|
||
|
||
dialog.innerHTML = `
|
||
<form id="photoUploadForm">
|
||
<h2 id="photoUploadTitle" style="margin:0 0 18px;font-size:1.2rem">
|
||
Carica una foto
|
||
</h2>
|
||
|
||
<label for="photoUploadUser" style="display:block;margin-bottom:6px">
|
||
Utente
|
||
</label>
|
||
<input id="photoUploadUser" type="text" readonly
|
||
style="box-sizing:border-box;width:100%;padding:10px;margin-bottom:16px">
|
||
|
||
<label for="photoUploadFolder" style="display:block;margin-bottom:6px">
|
||
Cartella di destinazione
|
||
</label>
|
||
<select id="photoUploadFolder" required
|
||
style="box-sizing:border-box;width:100%;padding:10px;margin-bottom:12px">
|
||
</select>
|
||
|
||
<div id="photoUploadNewFolderBox" hidden>
|
||
<label for="photoUploadNewFolder" style="display:block;margin-bottom:6px">
|
||
Nome della nuova cartella
|
||
</label>
|
||
<input id="photoUploadNewFolder" type="text"
|
||
maxlength="200" autocomplete="off"
|
||
placeholder="Inserisci il nome"
|
||
style="box-sizing:border-box;width:100%;padding:10px;margin-bottom:12px">
|
||
</div>
|
||
|
||
<p id="photoUploadDialogError" role="alert"
|
||
style="color:#c62828;margin:4px 0 12px"></p>
|
||
|
||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:12px">
|
||
<button type="button" id="photoUploadCancel">Annulla</button>
|
||
<button type="submit" id="photoUploadContinue">Scegli foto</button>
|
||
</div>
|
||
</form>
|
||
`;
|
||
|
||
document.body.appendChild(dialog);
|
||
|
||
const form = dialog.querySelector("#photoUploadForm");
|
||
const select = dialog.querySelector("#photoUploadFolder");
|
||
const newBox = dialog.querySelector("#photoUploadNewFolderBox");
|
||
const newInput = dialog.querySelector("#photoUploadNewFolder");
|
||
const error = dialog.querySelector("#photoUploadDialogError");
|
||
|
||
select.addEventListener("change", () => {
|
||
const isNew = select.value === "__new__";
|
||
newBox.hidden = !isNew;
|
||
newInput.required = isNew;
|
||
error.textContent = "";
|
||
|
||
if (isNew) newInput.focus();
|
||
});
|
||
|
||
dialog.querySelector("#photoUploadCancel").addEventListener("click", () => {
|
||
dialog.close("cancel");
|
||
});
|
||
|
||
dialog.addEventListener("click", (event) => {
|
||
if (event.target === dialog) dialog.close("cancel");
|
||
});
|
||
|
||
return {
|
||
dialog,
|
||
form,
|
||
select,
|
||
newBox,
|
||
newInput,
|
||
error,
|
||
userInput: dialog.querySelector("#photoUploadUser"),
|
||
continueButton: dialog.querySelector("#photoUploadContinue"),
|
||
};
|
||
}
|
||
|
||
async function openUploadDialog() {
|
||
const targetsResult = await api("/photos/upload-targets");
|
||
|
||
// La destinazione è sempre l'utente autenticato, non un device.
|
||
const target = targetsResult.targets?.find(
|
||
(item) => item.type === "user" &&
|
||
String(item.value).toLowerCase() ===
|
||
String(targetsResult.user).toLowerCase()
|
||
);
|
||
|
||
if (!target) {
|
||
throw new Error("Non è disponibile una destinazione personale autorizzata.");
|
||
}
|
||
|
||
const foldersResult = await api(
|
||
`/photos/upload-folders?source=${encodeURIComponent(target.value)}`
|
||
);
|
||
|
||
const ui = createDialog();
|
||
ui.userInput.value = targetsResult.user;
|
||
|
||
const newOption = document.createElement("option");
|
||
newOption.value = "__new__";
|
||
newOption.textContent = "+ Nuova cartella…";
|
||
ui.select.appendChild(newOption);
|
||
|
||
for (const folder of foldersResult.folders || []) {
|
||
const option = document.createElement("option");
|
||
option.value = folder;
|
||
option.textContent = folder;
|
||
ui.select.appendChild(option);
|
||
}
|
||
|
||
// Seleziona inizialmente la prima cartella esistente, se disponibile.
|
||
// La voce per creare una cartella resta sempre in cima.
|
||
if (foldersResult.folders?.length) {
|
||
ui.select.value = foldersResult.folders[0];
|
||
} else {
|
||
ui.select.value = "__new__";
|
||
ui.newBox.hidden = false;
|
||
ui.newInput.required = true;
|
||
}
|
||
|
||
ui.dialog.showModal();
|
||
|
||
return new Promise((resolve, reject) => {
|
||
let finished = false;
|
||
|
||
function finish(value) {
|
||
if (finished) return;
|
||
finished = true;
|
||
ui.dialog.close();
|
||
ui.dialog.remove();
|
||
resolve(value);
|
||
}
|
||
|
||
ui.dialog.addEventListener("close", () => {
|
||
if (!finished) finish(null);
|
||
}, { once: true });
|
||
|
||
ui.form.addEventListener("submit", (event) => {
|
||
event.preventDefault();
|
||
ui.error.textContent = "";
|
||
|
||
let folder = ui.select.value;
|
||
|
||
if (folder === "__new__") {
|
||
folder = ui.newInput.value.trim();
|
||
|
||
if (!validFolder(folder)) {
|
||
ui.error.textContent = "Inserisci un nome di cartella valido, senza / o \\.";
|
||
ui.newInput.focus();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!folder) {
|
||
ui.error.textContent = "Seleziona una cartella.";
|
||
return;
|
||
}
|
||
|
||
finish({ source: target.value, folder });
|
||
});
|
||
|
||
// Il reject resta disponibile per eventuali errori futuri nel dialogo.
|
||
void reject;
|
||
});
|
||
}
|
||
|
||
button.addEventListener("click", async () => {
|
||
if (uploading) return;
|
||
|
||
const status = getStatus();
|
||
const setStatus = (message) => {
|
||
status.textContent = message;
|
||
|
||
if (window.operationBanner) {
|
||
const lower = String(message).toLowerCase();
|
||
const error = lower.startsWith("errore:");
|
||
const done = lower.includes("upload completato");
|
||
const busy = !error && !done &&
|
||
(lower.includes("carico") || lower.includes("caricamento"));
|
||
|
||
window.operationBanner.show(message, {
|
||
type: error ? "error" : done ? "success" : "upload",
|
||
busy,
|
||
sticky: busy
|
||
});
|
||
}
|
||
};
|
||
|
||
uploading = true;
|
||
button.disabled = true;
|
||
|
||
try {
|
||
setStatus("Carico le cartelle...");
|
||
const destination = await openUploadDialog();
|
||
|
||
if (!destination) {
|
||
setStatus("Upload annullato.");
|
||
return;
|
||
}
|
||
|
||
setStatus("Seleziona una o più foto...");
|
||
const files = await chooseFile();
|
||
|
||
if (!files.length) {
|
||
setStatus("Nessuna foto selezionata.");
|
||
return;
|
||
}
|
||
|
||
const invalidFile = files.find(
|
||
file => file.type && !file.type.startsWith("image/")
|
||
);
|
||
|
||
if (invalidFile) {
|
||
throw new Error(`Il file "${invalidFile.name}" non è un'immagine.`);
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append("source", destination.source);
|
||
formData.append("folder", destination.folder);
|
||
|
||
for (const file of files) {
|
||
formData.append("photo", file, file.name);
|
||
}
|
||
|
||
setStatus(`Caricamento di ${files.length} foto...`);
|
||
|
||
const result = await api("/photos/upload", {
|
||
method: "POST",
|
||
body: formData,
|
||
});
|
||
|
||
const uploadedCount = Array.isArray(result.files)
|
||
? result.files.length
|
||
: files.length;
|
||
|
||
setStatus(
|
||
`${uploadedCount} foto caricate. Attendo l'indicizzazione...`
|
||
);
|
||
|
||
// Mantiene invariato il comportamento precedente della galleria.
|
||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||
|
||
if (typeof window.refreshGallery === "function") {
|
||
window.refreshGallery();
|
||
}
|
||
|
||
setStatus(
|
||
"Upload completato. L'indicizzazione potrebbe richiedere qualche istante."
|
||
);
|
||
|
||
if (window.operationBanner) {
|
||
window.operationBanner.show(
|
||
"Upload completato. L'indicizzazione potrebbe richiedere qualche istante.",
|
||
{ type: "success", busy: false, sticky: false, duration: 3500 }
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("[photoUpload] Errore:", error);
|
||
setStatus(`Errore: ${error.message || "Upload non riuscito"}`);
|
||
} finally {
|
||
uploading = false;
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
})();
|