5th edition

This commit is contained in:
Fabio 2026-01-04 18:37:03 +01:00
parent e702bce06d
commit eeaa3a6187
25 changed files with 1691 additions and 295 deletions

View file

@ -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: Longpress, Edit Mode, Context Menu, Global Close // BLOCCO 4/6 — Longpress, 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 licona
folderEl.insertBefore(placeholderEl, icon);
hideContextMenu(); hideContextMenu();
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Aggiorna posizione dellicona 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 dellicona 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();
} }

View file

@ -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
View file

View 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 };
}
}

View file

@ -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";

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -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;

View 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 licona 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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View file

@ -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;

View file

@ -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;
// Licona automatica è lultima 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;
} }
// ------------------------- if (e.target.classList.contains("editBtn")) {
// 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")) {
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";
closeModal.addEventListener("click", () => { document.getElementById("editModal").style.display = "flex";
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);

View 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 dallHTML
// -------------------------------
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>

View file

@ -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,19 +65,28 @@
</div> </div>
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
<div id="editModal" class="modal" style="display:none;">
<!-- MODAL EDIT -->
<div id="editModal" class="modal" style="display:none;">
<div class="modal-content"> <div class="modal-content">
<h3>Modifica link</h3> <h3>Modifica link</h3>
<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
View 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();