like ios
This commit is contained in:
parent
efc0c2f2c7
commit
5ea1f4fcd2
3 changed files with 720 additions and 34 deletions
534
app.js
534
app.js
|
|
@ -1,19 +1,539 @@
|
||||||
async function loadApps() {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const container = document.getElementById("folder");
|
document.addEventListener("contextmenu", e => e.preventDefault());
|
||||||
const apps = await fetch("apps.json").then(r => r.json());
|
// ==========================================================================
|
||||||
|
// 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;
|
||||||
|
|
||||||
apps.forEach(app => {
|
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "app-icon";
|
div.className = "app-icon";
|
||||||
div.onclick = () => window.open(app.url, "_blank", "noopener,noreferrer");
|
div.dataset.id = app.id;
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<img src="${app.icon}" alt="${app.name}">
|
<img src="${app.icon}" alt="${app.name}">
|
||||||
<span>${app.name}</span>
|
<span>${app.name}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.appendChild(div);
|
div.addEventListener("click", () => {
|
||||||
|
if (!editMode) window.open(app.url, "_blank", "noopener");
|
||||||
|
});
|
||||||
|
|
||||||
|
folderEl.appendChild(div);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadApps();
|
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();
|
||||||
|
})();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
|
||||||
17
index.html
17
index.html
|
|
@ -2,11 +2,26 @@
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Folder style macOS</title>
|
<title>Launcher</title>
|
||||||
|
|
||||||
|
<!-- Blocca lo zoom del browser -->
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<!-- Griglia icone -->
|
||||||
<div class="folder" id="folder"></div>
|
<div class="folder" id="folder"></div>
|
||||||
|
|
||||||
|
<!-- Menu contestuale -->
|
||||||
|
<div id="context-menu" class="context-menu hidden">
|
||||||
|
<button data-action="rename">Rinomina</button>
|
||||||
|
<button data-action="change-icon">Cambia icona</button>
|
||||||
|
<button data-action="remove">Rimuovi</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
189
style.css
189
style.css
|
|
@ -1,42 +1,193 @@
|
||||||
body {
|
/* ============================================================
|
||||||
background: #1e1e1e;
|
BASE PAGE
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden; /* impedisce pan orizzontale */
|
||||||
|
max-width: 100%;
|
||||||
|
touch-action: pan-y; /* solo scroll verticale */
|
||||||
|
background: #ffffff;
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
color: #f5f5f5;
|
color: #1a1a1a;
|
||||||
|
min-height: 100vh; /* evita scroll inutile se poche icone */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Impedisce selezione testo e highlight blu Android */
|
||||||
|
* {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variabile di zoom globale */
|
||||||
|
:root {
|
||||||
|
--zoom: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
GRIGLIA ICONE
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
.folder {
|
.folder {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, 100px);
|
grid-template-columns: repeat(
|
||||||
gap: 24px;
|
auto-fill,
|
||||||
padding: 32px;
|
minmax(calc(85px * var(--zoom)), 1fr)
|
||||||
justify-content: center;
|
);
|
||||||
|
gap: calc(16px * var(--zoom));
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
padding-bottom: 0; /* evita scroll verticale inutile */
|
||||||
|
justify-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
transition: grid-template-columns 0.15s ease-out,
|
||||||
|
gap 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Contenitore icona */
|
||||||
.app-icon {
|
.app-icon {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
transition: transform 0.18s ease, filter 0.18s ease;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Icona */
|
||||||
.app-icon img {
|
.app-icon img {
|
||||||
width: 72px;
|
width: calc(70px * var(--zoom));
|
||||||
height: 72px;
|
height: calc(70px * var(--zoom));
|
||||||
border-radius: 20px;
|
border-radius: calc(20px * var(--zoom));
|
||||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45);
|
background: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 10px rgba(0, 0, 0, 0.12),
|
||||||
|
0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
transition: width 0.18s ease-out,
|
||||||
|
height 0.18s ease-out,
|
||||||
|
border-radius 0.18s ease-out,
|
||||||
|
transform 0.18s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Etichetta */
|
||||||
.app-icon span {
|
.app-icon span {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 8px;
|
margin-top: calc(6px * var(--zoom));
|
||||||
font-size: 13px;
|
font-size: calc(11px * var(--zoom));
|
||||||
color: #ddd;
|
color: #3a3a3a;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
transition: font-size 0.18s ease-out,
|
||||||
|
margin-top 0.18s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-icon:hover {
|
/* ============================================================
|
||||||
transform: scale(1.08);
|
WIGGLE MODE
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0% { transform: rotate(-2deg) scale(1.02); }
|
||||||
|
50% { transform: rotate( 2deg) scale(0.98); }
|
||||||
|
100% { transform: rotate(-2deg) scale(1.02); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-icon:active {
|
.edit-mode .app-icon:not(.dragging) img {
|
||||||
transform: scale(0.97);
|
animation: wiggle 0.25s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icona trascinata */
|
||||||
|
.app-icon.dragging {
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder invisibile */
|
||||||
|
.app-icon.placeholder {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
MENU CONTESTUALE — ANDROID MATERIAL + RESPONSIVE ALLO ZOOM
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
#context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: calc(14px * var(--zoom));
|
||||||
|
min-width: calc(180px * var(--zoom));
|
||||||
|
padding: calc(8px * var(--zoom)) 0;
|
||||||
|
z-index: 2000;
|
||||||
|
|
||||||
|
/* Ombra Material */
|
||||||
|
box-shadow:
|
||||||
|
0 calc(6px * var(--zoom)) calc(20px * var(--zoom)) rgba(0,0,0,0.18),
|
||||||
|
0 calc(2px * var(--zoom)) calc(6px * var(--zoom)) rgba(0,0,0,0.12);
|
||||||
|
|
||||||
|
/* Animazione apertura */
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.85);
|
||||||
|
transform-origin: top center;
|
||||||
|
transition: opacity 120ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#context-menu:not(.hidden) {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#context-menu.hidden {
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulsanti del menù */
|
||||||
|
#context-menu button {
|
||||||
|
all: unset;
|
||||||
|
width: 100%;
|
||||||
|
padding: calc(14px * var(--zoom)) calc(18px * var(--zoom));
|
||||||
|
font-size: calc(15px * var(--zoom));
|
||||||
|
color: #222;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(12px * var(--zoom));
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ripple effect */
|
||||||
|
#context-menu button::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.08);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#context-menu button:active::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separatore tra voci */
|
||||||
|
#context-menu button + button {
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Voce "Rimuovi" in rosso */
|
||||||
|
#context-menu button:last-child {
|
||||||
|
color: #d11a2a;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue