diff --git a/app/app.js b/app/app.js
index e118506..54b8b8f 100644
--- a/app/app.js
+++ b/app/app.js
@@ -34,6 +34,9 @@ let dragStartX = 0;
let dragStartY = 0;
let placeholderEl = null;
+// PATCH MINIMA: flag per evitare che al primo long‑press parta anche il menu
+let justEnteredEditMode = false;
+
// ---------------------------------------------------------------------------
// CRITTOGRAFIA E STORAGE
// ---------------------------------------------------------------------------
@@ -492,11 +495,18 @@ function initLongPressHandlers() {
// Primo long‑press → entra in edit mode
if (!editMode) {
enterEditMode();
+ justEnteredEditMode = true; // PATCH: blocca il menu su questo primo long-press
if (navigator.vibrate) navigator.vibrate(10);
return;
}
- // Se già in edit mode → apri menu contestuale
+ // Se siamo appena entrati in edit mode con questo long-press → NON aprire menu
+ if (justEnteredEditMode) {
+ justEnteredEditMode = false;
+ return;
+ }
+
+ // Se già in edit mode da prima → apri menu contestuale
const r = icon.getBoundingClientRect();
showContextMenuFor(
icon.dataset.id,
@@ -541,6 +551,8 @@ function initLongPressHandlers() {
longPressTimer = null;
longPressTarget = null;
}
+ // PATCH: reset del flag
+ justEnteredEditMode = false;
}, { passive: true });
// ---------------------------------------------------------
@@ -623,6 +635,11 @@ function initGlobalCloseHandlers() {
// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED)
// ============================================================================
+// ============================================================================
+// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
+// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED)
+// ============================================================================
+
// ---------------------------------------------------------------------------
// Utility per ottenere posizione del puntatore (touch + mouse)
// ---------------------------------------------------------------------------
@@ -644,7 +661,7 @@ function getPointerPosition(e) {
}
// ---------------------------------------------------------------------------
-/* Inizio drag: icona flottante + placeholder nel layout */
+// Inizio drag: icona flottante + placeholder nel layout
// ---------------------------------------------------------------------------
function startDrag(icon, pos) {
const folderEl = document.getElementById("folder");
@@ -666,12 +683,11 @@ function startDrag(icon, pos) {
draggingIcon.style.pointerEvents = "none";
draggingIcon.style.transform = "translate3d(0,0,0)";
- // Placeholder nel layout (slot vuoto)
+ // Placeholder nel layout
placeholderEl = document.createElement("div");
placeholderEl.className = "app-icon placeholder";
placeholderEl.style.visibility = "hidden";
- // Inserisci il placeholder dove stava l’icona
folderEl.insertBefore(placeholderEl, icon);
hideContextMenu();
@@ -721,11 +737,9 @@ function endDrag() {
const folderEl = document.getElementById("folder");
- // Tutti i figli, inclusa la placeholder
const children = Array.from(folderEl.children);
const finalIndex = children.indexOf(placeholderEl);
- // Ripristina icona visuale
draggingIcon.classList.remove("dragging");
draggingIcon.style.position = "";
draggingIcon.style.left = "";
@@ -754,7 +768,6 @@ function endDrag() {
dragStartX = 0;
dragStartY = 0;
- // Ridisegna in base al nuovo ordine
renderApps();
}
@@ -870,10 +883,8 @@ function initDragHandlers() {
});
}
-// ============================================================================
-// LAUNCHER — VERSIONE COMPLETA E
-// OTTIMIZZATA (A) BLOCCO 6/6 — Context Menu
-// Actions + Config Save + Init Globale
+// ============================================================================
+// BLOCCO 6/6 — Context Menu Actions + Config Save + Init Globale
// ============================================================================
// ---------------------------------------------------------------------------
@@ -890,9 +901,7 @@ function initContextMenuActions() {
const app = appsData.find(a => a.id === contextMenuTargetId);
if (!app) return;
- // ---------------------------------------------------------
- // RINOMINA APP
- // ---------------------------------------------------------
+ // RINOMINA
if (action === "rename") {
const nuovoNome = prompt("Nuovo nome app:", app.name);
if (nuovoNome && nuovoNome.trim()) {
@@ -902,9 +911,7 @@ function initContextMenuActions() {
}
}
- // ---------------------------------------------------------
- // CAMBIA ICONA
- // ---------------------------------------------------------
+ // CAMBIA ICONA
if (action === "change-icon") {
const nuovaIcona = prompt("URL nuova icona:", app.icon);
if (nuovaIcona && nuovaIcona.trim()) {
@@ -914,9 +921,7 @@ function initContextMenuActions() {
}
}
- // ---------------------------------------------------------
- // RIMUOVI APP DALLA GRIGLIA
- // ---------------------------------------------------------
+ // RIMUOVI
if (action === "remove") {
if (confirm("Rimuovere questa app dalla griglia?")) {
appsOrder = appsOrder.filter(id => id !== app.id);
@@ -944,7 +949,7 @@ document.getElementById("cfg-refresh").addEventListener("click", async () => {
if (ok) {
hideSetupPage();
- startLauncher(); // Torna subito alla schermata principale
+ startLauncher();
} else {
alert("Impossibile aggiornare le app dal server.");
}
diff --git a/app/app.js.old b/app/app.js.old
new file mode 100644
index 0000000..e118506
--- /dev/null
+++ b/app/app.js.old
@@ -0,0 +1,983 @@
+// ============================================================================
+// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
+// BLOCCO 1/6 — Variabili globali + Storage + Config + Setup Page
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// VARIABILI GLOBALI
+// ---------------------------------------------------------------------------
+let URI;
+let USER;
+let PASSW;
+
+let appsData = []; // Lista completa delle app (nome, icona, url)
+let appsOrder = []; // Ordine delle icone nella griglia
+let editMode = false; // Modalità wiggle stile iOS
+
+// Zoom
+let zoomLevel;
+let zoomMax;
+let initialPinchDistance = null;
+let lastTapTime = 0;
+let zoomAnimFrame = null;
+
+// Long‑press / drag
+let longPressTimer = null;
+let longPressTarget = null;
+let contextMenuTargetId = null;
+
+let draggingIcon = null;
+let draggingId = null;
+let dragOffsetX = 0;
+let dragOffsetY = 0;
+let dragStartX = 0;
+let dragStartY = 0;
+let placeholderEl = null;
+
+// ---------------------------------------------------------------------------
+// CRITTOGRAFIA E STORAGE
+// ---------------------------------------------------------------------------
+const SECRET_KEY = "chiave-super-segreta-123";
+
+// Salva configurazione (URL, user, pass)
+function saveConfig(url, user, password) {
+ const data = { url, user, password };
+ URI = url;
+ USER = user;
+ PASSW = password;
+
+ const encrypted = CryptoJS.AES.encrypt(
+ JSON.stringify(data),
+ SECRET_KEY
+ ).toString();
+
+ localStorage.setItem("launcherConfig", encrypted);
+}
+
+// Carica configurazione
+function loadConfig() {
+ const encrypted = localStorage.getItem("launcherConfig");
+ if (!encrypted) return null;
+
+ try {
+ const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY);
+ const obj = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
+
+ URI = obj.url;
+ USER = obj.user;
+ PASSW = obj.password;
+
+ return obj;
+ } catch {
+ return null;
+ }
+}
+
+// Salva apps scaricate dal server
+function saveApps(jsonApps) {
+ const encrypted = CryptoJS.AES.encrypt(
+ JSON.stringify(jsonApps),
+ SECRET_KEY
+ ).toString();
+
+ localStorage.setItem("jsonApps", encrypted);
+}
+
+// Carica apps salvate in locale
+function loadApps() {
+ const encrypted = localStorage.getItem("jsonApps");
+ if (!encrypted) return null;
+
+ try {
+ const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY);
+ return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
+ } catch {
+ return null;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// SETUP PAGE (6 TAP PER APRIRE + AUTOCOMPILAZIONE + "Aggiorna ora")
+// ---------------------------------------------------------------------------
+function showSetupPage() {
+ const cfg = loadConfig();
+
+ if (cfg) {
+ document.getElementById("cfg-url").value = cfg.url;
+ document.getElementById("cfg-user").value = cfg.user;
+ document.getElementById("cfg-pass").value = cfg.password;
+
+ // Mostra il pulsante "Aggiorna ora" solo se esiste già una config
+ document.getElementById("cfg-refresh").style.display = "block";
+ } else {
+ // Nessuna config → nascondi il pulsante
+ document.getElementById("cfg-refresh").style.display = "none";
+ }
+
+ document.getElementById("setup-page").classList.remove("hidden");
+}
+
+function hideSetupPage() {
+ document.getElementById("setup-page").classList.add("hidden");
+}
+
+// 6 tap per aprire la setup page
+let tapCount = 0;
+let tapTimer = null;
+
+document.addEventListener("click", () => {
+ tapCount++;
+
+ if (tapTimer) clearTimeout(tapTimer);
+
+ tapTimer = setTimeout(() => {
+ tapCount = 0;
+ }, 600);
+
+ if (tapCount >= 6) {
+ tapCount = 0;
+ showSetupPage();
+ }
+});
+
+// ============================================================================
+// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
+// BLOCCO 2/6 — API login, getLinks, ordine apps, render, startLauncher
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// LOGIN API
+// ---------------------------------------------------------------------------
+async function login(email, password) {
+ try {
+ const res = await fetch(`${URI}/auth/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password })
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`HTTP ${res.status}: ${text}`);
+ }
+
+ const data = await res.json();
+ return data.token;
+
+ } catch (err) {
+ alert(err);
+ return null;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// GET LINKS (scarica apps dal server + salva + aggiorna ordine)
+// ---------------------------------------------------------------------------
+async function getLinks() {
+ try {
+ const token = await login(USER, PASSW);
+ if (!token) throw new Error("User o Password errati");
+
+ const res = await fetch(`${URI}/links`, {
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Accept": "application/json"
+ }
+ });
+
+ if (!res.ok) throw new Error("Server errato o non risponde");
+
+ const json = await res.json();
+
+ // Normalizza apps
+ appsData = json.map((a, i) => ({
+ id: a.id || `app-${i}`,
+ name: a.name,
+ url: a.url,
+ icon: `${URI}${a.icon}`
+ }));
+
+ // Salva in locale
+ saveApps(appsData);
+
+ // Aggiorna ordine
+ loadAppOrder();
+
+ // Render
+ renderApps();
+
+ return true;
+
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// CARICAMENTO ORDINE APPS
+// ---------------------------------------------------------------------------
+function loadAppOrder() {
+ const stored = localStorage.getItem("appsOrder");
+
+ if (stored) {
+ const parsed = JSON.parse(stored);
+
+ // Mantieni solo ID validi
+ appsOrder = parsed.filter(id => appsData.some(a => a.id === id));
+
+ // Aggiungi eventuali nuove app non presenti nell'ordine salvato
+ appsData.forEach(a => {
+ if (!appsOrder.includes(a.id)) appsOrder.push(a.id);
+ });
+
+ } else {
+ // Primo avvio → ordine naturale
+ appsOrder = appsData.map(a => a.id);
+ }
+}
+
+function saveOrder() {
+ localStorage.setItem("appsOrder", JSON.stringify(appsOrder));
+}
+
+// ---------------------------------------------------------------------------
+// RENDER DELLA GRIGLIA
+// ---------------------------------------------------------------------------
+function renderApps() {
+ const folderEl = document.getElementById("folder");
+ folderEl.innerHTML = "";
+
+ appsOrder.forEach(id => {
+ const app = appsData.find(a => a.id === id);
+ if (!app) return;
+
+ const div = document.createElement("div");
+ div.className = "app-icon";
+ div.dataset.id = app.id;
+
+ div.innerHTML = `
+
+ ${app.name}
+ `;
+
+ div.addEventListener("click", () => {
+ if (!editMode) window.open(app.url, "_blank", "noopener");
+ });
+
+ folderEl.appendChild(div);
+ });
+}
+
+// ---------------------------------------------------------------------------
+// START LAUNCHER (carica locale → render → init UI)
+// ---------------------------------------------------------------------------
+async function startLauncher() {
+
+ // 1️⃣ Carica apps salvate in locale
+ const saved = loadApps();
+ if (saved) {
+ appsData = saved;
+ console.log("Apps caricate da localStorage:", appsData);
+ }
+
+ // 2️⃣ Carica ordine
+ loadAppOrder();
+
+ // 3️⃣ Render immediato (istantaneo)
+ renderApps();
+
+ // ❌ Nessun aggiornamento automatico dal server
+ // getLinks();
+
+ // 4️⃣ Inizializza UI (zoom, drag, wiggle, menu…)
+ initZoomHandlers();
+ initLongPressHandlers();
+ initDragHandlers();
+ initContextMenuActions();
+ initGlobalCloseHandlers();
+}
+
+// ============================================================================
+// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
+// BLOCCO 3/6 — Zoom stile iPhone (pinch, elasticità, wheel)
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// Calcolo dinamico dello zoom massimo
+// ---------------------------------------------------------------------------
+function computeDynamicMaxZoom() {
+ return Math.min(window.innerWidth / 85, 4.0);
+}
+
+// ---------------------------------------------------------------------------
+// Carica lo zoom salvato in locale
+// ---------------------------------------------------------------------------
+function loadInitialZoom() {
+ const v = parseFloat(localStorage.getItem("zoomLevel"));
+ if (!isFinite(v) || v <= 0) return 1;
+ return Math.min(Math.max(v, 0.5), computeDynamicMaxZoom());
+}
+
+// ---------------------------------------------------------------------------
+// Applica lo zoom (aggiorna CSS + salva in locale)
+// ---------------------------------------------------------------------------
+function applyZoom(z) {
+ zoomLevel = (!isFinite(z) || z <= 0) ? 1 : z;
+ document.documentElement.style.setProperty("--zoom", zoomLevel);
+ localStorage.setItem("zoomLevel", String(zoomLevel));
+}
+
+// ---------------------------------------------------------------------------
+// Distanza tra due dita (pinch)
+// ---------------------------------------------------------------------------
+function getPinchDistance(touches) {
+ const [a, b] = touches;
+ return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
+}
+
+// ---------------------------------------------------------------------------
+// Elasticità ai limiti (effetto iOS)
+// ---------------------------------------------------------------------------
+function elasticEase(x) {
+ return Math.sin(x * Math.PI * 0.5) * 1.05;
+}
+
+// ---------------------------------------------------------------------------
+// Inizializzazione completa dello zoom
+// ---------------------------------------------------------------------------
+function initZoomHandlers() {
+ zoomMax = computeDynamicMaxZoom();
+ zoomLevel = loadInitialZoom();
+ applyZoom(zoomLevel);
+
+ // ---------------------------------------------------------
+ // PINCH SU MOBILE
+ // ---------------------------------------------------------
+
+ // Previeni scroll durante pinch
+ document.addEventListener("touchmove", e => {
+ if (e.touches.length === 2) e.preventDefault();
+ }, { passive: false });
+
+ // Inizio pinch (NO double tap zoom)
+ document.addEventListener("touchstart", e => {
+
+ // Inizio pinch
+ if (e.touches.length === 2) {
+ initialPinchDistance = getPinchDistance(e.touches);
+ if (zoomAnimFrame) cancelAnimationFrame(zoomAnimFrame);
+ }
+
+ // Nessuna azione sul doppio tap
+ lastTapTime = Date.now();
+ });
+
+ // Pinch in corso
+ document.addEventListener("touchmove", e => {
+ if (e.touches.length === 2 && initialPinchDistance) {
+ const newDist = getPinchDistance(e.touches);
+ const scale = newDist / initialPinchDistance;
+
+ let newZoom = zoomLevel * scale;
+ zoomMax = computeDynamicMaxZoom();
+
+ // Elasticità ai limiti
+ if (newZoom > zoomMax) newZoom = zoomMax + (newZoom - zoomMax) * 0.25;
+ if (newZoom < 0.5) newZoom = 0.5 - (0.5 - newZoom) * 0.25;
+
+ applyZoom(newZoom);
+ initialPinchDistance = newDist;
+ e.preventDefault();
+ }
+ }, { passive: false });
+
+ // Fine pinch → animazione elastica verso limite
+ document.addEventListener("touchend", e => {
+ if (e.touches.length < 2 && initialPinchDistance) {
+ initialPinchDistance = null;
+
+ zoomMax = computeDynamicMaxZoom();
+ const target = Math.min(Math.max(zoomLevel, 0.5), zoomMax);
+ const start = zoomLevel;
+ const duration = 250;
+ const startTime = performance.now();
+
+ function animate(t) {
+ const p = Math.min((t - startTime) / duration, 1);
+ const eased = start + (target - start) * elasticEase(p);
+ applyZoom(eased);
+ if (p < 1) zoomAnimFrame = requestAnimationFrame(animate);
+ }
+
+ zoomAnimFrame = requestAnimationFrame(animate);
+ }
+ });
+
+ // ---------------------------------------------------------
+ // ZOOM CON WHEEL SU DESKTOP
+ // ---------------------------------------------------------
+ document.addEventListener("wheel", e => {
+ e.preventDefault();
+
+ zoomMax = computeDynamicMaxZoom();
+ const direction = e.deltaY < 0 ? 1 : -1;
+ const factor = 1 + direction * 0.1;
+
+ let newZoom = zoomLevel * factor;
+
+ // Elasticità
+ if (newZoom > zoomMax) newZoom = zoomMax + (newZoom - zoomMax) * 0.25;
+ if (newZoom < 0.5) newZoom = 0.5 - (0.5 - newZoom) * 0.25;
+
+ applyZoom(newZoom);
+ }, { passive: false });
+}
+
+// ============================================================================
+// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
+// BLOCCO 4/6 — Long‑press, Edit Mode, Context Menu, Global Close
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// EDIT MODE (wiggle stile iOS)
+// ---------------------------------------------------------------------------
+function enterEditMode() {
+ editMode = true;
+ document.body.classList.add("edit-mode");
+}
+
+function exitEditMode() {
+ editMode = false;
+ document.body.classList.remove("edit-mode");
+ hideContextMenu();
+}
+
+// ---------------------------------------------------------------------------
+// MENU CONTESTUALE
+// ---------------------------------------------------------------------------
+function showContextMenuFor(id, x, y) {
+ contextMenuTargetId = id;
+ const menu = document.getElementById("context-menu");
+ menu.style.left = `${x}px`;
+ menu.style.top = `${y}px`;
+ menu.classList.remove("hidden");
+}
+
+function hideContextMenu() {
+ const menu = document.getElementById("context-menu");
+ menu.classList.add("hidden");
+ contextMenuTargetId = null;
+}
+
+// ---------------------------------------------------------------------------
+// LONG PRESS HANDLERS (TOUCH + MOUSE)
+// ---------------------------------------------------------------------------
+function initLongPressHandlers() {
+
+ // ---------------------------------------------------------
+ // TOUCH LONG PRESS
+ // ---------------------------------------------------------
+ document.addEventListener("touchstart", e => {
+ if (e.touches.length !== 1) return;
+
+ const touch = e.touches[0];
+ const icon = touch.target.closest(".app-icon");
+
+ if (icon) {
+ longPressTarget = icon;
+
+ longPressTimer = setTimeout(() => {
+
+ // Primo long‑press → entra in edit mode
+ if (!editMode) {
+ enterEditMode();
+ if (navigator.vibrate) navigator.vibrate(10);
+ return;
+ }
+
+ // Se già in edit mode → apri menu contestuale
+ const r = icon.getBoundingClientRect();
+ showContextMenuFor(
+ icon.dataset.id,
+ r.left + r.width / 2,
+ r.top + r.height
+ );
+ if (navigator.vibrate) navigator.vibrate(10);
+
+ }, 350);
+
+ return;
+ }
+
+ // Long press fuori dalle icone → esci da edit mode
+ longPressTimer = setTimeout(() => {
+ if (editMode) exitEditMode();
+ }, 350);
+
+ }, { passive: true });
+
+ // Cancella long‑press se l’utente si muove troppo
+ document.addEventListener("touchmove", e => {
+ if (!longPressTimer) return;
+
+ const touch = e.touches[0];
+ const r = longPressTarget?.getBoundingClientRect();
+
+ const dx = touch.clientX - (r?.left ?? touch.clientX);
+ const dy = touch.clientY - (r?.top ?? touch.clientY);
+
+ if (Math.hypot(dx, dy) > 15) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ longPressTarget = null;
+ }
+ }, { passive: true });
+
+ // Fine touch → cancella long‑press
+ document.addEventListener("touchend", () => {
+ if (longPressTimer) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ longPressTarget = null;
+ }
+ }, { passive: true });
+
+ // ---------------------------------------------------------
+ // MOUSE LONG PRESS
+ // ---------------------------------------------------------
+ document.addEventListener("mousedown", e => {
+ if (e.button !== 0) return;
+
+ const icon = e.target.closest(".app-icon");
+ longPressTarget = icon ?? null;
+
+ longPressTimer = setTimeout(() => {
+
+ if (!editMode) {
+ enterEditMode();
+ return;
+ }
+
+ if (icon) {
+ const r = icon.getBoundingClientRect();
+ showContextMenuFor(
+ icon.dataset.id,
+ r.left + r.width / 2,
+ r.top + r.height
+ );
+ }
+
+ }, 350);
+ });
+
+ // Cancella long‑press se il mouse si muove troppo
+ document.addEventListener("mousemove", e => {
+ if (!longPressTimer) return;
+
+ if (longPressTarget) {
+ const r = longPressTarget.getBoundingClientRect();
+ const dx = e.clientX - (r.left + r.width / 2);
+ const dy = e.clientY - (r.top + r.height / 2);
+
+ if (Math.hypot(dx, dy) > 15) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ longPressTarget = null;
+ }
+ }
+ });
+
+ // Mouse up → cancella long‑press
+ document.addEventListener("mouseup", () => {
+ if (longPressTimer) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ longPressTarget = null;
+ }
+ });
+}
+
+// ---------------------------------------------------------------------------
+// CHIUSURA MENU E USCITA DA EDIT MODE
+// ---------------------------------------------------------------------------
+function initGlobalCloseHandlers() {
+ document.addEventListener("pointerdown", e => {
+ const isIcon = e.target.closest(".app-icon");
+ const isMenu = e.target.closest("#context-menu");
+
+ // 1️⃣ Clic fuori dal menu → chiudi menu
+ if (!isMenu && !isIcon && !document.getElementById("context-menu").classList.contains("hidden")) {
+ hideContextMenu();
+ }
+
+ // 2️⃣ Clic fuori dalle icone → esci da edit mode
+ if (!isIcon && editMode) {
+ exitEditMode();
+ }
+ });
+}
+
+// ============================================================================
+// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
+// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED)
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// Utility per ottenere posizione del puntatore (touch + mouse)
+// ---------------------------------------------------------------------------
+function getPointerPosition(e) {
+ if (e.touches && e.touches.length > 0) {
+ return {
+ pageX: e.touches[0].pageX,
+ pageY: e.touches[0].pageY,
+ clientX: e.touches[0].clientX,
+ clientY: e.touches[0].clientY
+ };
+ }
+ return {
+ pageX: e.pageX,
+ pageY: e.pageY,
+ clientX: e.clientX,
+ clientY: e.clientY
+ };
+}
+
+// ---------------------------------------------------------------------------
+/* Inizio drag: icona flottante + placeholder nel layout */
+// ---------------------------------------------------------------------------
+function startDrag(icon, pos) {
+ const folderEl = document.getElementById("folder");
+
+ draggingId = icon.dataset.id;
+
+ const r = icon.getBoundingClientRect();
+ dragOffsetX = pos.pageX - r.left;
+ dragOffsetY = pos.pageY - r.top;
+
+ draggingIcon = icon;
+ draggingIcon.classList.add("dragging");
+ draggingIcon.style.position = "fixed";
+ draggingIcon.style.left = `${r.left}px`;
+ draggingIcon.style.top = `${r.top}px`;
+ draggingIcon.style.width = `${r.width}px`;
+ draggingIcon.style.height = `${r.height}px`;
+ draggingIcon.style.zIndex = "1000";
+ draggingIcon.style.pointerEvents = "none";
+ draggingIcon.style.transform = "translate3d(0,0,0)";
+
+ // Placeholder nel layout (slot vuoto)
+ placeholderEl = document.createElement("div");
+ placeholderEl.className = "app-icon placeholder";
+ placeholderEl.style.visibility = "hidden";
+
+ // Inserisci il placeholder dove stava l’icona
+ folderEl.insertBefore(placeholderEl, icon);
+
+ hideContextMenu();
+}
+
+// ---------------------------------------------------------------------------
+// Aggiorna posizione icona trascinata + posizione placeholder
+// ---------------------------------------------------------------------------
+function updateDragPosition(pos) {
+ if (!draggingIcon || !placeholderEl) return;
+
+ const x = pos.pageX - dragOffsetX;
+ const y = pos.pageY - dragOffsetY;
+
+ draggingIcon.style.left = `${x}px`;
+ draggingIcon.style.top = `${y}px`;
+
+ const centerX = pos.clientX;
+ const centerY = pos.clientY;
+
+ const elem = document.elementFromPoint(centerX, centerY);
+ const targetIcon = elem && elem.closest(".app-icon:not(.dragging)");
+ if (!targetIcon || targetIcon === placeholderEl) return;
+
+ const folderEl = document.getElementById("folder");
+ const targetRect = targetIcon.getBoundingClientRect();
+ const isBefore = centerY < targetRect.top + targetRect.height / 2;
+
+ if (isBefore) {
+ folderEl.insertBefore(placeholderEl, targetIcon);
+ } else {
+ folderEl.insertBefore(placeholderEl, targetIcon.nextSibling);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Fine drag: aggiorna appsOrder in base alla posizione del placeholder
+// ---------------------------------------------------------------------------
+function endDrag() {
+ if (!draggingIcon || !placeholderEl) {
+ draggingIcon = null;
+ placeholderEl = null;
+ dragStartX = 0;
+ dragStartY = 0;
+ return;
+ }
+
+ const folderEl = document.getElementById("folder");
+
+ // Tutti i figli, inclusa la placeholder
+ const children = Array.from(folderEl.children);
+ const finalIndex = children.indexOf(placeholderEl);
+
+ // Ripristina icona visuale
+ draggingIcon.classList.remove("dragging");
+ draggingIcon.style.position = "";
+ draggingIcon.style.left = "";
+ draggingIcon.style.top = "";
+ draggingIcon.style.width = "";
+ draggingIcon.style.height = "";
+ draggingIcon.style.zIndex = "";
+ draggingIcon.style.pointerEvents = "";
+ draggingIcon.style.transform = "";
+
+ if (finalIndex !== -1) {
+ const currentIndex = appsOrder.indexOf(draggingId);
+ if (currentIndex !== -1 && currentIndex !== finalIndex) {
+ appsOrder.splice(currentIndex, 1);
+ appsOrder.splice(finalIndex, 0, draggingId);
+ saveOrder();
+ }
+ }
+
+ if (placeholderEl && placeholderEl.parentNode) {
+ placeholderEl.parentNode.removeChild(placeholderEl);
+ }
+
+ draggingIcon = null;
+ placeholderEl = null;
+ dragStartX = 0;
+ dragStartY = 0;
+
+ // Ridisegna in base al nuovo ordine
+ renderApps();
+}
+
+// ---------------------------------------------------------------------------
+// Inizializzazione Drag & Drop (touch + mouse)
+// ---------------------------------------------------------------------------
+function initDragHandlers() {
+
+ // TOUCH DRAG
+ document.addEventListener("touchstart", e => {
+ if (!editMode) return;
+ if (e.touches.length !== 1) return;
+ if (contextMenuTargetId) return;
+
+ const pos = getPointerPosition(e);
+ const icon = e.touches[0].target.closest(".app-icon");
+ if (!icon) return;
+
+ dragStartX = pos.clientX;
+ dragStartY = pos.clientY;
+ draggingIcon = null;
+ draggingId = null;
+ }, { passive: true });
+
+ document.addEventListener("touchmove", e => {
+ if (!editMode) return;
+ if (e.touches.length !== 1) return;
+
+ const pos = getPointerPosition(e);
+
+ if (!draggingIcon) {
+ const dx = pos.clientX - dragStartX;
+ const dy = pos.clientY - dragStartY;
+ if (Math.hypot(dx, dy) > 10) {
+ const icon = e.touches[0].target.closest(".app-icon");
+ if (icon) {
+ if (longPressTimer) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ longPressTarget = null;
+ }
+ startDrag(icon, pos);
+ }
+ }
+ } else {
+ updateDragPosition(pos);
+ e.preventDefault();
+ }
+ }, { passive: false });
+
+ document.addEventListener("touchend", e => {
+ if (!editMode) return;
+ if (!draggingIcon) {
+ dragStartX = 0;
+ dragStartY = 0;
+ return;
+ }
+ if (!e.touches || e.touches.length === 0) {
+ endDrag();
+ }
+ }, { passive: true });
+
+ // MOUSE DRAG
+ document.addEventListener("mousedown", e => {
+ if (!editMode) return;
+ if (e.button !== 0) return;
+ if (contextMenuTargetId) return;
+
+ const icon = e.target.closest(".app-icon");
+ if (!icon) return;
+
+ const pos = getPointerPosition(e);
+ dragStartX = pos.clientX;
+ dragStartY = pos.clientY;
+ draggingIcon = null;
+ draggingId = null;
+ });
+
+ document.addEventListener("mousemove", e => {
+ if (!editMode) return;
+
+ const pos = getPointerPosition(e);
+
+ if (!draggingIcon) {
+ if (!dragStartX && !dragStartY) return;
+
+ const dx = pos.clientX - dragStartX;
+ const dy = pos.clientY - dragStartY;
+ if (Math.hypot(dx, dy) > 10) {
+ const icon = e.target.closest(".app-icon");
+ if (icon) {
+ if (longPressTimer) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ longPressTarget = null;
+ }
+ startDrag(icon, pos);
+ }
+ }
+ } else {
+ updateDragPosition(pos);
+ }
+ });
+
+ document.addEventListener("mouseup", () => {
+ if (!editMode) return;
+ if (!draggingIcon) {
+ dragStartX = 0;
+ dragStartY = 0;
+ return;
+ }
+ endDrag();
+ });
+}
+
+// ============================================================================
+// LAUNCHER — VERSIONE COMPLETA E
+// OTTIMIZZATA (A) BLOCCO 6/6 — Context Menu
+// Actions + Config Save + Init Globale
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// MENU CONTESTUALE — AZIONI (rename, change icon, remove)
+// ---------------------------------------------------------------------------
+function initContextMenuActions() {
+ const menu = document.getElementById("context-menu");
+
+ menu.addEventListener("click", e => {
+ const btn = e.target.closest("button");
+ if (!btn || !contextMenuTargetId) return;
+
+ const action = btn.dataset.action;
+ const app = appsData.find(a => a.id === contextMenuTargetId);
+ if (!app) return;
+
+ // ---------------------------------------------------------
+ // RINOMINA APP
+ // ---------------------------------------------------------
+ if (action === "rename") {
+ const nuovoNome = prompt("Nuovo nome app:", app.name);
+ if (nuovoNome && nuovoNome.trim()) {
+ app.name = nuovoNome.trim();
+ renderApps();
+ saveOrder();
+ }
+ }
+
+ // ---------------------------------------------------------
+ // CAMBIA ICONA
+ // ---------------------------------------------------------
+ if (action === "change-icon") {
+ const nuovaIcona = prompt("URL nuova icona:", app.icon);
+ if (nuovaIcona && nuovaIcona.trim()) {
+ app.icon = nuovaIcona.trim();
+ renderApps();
+ saveOrder();
+ }
+ }
+
+ // ---------------------------------------------------------
+ // RIMUOVI APP DALLA GRIGLIA
+ // ---------------------------------------------------------
+ if (action === "remove") {
+ if (confirm("Rimuovere questa app dalla griglia?")) {
+ appsOrder = appsOrder.filter(id => id !== app.id);
+ saveOrder();
+ renderApps();
+ }
+ }
+
+ hideContextMenu();
+ });
+}
+
+// ---------------------------------------------------------------------------
+// AGGIORNA ORA — aggiorna apps dal server senza cambiare config
+// ---------------------------------------------------------------------------
+document.getElementById("cfg-refresh").addEventListener("click", async () => {
+
+ const cfg = loadConfig();
+ if (!cfg) {
+ alert("Config mancante. Inserisci URL, user e password.");
+ return;
+ }
+
+ const ok = await getLinks();
+
+ if (ok) {
+ hideSetupPage();
+ startLauncher(); // Torna subito alla schermata principale
+ } else {
+ alert("Impossibile aggiornare le app dal server.");
+ }
+});
+
+// ---------------------------------------------------------------------------
+// SALVATAGGIO CONFIG + RESTART COMPLETO
+// ---------------------------------------------------------------------------
+document.getElementById("cfg-save").addEventListener("click", async () => {
+ const url = document.getElementById("cfg-url").value;
+ const user = document.getElementById("cfg-user").value;
+ const pass = document.getElementById("cfg-pass").value;
+
+ saveConfig(url, user, pass);
+
+ const ok = await getLinks();
+
+ if (ok) {
+ hideSetupPage();
+ startLauncher();
+ }
+});
+
+// ---------------------------------------------------------------------------
+// INIT GLOBALE — DOMContentLoaded
+// ---------------------------------------------------------------------------
+document.addEventListener("DOMContentLoaded", () => {
+ const cfg = loadConfig();
+
+ if (!cfg) {
+ showSetupPage();
+ } else {
+ hideSetupPage();
+ startLauncher();
+ }
+});