456 lines
14 KiB
HTML
456 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Siti vari</title>
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||
<link rel="apple-touch-icon" sizes="57x57" href="/myicons/favicon-57x57.png">
|
||
<link rel="apple-touch-icon" sizes="60x60" href="/myicons/favicon-60x60.png">
|
||
<link rel="apple-touch-icon" sizes="72x72" href="/myicons/favicon-72x72.png">
|
||
<link rel="apple-touch-icon" sizes="76x76" href="/myicons/favicon-76x76.png">
|
||
<link rel="apple-touch-icon" sizes="114x114" href="/myicons/favicon-114x114.png">
|
||
<link rel="apple-touch-icon" sizes="120x120" href="/myicons/favicon-120x120.png">
|
||
<link rel="apple-touch-icon" sizes="144x144" href="/myicons/favicon-144x144.png">
|
||
<link rel="apple-touch-icon" sizes="152x152" href="/myicons/favicon-152x152.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/myicons/favicon-180x180.png">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/myicons/favicon-16x16.png">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/myicons/favicon-32x32.png">
|
||
<link rel="icon" type="image/png" sizes="96x96" href="/myicons/favicon-96x96.png">
|
||
<link rel="icon" type="image/png" sizes="192x192" href="/myicons/favicon-192x192.png">
|
||
<link rel="shortcut icon" type="image/x-icon" href="/myicons/favicon.ico">
|
||
<link rel="icon" type="image/x-icon" href="/myicons/favicon.ico">
|
||
<meta name="msapplication-TileColor" content="#ffffff">
|
||
<meta name="msapplication-TileImage" content="/myicons/favicon-144x144.png">
|
||
<meta name="msapplication-config" content="/myicons/browserconfig.xml">
|
||
<link rel="manifest" href="/myicons/manifest.json">
|
||
<meta name="theme-color" content="#ffffff">
|
||
<style>
|
||
:root {
|
||
--sidebar-w: 260px;
|
||
--sidebar-bg: #0f172a;
|
||
--sidebar-fg: #e2e8f0;
|
||
--border: #1f2937;
|
||
--overlay-bg: rgba(0,0,0,0.5);
|
||
--transition: 250ms ease;
|
||
--focus: #f59e0b;
|
||
}
|
||
|
||
/* Base */
|
||
html, body {
|
||
margin: 0;
|
||
padding: 0;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
font-family: system-ui, sans-serif;
|
||
background: radial-gradient(1200px 700px at 20% 10%, #0b1220 0%, #070c1a 50%, #050a16 100%);
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
/* Icona menù */
|
||
.menu-btn {
|
||
position: fixed;
|
||
top: 12px;
|
||
left: 12px;
|
||
z-index: 100;
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: #e2e8f0;
|
||
padding: 4px;
|
||
transition: opacity var(--transition);
|
||
}
|
||
.menu-btn.hidden { opacity: 0; pointer-events: none; }
|
||
.menu-btn.dragging { cursor: grabbing; }
|
||
.menu-btn svg { pointer-events: none; }
|
||
|
||
/* Sidebar */
|
||
.sidebar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: var(--sidebar-w);
|
||
height: 100%;
|
||
background: linear-gradient(180deg, #0f172a 0%, #0c1423 100%);
|
||
color: var(--sidebar-fg);
|
||
transform: translateX(-100%);
|
||
transition: transform var(--transition);
|
||
z-index: 90;
|
||
padding: 16px 12px;
|
||
border-right: 1px solid var(--border);
|
||
}
|
||
.sidebar.open { transform: translateX(0); }
|
||
|
||
|
||
.nav-header { display: flex; align-items: center; }
|
||
.nav-title {
|
||
font-size: .8rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: .08em;
|
||
color: #94a3b8;
|
||
margin: 8px 8px 4px;
|
||
}
|
||
.nav-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between; /* titolo a sinistra, bottone a destra */
|
||
padding: 0 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.nav-title {
|
||
margin: 0; /* reset margini per allineamento */
|
||
}
|
||
|
||
.refresh-btn {
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: var(--sidebar-fg);
|
||
padding: 4px;
|
||
transition: transform var(--transition), color var(--transition);
|
||
}
|
||
|
||
.refresh-btn:hover {
|
||
transform: rotate(90deg);
|
||
color: var(--focus);
|
||
}
|
||
.edit-btn {
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: var(--sidebar-fg);
|
||
padding: 4px;
|
||
transition: transform var(--transition), color var(--transition);
|
||
}
|
||
|
||
.edit-btn:hover {
|
||
transform: rotate(90deg);
|
||
color: var(--focus);
|
||
}
|
||
.nav-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
|
||
.nav-link {
|
||
display: block;
|
||
width: 100%;
|
||
text-align: left;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
color: var(--sidebar-fg);
|
||
background: rgba(255,255,255,0.02);
|
||
border: 1px solid transparent;
|
||
transition: background var(--transition), transform var(--transition), border-color var(--transition);
|
||
cursor: pointer;
|
||
}
|
||
.nav-link:hover {
|
||
background: rgba(255,255,255,0.06);
|
||
transform: translateX(2px);
|
||
border-color: #263145;
|
||
}
|
||
.nav-link:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: 3px;
|
||
}
|
||
|
||
/* Overlay */
|
||
.overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: var(--overlay-bg);
|
||
backdrop-filter: blur(2px);
|
||
z-index: 80;
|
||
display: none;
|
||
}
|
||
.overlay.show { display: block; }
|
||
|
||
/* Contenuto principale */
|
||
.content {
|
||
margin: 0;
|
||
padding: 0;
|
||
height: 100dvh; /* viewport dinamico, elimina banda nera su mobile */
|
||
}
|
||
.frame-wrap {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
margin: 0;
|
||
display: block;
|
||
background: #fff; /* evita bleed del gradiente */
|
||
}
|
||
|
||
/* Fallback per browser che non supportano 100dvh */
|
||
@supports not (height: 100dvh) {
|
||
.content { height: 100vh; }
|
||
}
|
||
|
||
.frame-error {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
color: #ef4444;
|
||
background: rgba(0,0,0,0.25);
|
||
backdrop-filter: blur(2px);
|
||
text-align: center;
|
||
}
|
||
.frame-error.show { display: flex; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Icona menù -->
|
||
<button id="menuBtn" class="menu-btn" aria-label="Apri menù">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||
xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="2" y="3" width="20" height="2" fill="currentColor"/>
|
||
<rect x="2" y="7" width="20" height="2" fill="currentColor"/>
|
||
<rect x="2" y="11" width="20" height="2" fill="currentColor"/>
|
||
<rect x="2" y="15" width="20" height="2" fill="currentColor"/>
|
||
<rect x="2" y="19" width="20" height="2" fill="currentColor"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- Sidebar
|
||
<aside id="sidebar" class="sidebar" aria-hidden="true">
|
||
<nav class="nav" id="navRoot">
|
||
<h2 class="nav-title">Sites</h2>
|
||
<ul id="siteList" class="nav-list"></ul>
|
||
</nav>
|
||
</aside> -->
|
||
<!-- Sidebar -->
|
||
<aside id="sidebar" class="sidebar" aria-hidden="true">
|
||
<nav class="nav" id="navRoot">
|
||
|
||
<div class="nav-header">
|
||
<h2 class="nav-title">Sites</h2>
|
||
<div class="nav-actions">
|
||
<button id="refreshBtn" class="refresh-btn" aria-label="Ricarica">
|
||
<i class="fas fa-sync-alt"></i>
|
||
</button>
|
||
<button id="editBtn" class="edit-btn" aria-label="Edit">
|
||
<i class="fa-solid fa-gear"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<ul id="siteList" class="nav-list"></ul>
|
||
</nav>
|
||
</aside>
|
||
|
||
<!-- Overlay -->
|
||
<div id="overlay" class="overlay"></div>
|
||
|
||
<!-- Contenuto principale -->
|
||
<main class="content">
|
||
<div class="frame-wrap">
|
||
<iframe id="contentFrame" referrerpolicy="no-referrer"></iframe>
|
||
<div id="frameError" class="frame-error">
|
||
<div>
|
||
<strong>Il sito non può essere caricato in iframe.</strong><br />
|
||
Potrebbe avere X-Frame-Options o Content-Security-Policy che ne impediscono l’incorporamento.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
const menuBtn = document.getElementById('menuBtn');
|
||
const sidebar = document.getElementById('sidebar');
|
||
const overlay = document.getElementById('overlay');
|
||
const siteList = document.getElementById('siteList');
|
||
const iframe = document.getElementById('contentFrame');
|
||
const frameErr = document.getElementById('frameError');
|
||
const navRoot = document.getElementById('navRoot');
|
||
|
||
const refreshBtn = document.getElementById('refreshBtn');
|
||
|
||
|
||
function toggleSidebar() {
|
||
const isOpen = sidebar.classList.toggle('open');
|
||
overlay.classList.toggle('show', isOpen);
|
||
sidebar.setAttribute('aria-hidden', String(!isOpen));
|
||
menuBtn.classList.toggle('hidden', isOpen);
|
||
if (!isOpen) {
|
||
document.activeElement.blur();
|
||
}
|
||
}
|
||
|
||
overlay.addEventListener('click', toggleSidebar);
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && sidebar.classList.contains('open')) toggleSidebar();
|
||
});
|
||
|
||
/* Evita che i click dentro la sidebar propaghino al document (prevenendo chiusure involontarie) */
|
||
navRoot.addEventListener('click', (e) => e.stopPropagation());
|
||
navRoot.addEventListener('mousedown', (e) => e.stopPropagation());
|
||
navRoot.addEventListener('touchstart', (e) => e.stopPropagation(), { passive: false });
|
||
|
||
// Long press drag&drop — isolato SOLO al pulsante menù
|
||
let pressTimer, isDragging = false, offsetX, offsetY, pressActive = false;
|
||
|
||
function startPress(e) {
|
||
e.preventDefault();
|
||
pressActive = true;
|
||
const point = e.touches ? e.touches[0] : e;
|
||
pressTimer = setTimeout(() => {
|
||
isDragging = true;
|
||
menuBtn.classList.add('dragging');
|
||
offsetX = point.clientX - menuBtn.offsetLeft;
|
||
offsetY = point.clientY - menuBtn.offsetTop;
|
||
}, 400);
|
||
/* Attiva listeners di fine solo quando la press parte sul pulsante */
|
||
window.addEventListener('mouseup', endPressOnce);
|
||
window.addEventListener('touchend', endPressOnce);
|
||
window.addEventListener('mousemove', moveBtn);
|
||
window.addEventListener('touchmove', moveBtn, { passive: false });
|
||
}
|
||
|
||
function endPressCore() {
|
||
clearTimeout(pressTimer);
|
||
if (isDragging) {
|
||
isDragging = false;
|
||
menuBtn.classList.remove('dragging');
|
||
} else if (pressActive) {
|
||
// click breve sul pulsante → toggle menù
|
||
toggleSidebar();
|
||
}
|
||
pressActive = false;
|
||
}
|
||
|
||
function endPressOnce(e) {
|
||
endPressCore();
|
||
// pulizia listeners temporanei
|
||
window.removeEventListener('mouseup', endPressOnce);
|
||
window.removeEventListener('touchend', endPressOnce);
|
||
window.removeEventListener('mousemove', moveBtn);
|
||
window.removeEventListener('touchmove', moveBtn);
|
||
}
|
||
|
||
function moveBtn(e) {
|
||
if (!isDragging) return;
|
||
const point = e.touches ? e.touches[0] : e;
|
||
menuBtn.style.left = (point.clientX - offsetX) + 'px';
|
||
menuBtn.style.top = (point.clientY - offsetY) + 'px';
|
||
}
|
||
|
||
menuBtn.addEventListener('mousedown', startPress);
|
||
menuBtn.addEventListener('touchstart', startPress, { passive: false });
|
||
|
||
// Helper per URL pulite
|
||
function joinUrl(base, path) {
|
||
const cleanBase = String(base).replace(/\/+$/,'');
|
||
const cleanPath = String(path).replace(/^\/+/,'');
|
||
return cleanBase + '/' + cleanPath;
|
||
}
|
||
|
||
async function refreshSideBar() {
|
||
//if (doRefresh) {
|
||
try {
|
||
const res = await fetch('/config.json', { cache: 'no-cache' });
|
||
if (!res.ok) throw new Error('config.json non raggiungibile: ' + res.status);
|
||
const cfg = await res.json();
|
||
if (!cfg || !Array.isArray(cfg.s) || !cfg.url) {
|
||
throw new Error('Struttura config non valida. Attesi: { url: string, sites: string[] }');
|
||
}
|
||
siteList.innerHTML = '';
|
||
cfg.s.forEach(site => {
|
||
const li = document.createElement('li');
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.textContent = site.name;
|
||
btn.className = 'nav-link';
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation(); /* il click resta nel menù */
|
||
const target = joinUrl(cfg.url, site.dir);
|
||
openInFrame(target);
|
||
toggleSidebar();
|
||
});
|
||
li.appendChild(btn);
|
||
siteList.appendChild(li);
|
||
});
|
||
//doRefresh =false;
|
||
return cfg;
|
||
} catch (err) {
|
||
//doRefresh =false;
|
||
frameErr.classList.add('show');
|
||
frameErr.innerHTML = '<div><strong>Errore di configurazione.</strong><br />' +
|
||
(err && err.message ? err.message : 'Impossibile caricare config.json') + '</div>';
|
||
}
|
||
//}
|
||
}
|
||
|
||
window.addEventListener("message", (event) => {
|
||
if (event.data === "refreshSideBar") {
|
||
refreshSideBar();
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// Caricamento sites da config.json
|
||
(async function loadConfig() {
|
||
try {
|
||
let cfg = await refreshSideBar();
|
||
|
||
refreshBtn.addEventListener('click', () => {
|
||
// ricarica l’iframe
|
||
iframe.src = iframe.src.split('?')[0] + '?t=' + Date.now();
|
||
toggleSidebar();
|
||
});
|
||
|
||
editBtn.addEventListener('click', () => {
|
||
// carica /config
|
||
const target = joinUrl(cfg.url, "settings");
|
||
openInFrame(target);
|
||
toggleSidebar();
|
||
doRefresh = true;
|
||
});
|
||
|
||
// Sito iniziale
|
||
openInFrame(joinUrl(cfg.url, cfg.s[0].dir));
|
||
|
||
} catch (err) {
|
||
frameErr.classList.add('show');
|
||
frameErr.innerHTML = '<div><strong>Errore di configurazione.</strong><br />' +
|
||
(err && err.message ? err.message : 'Impossibile caricare config.json') + '</div>';
|
||
}
|
||
})();
|
||
|
||
// Carica URL nell’iframe e gestisce casi di blocco
|
||
function openInFrame(url) {
|
||
frameErr.classList.remove('show');
|
||
|
||
// Mixed content
|
||
const pageIsHttps = location.protocol === 'https:';
|
||
const urlIsHttp = /^http:/.test(url);
|
||
if (pageIsHttps && urlIsHttp) {
|
||
frameErr.classList.add('show');
|
||
frameErr.innerHTML = '<div><strong>Contenuto misto bloccato.</strong><br />Pagina HTTPS, sito HTTP. Usa HTTPS.</div>';
|
||
return;
|
||
}
|
||
|
||
// Caricamento con timeout (indicativo di X-Frame-Options/CSP)
|
||
iframe.src = url;
|
||
const timeout = setTimeout(() => {
|
||
frameErr.classList.add('show');
|
||
frameErr.innerHTML = '<div><strong>Il sito non può essere caricato in iframe.</strong><br />Verifica X-Frame-Options/CSP (frame-ancestors).</div>';
|
||
}, 6000);
|
||
|
||
iframe.onload = () => {
|
||
clearTimeout(timeout);
|
||
frameErr.classList.remove('show');
|
||
};
|
||
iframe.onerror = () => {
|
||
clearTimeout(timeout);
|
||
frameErr.classList.add('show');
|
||
frameErr.innerHTML = '<div><strong>Errore di caricamento.</strong><br />Verifica che l’URL sia raggiungibile.</div>';
|
||
};
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|