my_app_remote_server_ui_app/app/app.js
2025-12-31 17:26:53 +01:00

789 lines
22 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.

//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;
// Longpress / 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 = `
<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());
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();
// })();
}
});