//const URI = "https://my.patachina2.casacam.net"; //const USER = "fabio.micheluz@gmail.com"; //const PASSW = "master66"; // ========================================================================== // Salvataggio dati // ========================================================================== const SECRET_KEY = "chiave-super-segreta-123"; // puoi cambiarla function saveConfig(url, user, password) { const data = { url, user, password }; const encrypted = CryptoJS.AES.encrypt( JSON.stringify(data), SECRET_KEY ).toString(); localStorage.setItem("launcherConfig", encrypted); } function loadConfig() { const encrypted = localStorage.getItem("launcherConfig"); if (!encrypted) return null; try { const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY); return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); } catch { return null; } } function showSetupPage() { document.getElementById("setup-page").classList.remove("hidden"); } function hideSetupPage() { document.getElementById("setup-page").classList.add("hidden"); } 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(); } }); document.addEventListener("DOMContentLoaded", () => { // ========================================================================== // Salva config // ========================================================================== document.getElementById("cfg-save").addEventListener("click", () => { 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); hideSetupPage(); startLauncher(); }); // Blocca il menu contestuale nativo 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; const MOVE_TOLERANCE = 18; 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()); console.log(apps); 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(); } // ========================================================================== // UTILITY POINTER (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 }; } // ========================================================================== // ZOOM STILE IPHONE (PINCH ELASTICO) + WHEEL SU PC // ========================================================================== 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); // Pinch su mobile 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); } }); // Zoom con wheel su PC document.addEventListener("wheel", e => { // Se vuoi zoomare solo con CTRL, scommenta: // if (!e.ctrlKey) return; e.preventDefault(); zoomMax = computeDynamicMaxZoom(); const direction = e.deltaY < 0 ? 1 : -1; const factor = 1 + direction * 0.1; let newZoom = zoomLevel * factor; 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 }); } // ========================================================================== // 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 MOBILE + PC // ========================================================================== function initLongPressHandlers() { // --- TOUCH --- 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(() => { if (!editMode) { enterEditMode(); if (navigator.vibrate) navigator.vibrate(10); return; } 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 icone → esce da edit mode 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 }); // --- MOUSE --- 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); }); 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; } } }); document.addEventListener("mouseup", () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; longPressTarget = null; } }); } // ========================================================================== // DRAG FLUIDO STILE IPHONE CON PLACEHOLDER + FIX "SOTTO IL DITO" // ========================================================================== function startDrag(icon, pos) { 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)"; const placeholder = icon.cloneNode(true); placeholder.classList.add("placeholder"); placeholder.style.visibility = "hidden"; icon.parentNode.insertBefore(placeholder, icon); hideContextMenu(); } function updateDragPosition(pos) { if (!draggingIcon) return; const x = pos.pageX - dragOffsetX; const y = pos.pageY - dragOffsetY; draggingIcon.style.left = `${x}px`; draggingIcon.style.top = `${y}px`; const elem = document.elementFromPoint(pos.clientX, pos.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; appsOrder.splice(from, 1); appsOrder.splice(to, 0, draggingId); saveOrder(); } // ========================================================================== // DROP PRECISO NELLA CELLA CORRETTA // ========================================================================== function endDrag() { if (!draggingIcon) return; const icon = draggingIcon; draggingIcon = null; const placeholder = folderEl.querySelector(".app-icon.placeholder"); if (placeholder) placeholder.remove(); const left = parseFloat(icon.style.left) || 0; const top = parseFloat(icon.style.top) || 0; const dropXClient = left + icon.offsetWidth / 2; const dropYClient = top + icon.offsetHeight / 2; const elem = document.elementFromPoint(dropXClient, dropYClient); 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(); } } 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 = ""; renderApps(); } function initDragHandlers() { // --- TOUCH --- 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 && (!e.touches || e.touches.length === 0)) { endDrag(); } }, { passive: true }); // --- MOUSE --- 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; dragStartX = 0; dragStartY = 0; if (draggingIcon) { endDrag(); } }); } // ========================================================================== // 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(); }); } 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 && !contextMenuEl.classList.contains("hidden")) { hideContextMenu(); } // 2️⃣ Clic fuori dalle icone → esci da wiggle mode if (!isIcon && editMode) { exitEditMode(); } }); } // ========================================================================== // LOAD APPS // ========================================================================== async function login(email, password) { const res = await fetch(`${URI}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) }); const data = await res.json(); return data.token; } async function getLinks() { const token = await login(USER, PASSW); const res = await fetch(`${URI}/links`, { headers: { "Authorization": `Bearer ${token}`, "Accept": "application/json" } }); const json = await res.json(); //console.log(json); appsData = json.map((json, i) => ({ id: json.id || `app-${i}`, name: json.name, url: json.url, icon: `${URI}${json.icon}` })); console.log(appsData); 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(); } const config = loadConfig(); let URI; let USER; let PASSW; if (!config) { showSetupPage(); } else { hideSetupPage(); startLauncher(); // la tua funzione } // ========================================================================== // INIT GLOBALE // ========================================================================== async function startLauncher() { //(async function init() { //await loadApps(); const conf = loadConfig(); URI = conf.url; USER = conf.user; PASSW = conf.password await getLinks(); initZoomHandlers(); initLongPressHandlers(); initDragHandlers(); initContextMenuActions(); initGlobalCloseHandlers(); // })(); } });