5th edition
236
app/app.js
|
|
@ -1,6 +1,6 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 1/6
|
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
|
||||||
// Sezione: Variabili globali + Storage + Config + Setup Page
|
// BLOCCO 1/6 — Variabili globali + Storage + Config + Setup Page
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -32,6 +32,7 @@ let dragOffsetX = 0;
|
||||||
let dragOffsetY = 0;
|
let dragOffsetY = 0;
|
||||||
let dragStartX = 0;
|
let dragStartX = 0;
|
||||||
let dragStartY = 0;
|
let dragStartY = 0;
|
||||||
|
let placeholderEl = null;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CRITTOGRAFIA E STORAGE
|
// CRITTOGRAFIA E STORAGE
|
||||||
|
|
@ -96,30 +97,18 @@ function loadApps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// SETUP PAGE (6 TAP PER APRIRE + AUTOCOMPILAZIONE)
|
// SETUP PAGE (6 TAP PER APRIRE + AUTOCOMPILAZIONE + "Aggiorna ora")
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
/*function showSetupPage() {
|
|
||||||
const cfg = loadConfig();
|
|
||||||
if (cfg) {
|
|
||||||
document.getElementById("cfg-url").value = cfg.url;
|
|
||||||
document.getElementById("cfg-user").value = cfg.user;
|
|
||||||
document.getElementById("cfg-pass").value = cfg.password;
|
|
||||||
}
|
|
||||||
document.getElementById("setup-page").classList.remove("hidden");
|
|
||||||
}*/
|
|
||||||
|
|
||||||
function showSetupPage() {
|
function showSetupPage() {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
||||||
if (cfg) {
|
if (cfg) {
|
||||||
// Popola i campi
|
|
||||||
document.getElementById("cfg-url").value = cfg.url;
|
document.getElementById("cfg-url").value = cfg.url;
|
||||||
document.getElementById("cfg-user").value = cfg.user;
|
document.getElementById("cfg-user").value = cfg.user;
|
||||||
document.getElementById("cfg-pass").value = cfg.password;
|
document.getElementById("cfg-pass").value = cfg.password;
|
||||||
|
|
||||||
// Mostra il pulsante "Aggiorna ora"
|
// Mostra il pulsante "Aggiorna ora" solo se esiste già una config
|
||||||
document.getElementById("cfg-refresh").style.display = "block";
|
document.getElementById("cfg-refresh").style.display = "block";
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Nessuna config → nascondi il pulsante
|
// Nessuna config → nascondi il pulsante
|
||||||
document.getElementById("cfg-refresh").style.display = "none";
|
document.getElementById("cfg-refresh").style.display = "none";
|
||||||
|
|
@ -150,9 +139,10 @@ document.addEventListener("click", () => {
|
||||||
showSetupPage();
|
showSetupPage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 2/6
|
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
|
||||||
// Sezione: API login, getLinks, ordine apps, render, startLauncher
|
// BLOCCO 2/6 — API login, getLinks, ordine apps, render, startLauncher
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -280,7 +270,7 @@ function renderApps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// START LAUNCHER (carica locale → render → aggiorna server → init UI)
|
// START LAUNCHER (carica locale → render → init UI)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function startLauncher() {
|
async function startLauncher() {
|
||||||
|
|
||||||
|
|
@ -288,7 +278,7 @@ async function startLauncher() {
|
||||||
const saved = loadApps();
|
const saved = loadApps();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
appsData = saved;
|
appsData = saved;
|
||||||
//console.log("Apps caricate da localStorage:", appsData);
|
console.log("Apps caricate da localStorage:", appsData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ Carica ordine
|
// 2️⃣ Carica ordine
|
||||||
|
|
@ -297,24 +287,24 @@ async function startLauncher() {
|
||||||
// 3️⃣ Render immediato (istantaneo)
|
// 3️⃣ Render immediato (istantaneo)
|
||||||
renderApps();
|
renderApps();
|
||||||
|
|
||||||
// 4️⃣ Aggiorna in background dal server
|
// ❌ Nessun aggiornamento automatico dal server
|
||||||
// getLinks();
|
// getLinks();
|
||||||
|
|
||||||
// 5️⃣ Inizializza UI (zoom, drag, wiggle, menu…)
|
// 4️⃣ Inizializza UI (zoom, drag, wiggle, menu…)
|
||||||
initZoomHandlers();
|
initZoomHandlers();
|
||||||
initLongPressHandlers();
|
initLongPressHandlers();
|
||||||
initDragHandlers();
|
initDragHandlers();
|
||||||
initContextMenuActions();
|
initContextMenuActions();
|
||||||
initGlobalCloseHandlers();
|
initGlobalCloseHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 3/6
|
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
|
||||||
// Sezione: Zoom stile iPhone (pinch, elasticità, wheel)
|
// BLOCCO 3/6 — Zoom stile iPhone (pinch, elasticità, wheel)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Calcolo dinamico dello zoom massimo
|
// Calcolo dinamico dello zoom massimo
|
||||||
// (dipende dalla larghezza dello schermo e dalla dimensione delle icone)
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function computeDynamicMaxZoom() {
|
function computeDynamicMaxZoom() {
|
||||||
return Math.min(window.innerWidth / 85, 4.0);
|
return Math.min(window.innerWidth / 85, 4.0);
|
||||||
|
|
@ -370,7 +360,7 @@ function initZoomHandlers() {
|
||||||
if (e.touches.length === 2) e.preventDefault();
|
if (e.touches.length === 2) e.preventDefault();
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
// Inizio pinch o doppio tap
|
// Inizio pinch (NO double tap zoom)
|
||||||
document.addEventListener("touchstart", e => {
|
document.addEventListener("touchstart", e => {
|
||||||
|
|
||||||
// Inizio pinch
|
// Inizio pinch
|
||||||
|
|
@ -379,13 +369,7 @@ function initZoomHandlers() {
|
||||||
if (zoomAnimFrame) cancelAnimationFrame(zoomAnimFrame);
|
if (zoomAnimFrame) cancelAnimationFrame(zoomAnimFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doppio tap → zoom rapido
|
// Nessuna azione sul doppio tap
|
||||||
/*const now = Date.now();
|
|
||||||
if (e.touches.length === 1 && now - lastTapTime < 300) {
|
|
||||||
zoomMax = computeDynamicMaxZoom();
|
|
||||||
applyZoom(Math.min(zoomLevel * 1.15, zoomMax));
|
|
||||||
}
|
|
||||||
lastTapTime = now;*/
|
|
||||||
lastTapTime = Date.now();
|
lastTapTime = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -449,9 +433,10 @@ function initZoomHandlers() {
|
||||||
applyZoom(newZoom);
|
applyZoom(newZoom);
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 4/6
|
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
|
||||||
// Sezione: Long‑press, Edit Mode, Context Menu, Global Close
|
// BLOCCO 4/6 — Long‑press, Edit Mode, Context Menu, Global Close
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -632,9 +617,10 @@ function initGlobalCloseHandlers() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 5/6
|
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A)
|
||||||
// Sezione: Drag & Drop stile iPhone
|
// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -658,9 +644,11 @@ function getPointerPosition(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Inizio drag: crea icona flottante + placeholder invisibile
|
/* Inizio drag: icona flottante + placeholder nel layout */
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function startDrag(icon, pos) {
|
function startDrag(icon, pos) {
|
||||||
|
const folderEl = document.getElementById("folder");
|
||||||
|
|
||||||
draggingId = icon.dataset.id;
|
draggingId = icon.dataset.id;
|
||||||
|
|
||||||
const r = icon.getBoundingClientRect();
|
const r = icon.getBoundingClientRect();
|
||||||
|
|
@ -678,20 +666,22 @@ function startDrag(icon, pos) {
|
||||||
draggingIcon.style.pointerEvents = "none";
|
draggingIcon.style.pointerEvents = "none";
|
||||||
draggingIcon.style.transform = "translate3d(0,0,0)";
|
draggingIcon.style.transform = "translate3d(0,0,0)";
|
||||||
|
|
||||||
// Placeholder invisibile che mantiene lo spazio
|
// Placeholder nel layout (slot vuoto)
|
||||||
const placeholder = icon.cloneNode(true);
|
placeholderEl = document.createElement("div");
|
||||||
placeholder.classList.add("placeholder");
|
placeholderEl.className = "app-icon placeholder";
|
||||||
placeholder.style.visibility = "hidden";
|
placeholderEl.style.visibility = "hidden";
|
||||||
icon.parentNode.insertBefore(placeholder, icon);
|
|
||||||
|
// Inserisci il placeholder dove stava l’icona
|
||||||
|
folderEl.insertBefore(placeholderEl, icon);
|
||||||
|
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Aggiorna posizione dell’icona trascinata + reorder dinamico
|
// Aggiorna posizione icona trascinata + posizione placeholder
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function updateDragPosition(pos) {
|
function updateDragPosition(pos) {
|
||||||
if (!draggingIcon) return;
|
if (!draggingIcon || !placeholderEl) return;
|
||||||
|
|
||||||
const x = pos.pageX - dragOffsetX;
|
const x = pos.pageX - dragOffsetX;
|
||||||
const y = pos.pageY - dragOffsetY;
|
const y = pos.pageY - dragOffsetY;
|
||||||
|
|
@ -699,63 +689,72 @@ function updateDragPosition(pos) {
|
||||||
draggingIcon.style.left = `${x}px`;
|
draggingIcon.style.left = `${x}px`;
|
||||||
draggingIcon.style.top = `${y}px`;
|
draggingIcon.style.top = `${y}px`;
|
||||||
|
|
||||||
const elem = document.elementFromPoint(pos.clientX, pos.clientY);
|
const centerX = pos.clientX;
|
||||||
const targetIcon = elem && elem.closest(".app-icon:not(.dragging):not(.placeholder)");
|
const centerY = pos.clientY;
|
||||||
if (!targetIcon) return;
|
|
||||||
|
|
||||||
const from = appsOrder.indexOf(draggingId);
|
const elem = document.elementFromPoint(centerX, centerY);
|
||||||
const to = appsOrder.indexOf(targetIcon.dataset.id);
|
const targetIcon = elem && elem.closest(".app-icon:not(.dragging)");
|
||||||
if (from === -1 || to === -1 || from === to) return;
|
if (!targetIcon || targetIcon === placeholderEl) return;
|
||||||
|
|
||||||
appsOrder.splice(from, 1);
|
const folderEl = document.getElementById("folder");
|
||||||
appsOrder.splice(to, 0, draggingId);
|
const targetRect = targetIcon.getBoundingClientRect();
|
||||||
saveOrder();
|
const isBefore = centerY < targetRect.top + targetRect.height / 2;
|
||||||
|
|
||||||
|
if (isBefore) {
|
||||||
|
folderEl.insertBefore(placeholderEl, targetIcon);
|
||||||
|
} else {
|
||||||
|
folderEl.insertBefore(placeholderEl, targetIcon.nextSibling);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Fine drag: drop preciso nella cella corretta
|
// Fine drag: aggiorna appsOrder in base alla posizione del placeholder
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function endDrag() {
|
function endDrag() {
|
||||||
if (!draggingIcon) return;
|
if (!draggingIcon || !placeholderEl) {
|
||||||
|
|
||||||
const icon = draggingIcon;
|
|
||||||
draggingIcon = null;
|
draggingIcon = null;
|
||||||
|
placeholderEl = null;
|
||||||
|
dragStartX = 0;
|
||||||
|
dragStartY = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Rimuovi placeholder
|
const folderEl = document.getElementById("folder");
|
||||||
const placeholder = document.querySelector(".app-icon.placeholder");
|
|
||||||
if (placeholder) placeholder.remove();
|
|
||||||
|
|
||||||
// Calcola punto centrale dell’icona trascinata
|
// Tutti i figli, inclusa la placeholder
|
||||||
const left = parseFloat(icon.style.left) || 0;
|
const children = Array.from(folderEl.children);
|
||||||
const top = parseFloat(icon.style.top) || 0;
|
const finalIndex = children.indexOf(placeholderEl);
|
||||||
const dropXClient = left + icon.offsetWidth / 2;
|
|
||||||
const dropYClient = top + icon.offsetHeight / 2;
|
|
||||||
|
|
||||||
const elem = document.elementFromPoint(dropXClient, dropYClient);
|
// Ripristina icona visuale
|
||||||
const targetIcon = elem && elem.closest(".app-icon:not(.dragging)");
|
draggingIcon.classList.remove("dragging");
|
||||||
|
draggingIcon.style.position = "";
|
||||||
|
draggingIcon.style.left = "";
|
||||||
|
draggingIcon.style.top = "";
|
||||||
|
draggingIcon.style.width = "";
|
||||||
|
draggingIcon.style.height = "";
|
||||||
|
draggingIcon.style.zIndex = "";
|
||||||
|
draggingIcon.style.pointerEvents = "";
|
||||||
|
draggingIcon.style.transform = "";
|
||||||
|
|
||||||
if (targetIcon) {
|
if (finalIndex !== -1) {
|
||||||
const from = appsOrder.indexOf(icon.dataset.id);
|
const currentIndex = appsOrder.indexOf(draggingId);
|
||||||
const to = appsOrder.indexOf(targetIcon.dataset.id);
|
if (currentIndex !== -1 && currentIndex !== finalIndex) {
|
||||||
|
appsOrder.splice(currentIndex, 1);
|
||||||
if (from !== -1 && to !== -1 && from !== to) {
|
appsOrder.splice(finalIndex, 0, draggingId);
|
||||||
appsOrder.splice(from, 1);
|
|
||||||
appsOrder.splice(to, 0, icon.dataset.id);
|
|
||||||
saveOrder();
|
saveOrder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ripristina icona
|
if (placeholderEl && placeholderEl.parentNode) {
|
||||||
icon.classList.remove("dragging");
|
placeholderEl.parentNode.removeChild(placeholderEl);
|
||||||
icon.style.position = "";
|
}
|
||||||
icon.style.left = "";
|
|
||||||
icon.style.top = "";
|
|
||||||
icon.style.width = "";
|
|
||||||
icon.style.height = "";
|
|
||||||
icon.style.zIndex = "";
|
|
||||||
icon.style.pointerEvents = "";
|
|
||||||
icon.style.transform = "";
|
|
||||||
|
|
||||||
|
draggingIcon = null;
|
||||||
|
placeholderEl = null;
|
||||||
|
dragStartX = 0;
|
||||||
|
dragStartY = 0;
|
||||||
|
|
||||||
|
// Ridisegna in base al nuovo ordine
|
||||||
renderApps();
|
renderApps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -764,9 +763,7 @@ function endDrag() {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function initDragHandlers() {
|
function initDragHandlers() {
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// TOUCH DRAG
|
// TOUCH DRAG
|
||||||
// ---------------------------------------------------------
|
|
||||||
document.addEventListener("touchstart", e => {
|
document.addEventListener("touchstart", e => {
|
||||||
if (!editMode) return;
|
if (!editMode) return;
|
||||||
if (e.touches.length !== 1) return;
|
if (e.touches.length !== 1) return;
|
||||||
|
|
@ -788,7 +785,6 @@ function initDragHandlers() {
|
||||||
|
|
||||||
const pos = getPointerPosition(e);
|
const pos = getPointerPosition(e);
|
||||||
|
|
||||||
// Inizio drag
|
|
||||||
if (!draggingIcon) {
|
if (!draggingIcon) {
|
||||||
const dx = pos.clientX - dragStartX;
|
const dx = pos.clientX - dragStartX;
|
||||||
const dy = pos.clientY - dragStartY;
|
const dy = pos.clientY - dragStartY;
|
||||||
|
|
@ -811,14 +807,17 @@ function initDragHandlers() {
|
||||||
|
|
||||||
document.addEventListener("touchend", e => {
|
document.addEventListener("touchend", e => {
|
||||||
if (!editMode) return;
|
if (!editMode) return;
|
||||||
if (draggingIcon && (!e.touches || e.touches.length === 0)) {
|
if (!draggingIcon) {
|
||||||
|
dragStartX = 0;
|
||||||
|
dragStartY = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!e.touches || e.touches.length === 0) {
|
||||||
endDrag();
|
endDrag();
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// MOUSE DRAG
|
// MOUSE DRAG
|
||||||
// ---------------------------------------------------------
|
|
||||||
document.addEventListener("mousedown", e => {
|
document.addEventListener("mousedown", e => {
|
||||||
if (!editMode) return;
|
if (!editMode) return;
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
|
|
@ -862,54 +861,19 @@ function initDragHandlers() {
|
||||||
|
|
||||||
document.addEventListener("mouseup", () => {
|
document.addEventListener("mouseup", () => {
|
||||||
if (!editMode) return;
|
if (!editMode) return;
|
||||||
dragStartX = 0;
|
|
||||||
dragStartY = 0;
|
|
||||||
if (draggingIcon) {
|
|
||||||
endDrag();
|
|
||||||
}
|
|
||||||
draggingIcon = null;
|
|
||||||
draggingId = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", e => {
|
|
||||||
if (!editMode) return;
|
|
||||||
|
|
||||||
const pos = getPointerPosition(e);
|
|
||||||
|
|
||||||
if (!draggingIcon) {
|
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;
|
dragStartX = 0;
|
||||||
dragStartY = 0;
|
dragStartY = 0;
|
||||||
if (draggingIcon) {
|
return;
|
||||||
endDrag();
|
|
||||||
}
|
}
|
||||||
|
endDrag();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 6/6 (FINALE)
|
// LAUNCHER — VERSIONE COMPLETA E
|
||||||
// Sezione: Context Menu Actions + Config Save + Init Globale
|
// OTTIMIZZATA (A) BLOCCO 6/6 — Context Menu
|
||||||
|
// Actions + Config Save + Init Globale
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -970,19 +934,17 @@ function initContextMenuActions() {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
document.getElementById("cfg-refresh").addEventListener("click", async () => {
|
document.getElementById("cfg-refresh").addEventListener("click", async () => {
|
||||||
|
|
||||||
// Carica config attuale
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
if (!cfg) {
|
if (!cfg) {
|
||||||
alert("Config mancante. Inserisci URL, user e password.");
|
alert("Config mancante. Inserisci URL, user e password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggiorna apps dal server
|
|
||||||
const ok = await getLinks();
|
const ok = await getLinks();
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
hideSetupPage();
|
hideSetupPage();
|
||||||
startLauncher(); // Ritorna subito alla schermata principale
|
startLauncher(); // Torna subito alla schermata principale
|
||||||
} else {
|
} else {
|
||||||
alert("Impossibile aggiornare le app dal server.");
|
alert("Impossibile aggiornare le app dal server.");
|
||||||
}
|
}
|
||||||
|
|
@ -996,16 +958,12 @@ document.getElementById("cfg-save").addEventListener("click", async () => {
|
||||||
const user = document.getElementById("cfg-user").value;
|
const user = document.getElementById("cfg-user").value;
|
||||||
const pass = document.getElementById("cfg-pass").value;
|
const pass = document.getElementById("cfg-pass").value;
|
||||||
|
|
||||||
// Salva configurazione
|
|
||||||
saveConfig(url, user, pass);
|
saveConfig(url, user, pass);
|
||||||
|
|
||||||
// Scarica apps dal server
|
|
||||||
const ok = await getLinks();
|
const ok = await getLinks();
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
hideSetupPage();
|
hideSetupPage();
|
||||||
|
|
||||||
// Restart completo del launcher
|
|
||||||
startLauncher();
|
startLauncher();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1017,10 +975,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
||||||
if (!cfg) {
|
if (!cfg) {
|
||||||
// Primo avvio → mostra setup
|
|
||||||
showSetupPage();
|
showSetupPage();
|
||||||
} else {
|
} else {
|
||||||
// Config presente → avvia launcher
|
|
||||||
hideSetupPage();
|
hideSetupPage();
|
||||||
startLauncher();
|
startLauncher();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|
||||||
<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
<!-- --> <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
<script>
|
<script>
|
||||||
eruda.init();
|
eruda.init();
|
||||||
</script> -->
|
</script> <!-- -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
0
app/start.sh
Executable file → Normal file
132
server/backend/appMetadata.js
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
// appMetadata.js
|
||||||
|
import axios from "axios";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
import sizeOf from "image-size";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
export async function getAppMetadata(baseUrl) {
|
||||||
|
console.log(baseUrl);
|
||||||
|
try {
|
||||||
|
const res = await axios.get(baseUrl, { timeout: 3000 });
|
||||||
|
const $ = cheerio.load(res.data);
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 1. Nome più corto
|
||||||
|
// -------------------------------
|
||||||
|
const nameCandidates = [
|
||||||
|
$('meta[property="og:site_name"]').attr("content"),
|
||||||
|
$('meta[name="application-name"]').attr("content"),
|
||||||
|
$('meta[property="og:title"]').attr("content"),
|
||||||
|
$("title").text().trim()
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const name = nameCandidates.length > 0
|
||||||
|
? nameCandidates.sort((a, b) => a.length - b.length)[0]
|
||||||
|
: "no_name";
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 2. Icone HTML
|
||||||
|
// -------------------------------
|
||||||
|
const htmlIcons = [];
|
||||||
|
$("link[rel*='icon']").each((_, el) => {
|
||||||
|
const href = $(el).attr("href");
|
||||||
|
const sizes = $(el).attr("sizes") || "";
|
||||||
|
if (href) htmlIcons.push({ href, sizes });
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 3. Manifest.json
|
||||||
|
// -------------------------------
|
||||||
|
let manifestIcons = [];
|
||||||
|
const manifestHref = $('link[rel="manifest"]').attr("href");
|
||||||
|
|
||||||
|
if (manifestHref) {
|
||||||
|
try {
|
||||||
|
const manifestUrl = new URL(manifestHref, baseUrl).href;
|
||||||
|
const manifestRes = await axios.get(manifestUrl, { timeout: 3000 });
|
||||||
|
const manifest = manifestRes.data;
|
||||||
|
|
||||||
|
if (manifest.icons && Array.isArray(manifest.icons)) {
|
||||||
|
manifestIcons = manifest.icons.map(icon => ({
|
||||||
|
href: icon.src,
|
||||||
|
sizes: icon.sizes || ""
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 4. Fallback 4 icone
|
||||||
|
// -------------------------------
|
||||||
|
const fallbackPaths = [
|
||||||
|
"/favicon.ico",
|
||||||
|
"/favicon.png",
|
||||||
|
"/icon.png",
|
||||||
|
"/apple-touch-icon.png"
|
||||||
|
];
|
||||||
|
|
||||||
|
const fallbackIcons = fallbackPaths.map(p => ({
|
||||||
|
href: p,
|
||||||
|
sizes: ""
|
||||||
|
}));
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 5. Unisci tutte le icone
|
||||||
|
// -------------------------------
|
||||||
|
const allIcons = [...htmlIcons, ...manifestIcons, ...fallbackIcons];
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 6. Determina dimensione reale (PNG, ICO, SVG)
|
||||||
|
// -------------------------------
|
||||||
|
const iconsWithRealSize = [];
|
||||||
|
|
||||||
|
for (const icon of allIcons) {
|
||||||
|
try {
|
||||||
|
const url = new URL(icon.href, baseUrl).href;
|
||||||
|
const imgRes = await axios.get(url, { responseType: "arraybuffer" });
|
||||||
|
|
||||||
|
let width = 0;
|
||||||
|
|
||||||
|
// ---- PNG / JPG / ICO ----
|
||||||
|
try {
|
||||||
|
const dim = sizeOf(imgRes.data);
|
||||||
|
if (dim.width) width = dim.width;
|
||||||
|
} catch {
|
||||||
|
// Non è un formato supportato da image-size
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- SVG → converti in PNG e misura ----
|
||||||
|
if (width === 0) {
|
||||||
|
try {
|
||||||
|
const pngBuffer = await sharp(imgRes.data).png().toBuffer();
|
||||||
|
const dim = sizeOf(pngBuffer);
|
||||||
|
if (dim.width) width = dim.width;
|
||||||
|
} catch {
|
||||||
|
// SVG non convertibile → ignora
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width > 0) {
|
||||||
|
iconsWithRealSize.push({ url, size: width });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
// icona non accessibile → ignora
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 7. Scegli la più grande
|
||||||
|
// -------------------------------
|
||||||
|
iconsWithRealSize.sort((a, b) => b.size - a.size);
|
||||||
|
|
||||||
|
const icon = iconsWithRealSize.length > 0
|
||||||
|
? iconsWithRealSize[0].url
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { name, icon };
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
return { name: "no_name", icon: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import cors from "cors";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import linksRouter from "./routes/links.js";
|
import linksRouter from "./routes/links.js";
|
||||||
import authRouter from "./routes/auth.js";
|
import authRouter from "./routes/auth.js";
|
||||||
|
import metadataRouter from "./routes/metadata.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
|
@ -21,6 +22,9 @@ app.use("/auth", authRouter);
|
||||||
// Link routes (protette)
|
// Link routes (protette)
|
||||||
app.use("/links", linksRouter);
|
app.use("/links", linksRouter);
|
||||||
|
|
||||||
|
// link per metadata
|
||||||
|
app.use("/metadata", metadataRouter);
|
||||||
|
|
||||||
// Connessione Mongo (URL da env con fallback)
|
// Connessione Mongo (URL da env con fallback)
|
||||||
const MONGO_URI = process.env.MONGO_URI || "mongodb://mongo:27017/mydb";
|
const MONGO_URI = process.env.MONGO_URI || "mongodb://mongo:27017/mydb";
|
||||||
|
|
||||||
|
|
|
||||||
989
server/backend/package-lock.json
generated
|
|
@ -1,13 +1,17 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"cheerio": "^1.1.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"image-size": "^2.0.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.0.2",
|
"mongoose": "^9.0.2",
|
||||||
"multer": "^2.0.2"
|
"multer": "^2.0.2",
|
||||||
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js"
|
"start": "node index.js"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
|
import axios from "axios";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
import Link from "../models/Link.js";
|
import Link from "../models/Link.js";
|
||||||
import { authMiddleware } from "../middleware/auth.js";
|
import { authMiddleware } from "../middleware/auth.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Config upload
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: "uploads/",
|
destination: "uploads/",
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
|
|
@ -15,19 +17,46 @@ const storage = multer.diskStorage({
|
||||||
});
|
});
|
||||||
const upload = multer({ storage });
|
const upload = multer({ storage });
|
||||||
|
|
||||||
// Tutte le rotte protette
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
// GET /links - lista dei link dell'utente
|
async function downloadImage(url) {
|
||||||
|
const filename = Date.now() + ".png";
|
||||||
|
const filepath = path.join("uploads", filename);
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
url,
|
||||||
|
method: "GET",
|
||||||
|
responseType: "stream",
|
||||||
|
headers: { "User-Agent": "Mozilla/5.0" }
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const stream = response.data.pipe(fs.createWriteStream(filepath));
|
||||||
|
stream.on("finish", resolve);
|
||||||
|
stream.on("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
return "/uploads/" + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOldIcon(iconPath) {
|
||||||
|
if (!iconPath) return;
|
||||||
|
const full = path.join(process.cwd(), iconPath.replace("/", ""));
|
||||||
|
fs.unlink(full, () => {});
|
||||||
|
}
|
||||||
|
|
||||||
router.get("/", async (req, res) => {
|
router.get("/", async (req, res) => {
|
||||||
const links = await Link.find({ owner: req.userId });
|
const links = await Link.find({ owner: req.userId });
|
||||||
res.json(links);
|
res.json(links);
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /links - crea nuovo link con eventuale icona
|
|
||||||
router.post("/", upload.single("icon"), async (req, res) => {
|
router.post("/", upload.single("icon"), async (req, res) => {
|
||||||
const { url, name } = req.body;
|
const { url, name, iconURL } = req.body;
|
||||||
const iconPath = req.file ? `/uploads/${req.file.filename}` : null;
|
|
||||||
|
let iconPath = null;
|
||||||
|
|
||||||
|
if (req.file) iconPath = `/uploads/${req.file.filename}`;
|
||||||
|
else if (iconURL) iconPath = await downloadImage(iconURL);
|
||||||
|
|
||||||
const link = await Link.create({
|
const link = await Link.create({
|
||||||
url,
|
url,
|
||||||
|
|
@ -39,39 +68,44 @@ router.post("/", upload.single("icon"), async (req, res) => {
|
||||||
res.json(link);
|
res.json(link);
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /links/:id
|
|
||||||
router.delete("/:id", async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const link = await Link.findOneAndDelete({
|
|
||||||
_id: id,
|
|
||||||
owner: req.userId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!link) return res.status(404).json({ error: "Link non trovato" });
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT /links/:id
|
|
||||||
router.put("/:id", upload.single("icon"), async (req, res) => {
|
router.put("/:id", upload.single("icon"), async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { name, url } = req.body;
|
const { name, url, iconURL } = req.body;
|
||||||
|
|
||||||
const update = {};
|
const link = await Link.findOne({ _id: id, owner: req.userId });
|
||||||
if (name) update.name = name;
|
if (!link) return res.status(404).json({ error: "Link non trovato" });
|
||||||
if (url) update.url = url;
|
|
||||||
if (req.file) update.icon = `/uploads/${req.file.filename}`;
|
|
||||||
|
|
||||||
const link = await Link.findOneAndUpdate(
|
const update = { name, url };
|
||||||
|
let newIcon = null;
|
||||||
|
|
||||||
|
if (req.file) newIcon = `/uploads/${req.file.filename}`;
|
||||||
|
else if (iconURL) newIcon = await downloadImage(iconURL);
|
||||||
|
|
||||||
|
if (newIcon) {
|
||||||
|
deleteOldIcon(link.icon);
|
||||||
|
update.icon = newIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await Link.findOneAndUpdate(
|
||||||
{ _id: id, owner: req.userId },
|
{ _id: id, owner: req.userId },
|
||||||
update,
|
update,
|
||||||
{ new: true }
|
{ new: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
res.json(updated);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/:id", async (req, res) => {
|
||||||
|
const link = await Link.findOneAndDelete({
|
||||||
|
_id: req.params.id,
|
||||||
|
owner: req.userId
|
||||||
|
});
|
||||||
|
|
||||||
if (!link) return res.status(404).json({ error: "Link non trovato" });
|
if (!link) return res.status(404).json({ error: "Link non trovato" });
|
||||||
|
|
||||||
res.json(link);
|
deleteOldIcon(link.icon);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
104
server/backend/routes/metadata.js
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import express from "express";
|
||||||
|
import axios from "axios";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
import { URL } from "url";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Normalizza URL relativi → assoluti
|
||||||
|
function normalize(base, relative) {
|
||||||
|
try {
|
||||||
|
return new URL(relative, base).href;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scarica HTML con fallback CORS
|
||||||
|
async function fetchHTML(url) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(url, {
|
||||||
|
timeout: 8000,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const siteUrl = req.query.url;
|
||||||
|
if (!siteUrl) return res.json({ error: "Missing URL" });
|
||||||
|
|
||||||
|
const html = await fetchHTML(siteUrl);
|
||||||
|
if (!html) return res.json({ name: null, icon: null });
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// 1. Trova il nome più corto
|
||||||
|
// -----------------------------------------
|
||||||
|
let names = [];
|
||||||
|
|
||||||
|
const title = $("title").text().trim();
|
||||||
|
if (title) names.push(title);
|
||||||
|
|
||||||
|
$('meta[name="application-name"]').each((i, el) => {
|
||||||
|
const v = $(el).attr("content");
|
||||||
|
if (v) names.push(v.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
$('meta[property="og:site_name"]').each((i, el) => {
|
||||||
|
const v = $(el).attr("content");
|
||||||
|
if (v) names.push(v.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
const shortestName = names.length
|
||||||
|
? names.sort((a, b) => a.length - b.length)[0]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// 2. Trova l’icona più grande
|
||||||
|
// -----------------------------------------
|
||||||
|
let icons = [];
|
||||||
|
|
||||||
|
$('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"]').each((i, el) => {
|
||||||
|
const href = $(el).attr("href");
|
||||||
|
if (!href) return;
|
||||||
|
|
||||||
|
const sizeAttr = $(el).attr("sizes");
|
||||||
|
let size = 0;
|
||||||
|
|
||||||
|
if (sizeAttr && sizeAttr.includes("x")) {
|
||||||
|
const parts = sizeAttr.split("x");
|
||||||
|
size = parseInt(parts[0]) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
icons.push({
|
||||||
|
url: normalize(siteUrl, href),
|
||||||
|
size
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// fallback favicon
|
||||||
|
icons.push({
|
||||||
|
url: normalize(siteUrl, "/favicon.ico"),
|
||||||
|
size: 16
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ordina per dimensione
|
||||||
|
icons = icons.filter(i => i.url);
|
||||||
|
icons.sort((a, b) => b.size - a.size);
|
||||||
|
|
||||||
|
const bestIcon = icons.length ? icons[0].url : null;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
name: shortestName,
|
||||||
|
icon: bestIcon
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
const API_BASE = "http://192.168.1.3:3000";
|
const API_BASE = "https://myapps_svr.patachina2.casacam.net";
|
||||||
|
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
// AUTH
|
// AUTH
|
||||||
|
|
@ -45,7 +45,7 @@ export async function getLinks(token) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createLink(token, { name, url, iconFile }) {
|
/*export async function createLink(token, { name, url, iconFile }) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("name", name);
|
formData.append("name", name);
|
||||||
formData.append("url", url);
|
formData.append("url", url);
|
||||||
|
|
@ -59,6 +59,30 @@ export async function createLink(token, { name, url, iconFile }) {
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || "Errore creazione link");
|
||||||
|
return data;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
export async function createLink(token, { name, url, iconFile, iconURL }) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("name", name);
|
||||||
|
formData.append("url", url);
|
||||||
|
|
||||||
|
if (iconFile) {
|
||||||
|
formData.append("icon", iconFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconURL) {
|
||||||
|
formData.append("iconURL", iconURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/links`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || "Errore creazione link");
|
if (!res.ok) throw new Error(data.error || "Errore creazione link");
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
|
|
@ -7,78 +7,70 @@ import {
|
||||||
updateLink
|
updateLink
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
|
|
||||||
const authSection = document.getElementById("authSection");
|
const URL_SVR = "https://myapps_svr.patachina2.casacam.net";
|
||||||
const linkSection = document.getElementById("linkSection");
|
|
||||||
const authStatus = document.getElementById("authStatus");
|
|
||||||
const list = document.getElementById("list");
|
|
||||||
const editModal = document.getElementById("editModal");
|
|
||||||
const editForm = document.getElementById("editForm");
|
|
||||||
const closeModal = document.getElementById("closeModal");
|
|
||||||
|
|
||||||
let editingId = null;
|
|
||||||
let token = null;
|
let token = null;
|
||||||
|
let autoIconURL = null;
|
||||||
|
let editingId = null;
|
||||||
|
|
||||||
// ======================================================
|
// ===============================
|
||||||
|
// MOSTRA DIMENSIONI IMMAGINE
|
||||||
|
// ===============================
|
||||||
|
function showImageSize(imgElement, sizeElement) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
sizeElement.textContent = `${img.width} × ${img.height} px`;
|
||||||
|
sizeElement.style.display = "block";
|
||||||
|
};
|
||||||
|
img.src = imgElement.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===============================
|
||||||
// AUTH
|
// AUTH
|
||||||
// ======================================================
|
// ===============================
|
||||||
|
|
||||||
function setToken(t) {
|
function setToken(t) {
|
||||||
token = t;
|
token = t;
|
||||||
|
document.getElementById("authSection").style.display = token ? "none" : "block";
|
||||||
if (token) {
|
document.getElementById("linkSection").style.display = token ? "block" : "none";
|
||||||
authSection.style.display = "none";
|
if (token) loadLinks();
|
||||||
linkSection.style.display = "block";
|
|
||||||
loadLinks();
|
|
||||||
} else {
|
|
||||||
authSection.style.display = "block";
|
|
||||||
linkSection.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("loginForm").addEventListener("submit", async e => {
|
document.getElementById("loginForm").addEventListener("submit", async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const email = e.target.email.value;
|
|
||||||
const password = e.target.password.value;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const t = await login(email, password);
|
const t = await login(e.target.email.value, e.target.password.value);
|
||||||
setToken(t);
|
setToken(t);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authStatus.textContent = err.message;
|
document.getElementById("authStatus").textContent = err.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("registerForm").addEventListener("submit", async e => {
|
document.getElementById("registerForm").addEventListener("submit", async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const email = e.target.email.value;
|
|
||||||
const password = e.target.password.value;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await register(email, password);
|
await register(e.target.email.value, e.target.password.value);
|
||||||
authStatus.textContent = "Registrato! Ora effettua il login.";
|
document.getElementById("authStatus").textContent = "Registrato! Ora accedi.";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authStatus.textContent = err.message;
|
document.getElementById("authStatus").textContent = err.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ======================================================
|
// ===============================
|
||||||
// LINKS
|
// LOAD LINKS
|
||||||
// ======================================================
|
// ===============================
|
||||||
|
|
||||||
async function loadLinks() {
|
async function loadLinks() {
|
||||||
const links = await getLinks(token);
|
const links = await getLinks(token);
|
||||||
|
const list = document.getElementById("list");
|
||||||
|
|
||||||
list.innerHTML = links
|
list.innerHTML = links
|
||||||
.map(
|
.map(
|
||||||
link => `
|
link => `
|
||||||
<div class="item" data-id="${link._id}">
|
<div class="item" data-id="${link._id}">
|
||||||
${link.icon ? `<img src="http://192.168.1.3:3000${link.icon}">` : ""}
|
${link.icon ? `<img src="${URL_SVR}${link.icon}">` : ""}
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<strong>${link.name}</strong><br>
|
<strong>${link.name}</strong><br>
|
||||||
<a href="${link.url}" target="_blank">${link.url}</a>
|
<a href="${link.url}" target="_blank">${link.url}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="editBtn" data-id="${link._id}">Modifica</button>
|
<button class="editBtn" data-id="${link._id}">Modifica</button>
|
||||||
<button class="deleteBtn" data-id="${link._id}">Elimina</button>
|
<button class="deleteBtn" data-id="${link._id}">Elimina</button>
|
||||||
|
|
@ -89,103 +81,144 @@ async function loadLinks() {
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ===============================
|
||||||
// CREAZIONE LINK
|
// METADATA (icona automatica)
|
||||||
// ======================================================
|
// ===============================
|
||||||
|
document.getElementById("fetchMetaBtn").addEventListener("click", async () => {
|
||||||
|
const url = document.getElementById("urlInput").value.trim();
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
const res = await fetch(`${URL_SVR}/metadata?url=${encodeURIComponent(url)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
document.getElementById("nameInput").value = data.name || "";
|
||||||
|
autoIconURL = data.icon || null;
|
||||||
|
|
||||||
|
// L’icona automatica è l’ultima scelta → reset input manuale
|
||||||
|
const fileInput = document.getElementById("iconInput");
|
||||||
|
fileInput.value = "";
|
||||||
|
|
||||||
|
const preview = document.getElementById("iconPreview");
|
||||||
|
const sizeBox = document.getElementById("iconSize");
|
||||||
|
|
||||||
|
if (autoIconURL) {
|
||||||
|
preview.src = autoIconURL;
|
||||||
|
preview.style.display = "block";
|
||||||
|
sizeBox.style.display = "none";
|
||||||
|
showImageSize(preview, sizeBox);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// ANTEPRIMA ICONA MANUALE
|
||||||
|
// ===============================
|
||||||
|
document.getElementById("iconInput").addEventListener("change", e => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
autoIconURL = null; // manuale vince
|
||||||
|
|
||||||
|
const preview = document.getElementById("iconPreview");
|
||||||
|
const sizeBox = document.getElementById("iconSize");
|
||||||
|
|
||||||
|
preview.src = URL.createObjectURL(file);
|
||||||
|
preview.style.display = "block";
|
||||||
|
sizeBox.style.display = "none";
|
||||||
|
|
||||||
|
showImageSize(preview, sizeBox);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// CREAZIONE LINK
|
||||||
|
// ===============================
|
||||||
document.getElementById("linkForm").addEventListener("submit", async e => {
|
document.getElementById("linkForm").addEventListener("submit", async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const formData = new FormData(e.target);
|
const raw = new FormData(e.target);
|
||||||
const iconFile = formData.get("icon");
|
const manualFile = raw.get("icon");
|
||||||
|
const hasManualFile = manualFile instanceof File && manualFile.size > 0;
|
||||||
|
|
||||||
await createLink(token, {
|
await createLink(token, {
|
||||||
name: formData.get("name"),
|
name: raw.get("name"),
|
||||||
url: formData.get("url"),
|
url: raw.get("url"),
|
||||||
iconFile: iconFile.size > 0 ? iconFile : null
|
iconFile: hasManualFile ? manualFile : null,
|
||||||
|
iconURL: !hasManualFile ? autoIconURL : null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
autoIconURL = null;
|
||||||
|
document.getElementById("iconPreview").style.display = "none";
|
||||||
|
document.getElementById("iconSize").style.display = "none";
|
||||||
|
|
||||||
e.target.reset();
|
e.target.reset();
|
||||||
loadLinks();
|
loadLinks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ======================================================
|
// ===============================
|
||||||
// AZIONI: MODIFICA + ELIMINA
|
// EDIT
|
||||||
// ======================================================
|
// ===============================
|
||||||
|
document.getElementById("list").addEventListener("click", e => {
|
||||||
list.addEventListener("click", async e => {
|
|
||||||
const id = e.target.dataset.id;
|
const id = e.target.dataset.id;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// ELIMINA
|
|
||||||
// -------------------------
|
|
||||||
if (e.target.classList.contains("deleteBtn")) {
|
if (e.target.classList.contains("deleteBtn")) {
|
||||||
if (confirm("Vuoi davvero eliminare questo link?")) {
|
deleteLink(token, id).then(loadLinks);
|
||||||
await deleteLink(token, id);
|
|
||||||
loadLinks();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// MODIFICA
|
|
||||||
// -------------------------
|
|
||||||
/* if (e.target.classList.contains("editBtn")) {
|
|
||||||
const newName = prompt("Nuovo nome:");
|
|
||||||
const newUrl = prompt("Nuovo URL:");
|
|
||||||
|
|
||||||
if (!newName && !newUrl) return;
|
|
||||||
|
|
||||||
await updateLink(token, id, {
|
|
||||||
name: newName,
|
|
||||||
url: newUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
loadLinks();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if (e.target.classList.contains("editBtn")) {
|
if (e.target.classList.contains("editBtn")) {
|
||||||
const id = e.target.dataset.id;
|
|
||||||
editingId = id;
|
editingId = id;
|
||||||
|
|
||||||
// Precompila i campi
|
|
||||||
const item = e.target.closest(".item");
|
const item = e.target.closest(".item");
|
||||||
const name = item.querySelector("strong").textContent;
|
const name = item.querySelector("strong").textContent;
|
||||||
const url = item.querySelector("a").textContent;
|
const url = item.querySelector("a").textContent;
|
||||||
|
|
||||||
editForm.name.value = name;
|
const form = document.getElementById("editForm");
|
||||||
editForm.url.value = url;
|
form.name.value = name;
|
||||||
editForm.icon.value = ""; // reset file input
|
form.url.value = url;
|
||||||
|
|
||||||
editModal.style.display = "flex";
|
document.getElementById("iconPreviewEdit").style.display = "none";
|
||||||
|
document.getElementById("iconSizeEdit").style.display = "none";
|
||||||
|
|
||||||
|
document.getElementById("editModal").style.display = "flex";
|
||||||
}
|
}
|
||||||
|
|
||||||
closeModal.addEventListener("click", () => {
|
|
||||||
editModal.style.display = "none";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
editForm.addEventListener("submit", async e => {
|
// ANTEPRIMA MANUALE IN EDIT
|
||||||
|
document.getElementById("iconInputEdit").addEventListener("change", e => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const preview = document.getElementById("iconPreviewEdit");
|
||||||
|
const sizeBox = document.getElementById("iconSizeEdit");
|
||||||
|
|
||||||
|
preview.src = URL.createObjectURL(file);
|
||||||
|
preview.style.display = "block";
|
||||||
|
sizeBox.style.display = "none";
|
||||||
|
|
||||||
|
showImageSize(preview, sizeBox);
|
||||||
|
});
|
||||||
|
|
||||||
|
// SALVA EDIT
|
||||||
|
document.getElementById("editForm").addEventListener("submit", async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const name = editForm.name.value;
|
const name = e.target.name.value;
|
||||||
const url = editForm.url.value;
|
const url = e.target.url.value;
|
||||||
const iconFile = editForm.icon.files[0] || null;
|
const iconFile = e.target.icon.files[0] || null;
|
||||||
|
|
||||||
await updateLink(token, editingId, {
|
await updateLink(token, editingId, {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
iconFile
|
iconFile,
|
||||||
|
iconURL: null
|
||||||
});
|
});
|
||||||
|
|
||||||
editModal.style.display = "none";
|
document.getElementById("editModal").style.display = "none";
|
||||||
loadLinks();
|
loadLinks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById("closeModal").addEventListener("click", () => {
|
||||||
|
document.getElementById("editModal").style.display = "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// INIT
|
|
||||||
// ======================================================
|
|
||||||
|
|
||||||
setToken(null);
|
setToken(null);
|
||||||
|
|
|
||||||
83
server/frontend/getAppMetadata.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script>0
|
||||||
|
async function getAppMetadata(baseUrl) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(baseUrl, { mode: "cors" });
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, "text/html");
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 1. Raccogli tutti i nomi possibili
|
||||||
|
// -------------------------------
|
||||||
|
const nameCandidates = [
|
||||||
|
doc.querySelector('meta[property="og:site_name"]')?.content,
|
||||||
|
doc.querySelector('meta[name="application-name"]')?.content,
|
||||||
|
doc.querySelector('meta[property="og:title"]')?.content,
|
||||||
|
doc.querySelector("title")?.textContent?.trim()
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const name = nameCandidates.length > 0
|
||||||
|
? nameCandidates.sort((a, b) => a.length - b.length)[0]
|
||||||
|
: "no_name";
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 2. Raccogli icone dall’HTML
|
||||||
|
// -------------------------------
|
||||||
|
const htmlIcons = [...doc.querySelectorAll("link[rel*='icon']")].map(link => ({
|
||||||
|
href: link.getAttribute("href"),
|
||||||
|
sizes: link.getAttribute("sizes") || ""
|
||||||
|
}));
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 3. Cerca il manifest.json
|
||||||
|
// -------------------------------
|
||||||
|
let manifestIcons = [];
|
||||||
|
const manifestLink = doc.querySelector('link[rel="manifest"]');
|
||||||
|
|
||||||
|
if (manifestLink) {
|
||||||
|
try {
|
||||||
|
const manifestUrl = new URL(manifestLink.href, baseUrl).href;
|
||||||
|
const manifestRes = await fetch(manifestUrl, { mode: "cors" });
|
||||||
|
const manifestJson = await manifestRes.json();
|
||||||
|
|
||||||
|
if (manifestJson.icons && Array.isArray(manifestJson.icons)) {
|
||||||
|
manifestIcons = manifestJson.icons.map(icon => ({
|
||||||
|
href: icon.src,
|
||||||
|
sizes: icon.sizes || ""
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Manifest non accessibile o non valido
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 4. Unisci icone HTML + manifest
|
||||||
|
// -------------------------------
|
||||||
|
const allIcons = [...htmlIcons, ...manifestIcons];
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 5. Ordina per dimensione (più grande prima)
|
||||||
|
// -------------------------------
|
||||||
|
allIcons.sort((a, b) => {
|
||||||
|
const sizeA = parseInt(a.sizes.split("x")[0]) || 0;
|
||||||
|
const sizeB = parseInt(b.sizes.split("x")[0]) || 0;
|
||||||
|
return sizeB - sizeA;
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// 6. Risolvi URL assoluto
|
||||||
|
// -------------------------------
|
||||||
|
let icon = null;
|
||||||
|
if (allIcons.length > 0) {
|
||||||
|
icon = new URL(allIcons[0].href, baseUrl).href;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, icon };
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return { name: "no_name", icon: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -37,9 +37,21 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Nuovo link</h2>
|
<h2>Nuovo link</h2>
|
||||||
<form id="linkForm">
|
<form id="linkForm">
|
||||||
<input type="text" name="name" placeholder="Nome" required>
|
|
||||||
<input type="text" name="url" placeholder="URL" required>
|
<div style="display:flex; gap:10px; align-items:center;">
|
||||||
<input type="file" name="icon" accept="image/*">
|
<input type="text" name="url" id="urlInput" placeholder="URL" required style="flex:1;">
|
||||||
|
<button type="button" id="fetchMetaBtn">Cerca</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="text" name="name" id="nameInput" placeholder="Nome" required>
|
||||||
|
|
||||||
|
<!-- Icona manuale -->
|
||||||
|
<input type="file" id="iconInput" name="icon" accept="image/*">
|
||||||
|
|
||||||
|
<!-- Anteprima -->
|
||||||
|
<img id="iconPreview" style="width:64px; height:64px; margin-top:10px; display:none;">
|
||||||
|
<div id="iconSize" style="margin-top:5px; font-size:12px; color:#666; display:none;"></div>
|
||||||
|
|
||||||
<button type="submit">Salva</button>
|
<button type="submit">Salva</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -53,6 +65,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="app.js"></script>
|
<script type="module" src="app.js"></script>
|
||||||
|
|
||||||
|
<!-- MODAL EDIT -->
|
||||||
<div id="editModal" class="modal" style="display:none;">
|
<div id="editModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3>Modifica link</h3>
|
<h3>Modifica link</h3>
|
||||||
|
|
@ -60,12 +74,19 @@
|
||||||
<form id="editForm">
|
<form id="editForm">
|
||||||
<input type="text" name="name" placeholder="Nome">
|
<input type="text" name="name" placeholder="Nome">
|
||||||
<input type="text" name="url" placeholder="URL">
|
<input type="text" name="url" placeholder="URL">
|
||||||
<input type="file" name="icon" accept="image/*">
|
|
||||||
|
<input type="file" id="iconInputEdit" name="icon" accept="image/*">
|
||||||
|
|
||||||
|
<img id="iconPreviewEdit" style="width:64px; height:64px; margin-top:10px; display:none;">
|
||||||
|
<div id="iconSizeEdit" style="margin-top:5px; font-size:12px; color:#666; display:none;"></div>
|
||||||
|
|
||||||
<button type="submit">Salva modifiche</button>
|
<button type="submit">Salva modifiche</button>
|
||||||
<button type="button" id="closeModal">Annulla</button>
|
<button type="button" id="closeModal">Annulla</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
|
<script>eruda.init();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
16
varie/a.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// a.js
|
||||||
|
import { getAppMetadata } from "./appMetadata.js";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const url = process.argv[2];
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.log("Uso: node a.js <URL>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getAppMetadata(url);
|
||||||
|
console.log(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||