document.addEventListener("DOMContentLoaded", () => { document.addEventListener("contextmenu", e => e.preventDefault()); // ========================================================================== // RIFERIMENTI DOM // ========================================================================== const folderEl = document.getElementById("folder"); const contextMenuEl = document.getElementById("context-menu"); // ========================================================================== // STATO GLOBALE // ========================================================================== let appsData = []; let appsOrder = []; let editMode = false; // 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; // ========================================================================== // CARICAMENTO APPS // ========================================================================== function loadOrder() { try { const val = localStorage.getItem("appsOrder"); if (!val) return null; const parsed = JSON.parse(val); return Array.isArray(parsed) ? parsed : null; } catch { return null; } } function saveOrder() { localStorage.setItem("appsOrder", JSON.stringify(appsOrder)); } function renderApps() { 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); }); } async function loadApps() { const apps = await fetch("apps.json").then(r => r.json()); appsData = apps.map((app, i) => ({ id: app.id || `app-${i}`, name: app.name, url: app.url, icon: app.icon })); const stored = loadOrder(); if (stored) { appsOrder = stored.filter(id => appsData.some(a => a.id === id)); appsData.forEach(a => { if (!appsOrder.includes(a.id)) appsOrder.push(a.id); }); } else { appsOrder = appsData.map(a => a.id); } renderApps(); } // ========================================================================== // ZOOM STILE IPHONE (PINCH ELASTICO) // ========================================================================== function computeDynamicMaxZoom() { return Math.min(window.innerWidth / 85, 4.0); } function loadInitialZoom() { const v = parseFloat(localStorage.getItem("zoomLevel")); if (!isFinite(v) || v <= 0) return 1; return Math.min(Math.max(v, 0.5), computeDynamicMaxZoom()); } function applyZoom(z) { zoomLevel = (!isFinite(z) || z <= 0) ? 1 : z; document.documentElement.style.setProperty("--zoom", zoomLevel); localStorage.setItem("zoomLevel", String(zoomLevel)); } function getPinchDistance(touches) { const [a, b] = touches; return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY); } function elasticEase(x) { return Math.sin(x * Math.PI * 0.5) * 1.05; } function initZoomHandlers() { zoomMax = computeDynamicMaxZoom(); zoomLevel = loadInitialZoom(); applyZoom(zoomLevel); document.addEventListener("touchmove", e => { if (e.touches.length === 2) e.preventDefault(); }, { passive: false }); document.addEventListener("touchstart", e => { if (e.touches.length === 2) { initialPinchDistance = getPinchDistance(e.touches); if (zoomAnimFrame) cancelAnimationFrame(zoomAnimFrame); } const now = Date.now(); if (e.touches.length === 1 && now - lastTapTime < 300) { zoomMax = computeDynamicMaxZoom(); applyZoom(Math.min(zoomLevel * 1.15, zoomMax)); } lastTapTime = now; }); 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(); 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 }); 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); } }); } // ========================================================================== // EDIT MODE + MENU CONTESTUALE + WIGGLE // ========================================================================== function enterEditMode() { editMode = true; document.body.classList.add("edit-mode"); } function exitEditMode() { editMode = false; document.body.classList.remove("edit-mode"); hideContextMenu(); } function showContextMenuFor(id, x, y) { contextMenuTargetId = id; contextMenuEl.style.left = `${x}px`; contextMenuEl.style.top = `${y}px`; contextMenuEl.classList.remove("hidden"); } function hideContextMenu() { contextMenuEl.classList.add("hidden"); contextMenuTargetId = null; } // ========================================================================== // LONG PRESS → SOLO ENTRA IN WIGGLE MODE (NON APRE PIÙ IL MENÙ) // ========================================================================== function initLongPressHandlers() { document.addEventListener("touchstart", e => { if (e.touches.length !== 1) return; const touch = e.touches[0]; const icon = touch.target.closest(".app-icon"); // Se premi su un'icona if (icon) { longPressTarget = icon; longPressTimer = setTimeout(() => { // 1️⃣ Se NON siamo in wiggle mode → entra in wiggle mode if (!editMode) { enterEditMode(); if (navigator.vibrate) navigator.vibrate(10); return; } // 2️⃣ Se SIAMO in wiggle mode → apri il menù 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; } // Se premi FUORI dalle icone longPressTimer = setTimeout(() => { if (editMode) exitEditMode(); }, 350); }, { passive: true }); document.addEventListener("touchmove", e => { if (!longPressTimer) return; const touch = e.touches[0]; const dx = touch.clientX - (longPressTarget?.getBoundingClientRect().left ?? touch.clientX); const dy = touch.clientY - (longPressTarget?.getBoundingClientRect().top ?? touch.clientY); if (Math.hypot(dx, dy) > 15) { clearTimeout(longPressTimer); longPressTimer = null; longPressTarget = null; } }, { passive: true }); document.addEventListener("touchend", () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; longPressTarget = null; } }, { passive: true }); } // ========================================================================== // DOUBLE TAP (MOBILE) + DOUBLE CLICK (DESKTOP) → APRE IL MENÙ // ========================================================================== /* function initDoubleTapContextMenu() { let lastTap = 0; // MOBILE double tap folderEl.addEventListener("touchend", e => { if (!editMode) return; const icon = e.target.closest(".app-icon"); if (!icon) return; const now = Date.now(); if (now - lastTap < 300) { const r = icon.getBoundingClientRect(); showContextMenuFor( icon.dataset.id, r.left + r.width / 2, r.top + r.height ); if (navigator.vibrate) navigator.vibrate(10); } lastTap = now; }); // DESKTOP double click folderEl.addEventListener("dblclick", e => { if (!editMode) return; const icon = e.target.closest(".app-icon"); if (!icon) return; const r = icon.getBoundingClientRect(); showContextMenuFor( icon.dataset.id, r.left + r.width / 2, r.top + r.height ); }); } */ // ========================================================================== // DRAG FLUIDO STILE IPHONE CON PLACEHOLDER + FIX "SOTTO IL DITO" // ========================================================================== function startDrag(icon, touch) { draggingId = icon.dataset.id; // 1️⃣ Cattura posizione PRIMA di toccare il DOM const r = icon.getBoundingClientRect(); dragOffsetX = touch.pageX - r.left; dragOffsetY = touch.pageY - r.top; // 2️⃣ Sposta SUBITO l’icona vera fuori dal flusso 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)`; draggingIcon.style.transform = "translate3d(0,0,0)"; // 3️⃣ SOLO ORA crea il placeholder invisibile const placeholder = icon.cloneNode(true); placeholder.classList.add("placeholder"); placeholder.style.visibility = "hidden"; icon.parentNode.insertBefore(placeholder, icon); hideContextMenu(); } function updateDragPosition(touch) { if (!draggingIcon) return; // Mantieni l’icona ESATTAMENTE sotto il dito const x = touch.pageX - dragOffsetX; const y = touch.pageY - dragOffsetY; draggingIcon.style.left = `${x}px`; draggingIcon.style.top = `${y}px`; // Trova icona sotto il dito const elem = document.elementFromPoint(touch.clientX, touch.clientY); const targetIcon = elem && elem.closest(".app-icon:not(.dragging):not(.placeholder)"); if (!targetIcon) return; const from = appsOrder.indexOf(draggingId); const to = appsOrder.indexOf(targetIcon.dataset.id); if (from === -1 || to === -1 || from === to) return; // Riordino appsOrder.splice(from, 1); appsOrder.splice(to, 0, draggingId); saveOrder(); //renderApps(); // Ricrea placeholder nella nuova posizione /*const oldPlaceholder = folderEl.querySelector(".app-icon.placeholder"); if (oldPlaceholder) oldPlaceholder.remove(); const newPos = folderEl.querySelector(`.app-icon[data-id="${draggingId}"]`); if (newPos) { const clone = newPos.cloneNode(true); clone.classList.add("placeholder"); clone.style.visibility = "hidden"; newPos.parentNode.insertBefore(clone, newPos); }*/ } // ========================================================================== // DROP PRECISO NELLA CELLA CORRETTA // ========================================================================== function endDrag() { if (!draggingIcon) return; const icon = draggingIcon; draggingIcon = null; // 1️⃣ Rimuovi placeholder const placeholder = folderEl.querySelector(".app-icon.placeholder"); if (placeholder) placeholder.remove(); // 2️⃣ Calcola la cella target sotto il dito const dropX = parseFloat(icon.style.left) + icon.offsetWidth / 2; const dropY = parseFloat(icon.style.top) + icon.offsetHeight / 2; const elem = document.elementFromPoint(dropX, dropY); const targetIcon = elem && elem.closest(".app-icon:not(.dragging)"); if (targetIcon) { const from = appsOrder.indexOf(icon.dataset.id); const to = appsOrder.indexOf(targetIcon.dataset.id); if (from !== -1 && to !== -1 && from !== to) { appsOrder.splice(from, 1); appsOrder.splice(to, 0, icon.dataset.id); saveOrder(); } } // 3️⃣ Ripristina icona icon.classList.remove("dragging"); icon.style.position = ""; icon.style.left = ""; icon.style.top = ""; icon.style.width = ""; icon.style.height = ""; icon.style.zIndex = ""; icon.style.pointerEvents = ""; icon.style.transform = ""; // 4️⃣ Re-render finale renderApps(); } function initDragHandlers() { document.addEventListener("touchstart", e => { if (!editMode) return; if (e.touches.length !== 1) return; const touch = e.touches[0]; const icon = touch.target.closest(".app-icon"); if (!icon) return; if (contextMenuTargetId) return; dragStartX = touch.clientX; dragStartY = touch.clientY; draggingIcon = null; draggingId = null; }, { passive: true }); document.addEventListener("touchmove", e => { if (!editMode) return; if (e.touches.length !== 1) return; const touch = e.touches[0]; if (!draggingIcon) { const dx = touch.clientX - dragStartX; const dy = touch.clientY - dragStartY; if (Math.hypot(dx, dy) > 10) { const icon = touch.target.closest(".app-icon"); if (icon) startDrag(icon, touch); } } else { updateDragPosition(touch); e.preventDefault(); } }, { passive: false }); document.addEventListener("touchend", e => { if (!editMode) return; if (draggingIcon && e.touches.length === 0) { endDrag(); } }, { passive: true }); } // ========================================================================== // MENU CONTESTUALE: AZIONI // ========================================================================== function initContextMenuActions() { contextMenuEl.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; if (action === "rename") { const nuovoNome = prompt("Nuovo nome app:", app.name); if (nuovoNome && nuovoNome.trim()) { app.name = nuovoNome.trim(); renderApps(); saveOrder(); } } if (action === "change-icon") { const nuovaIcona = prompt("URL nuova icona:", app.icon); if (nuovaIcona && nuovaIcona.trim()) { app.icon = nuovaIcona.trim(); renderApps(); saveOrder(); } } if (action === "remove") { if (confirm("Rimuovere questa app dalla griglia?")) { appsOrder = appsOrder.filter(id => id !== app.id); saveOrder(); renderApps(); } } hideContextMenu(); }); } // ========================================================================== // INIT GLOBALE // ========================================================================== (async function init() { await loadApps(); initZoomHandlers(); initLongPressHandlers(); // long‑press → wiggle mode // initDoubleTapContextMenu(); // double tap / double click → menù initDragHandlers(); initContextMenuActions(); })(); });