diff --git a/app.js b/app.js
index 109599c..2c64de6 100644
--- a/app.js
+++ b/app.js
@@ -1,19 +1,539 @@
-async function loadApps() {
- const container = document.getElementById("folder");
- const apps = await fetch("apps.json").then(r => r.json());
+document.addEventListener("DOMContentLoaded", () => {
+ document.addEventListener("contextmenu", e => e.preventDefault());
+ // ==========================================================================
+ // RIFERIMENTI DOM
+ // ==========================================================================
+ const folderEl = document.getElementById("folder");
+ const contextMenuEl = document.getElementById("context-menu");
- apps.forEach(app => {
- const div = document.createElement("div");
- div.className = "app-icon";
- div.onclick = () => window.open(app.url, "_blank", "noopener,noreferrer");
+ // ==========================================================================
+ // STATO GLOBALE
+ // ==========================================================================
+ let appsData = [];
+ let appsOrder = [];
+ let editMode = false;
- div.innerHTML = `
-
- ${app.name}
- `;
+ // Zoom
+ let zoomLevel;
+ let zoomMax;
+ let initialPinchDistance = null;
+ let lastTapTime = 0;
+ let zoomAnimFrame = null;
- container.appendChild(div);
- });
+ // 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}
+ `;
+
+ 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 });
}
-loadApps();
+ // ==========================================================================
+ // 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();
+ })();
+
+});
diff --git a/index.html b/index.html
index 2b77246..2fc1ee5 100644
--- a/index.html
+++ b/index.html
@@ -2,11 +2,26 @@