mac_folder/app.js
2025-12-29 18:24:58 +01:00

539 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
// Longpress / 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 = `
<img src="${app.icon}" alt="${app.name}">
<span>${app.name}</span>
`;
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 licona 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 licona 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(); // longpress → wiggle mode
// initDoubleTapContextMenu(); // double tap / double click → menù
initDragHandlers();
initContextMenuActions();
})();
});