From 44412bc035594be26e7bc8c53111a88139aec48e Mon Sep 17 00:00:00 2001 From: Fabio Date: Sun, 4 Jan 2026 23:56:42 +0100 Subject: [PATCH] 8th risolto bug menu --- app/app.js | 47 +-- app/app.js.old | 983 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1009 insertions(+), 21 deletions(-) create mode 100644 app/app.js.old 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} + ${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(); + } +});