multi_static_website/home/manifest-editor.html
2025-12-23 13:06:25 +01:00

319 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Editor Manifest PWA</title>
<!-- Link al manifest con id per cache-busting -->
<link rel="manifest" id="manifestLink" href="/manifest.json?v=0">
<meta name="theme-color" content="#0d6efd">
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
fieldset { border: 1px solid #ddd; padding: 1rem; margin-bottom: 1rem; border-radius: 8px; }
label { display: block; margin-top: .5rem; }
input[type="text"], input[type="color"], select, textarea {
width: 100%; max-width: 640px; padding: .5rem; margin-top: .25rem;
}
.icons { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
.icon-card { border: 1px solid #eee; border-radius: 8px; padding: .75rem; background: #fafafa; }
.row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
button { padding: .5rem .75rem; border-radius: 6px; border: 1px solid #ccc; background: #fff; cursor: pointer; }
button.primary { background: #0d6efd; color: #fff; border-color: #0d6efd; }
button.danger { background: #dc3545; color: #fff; border-color: #dc3545; }
.status { margin-top: .5rem; font-size: .95rem; color: #555; }
.thumb { width: 64px; height: 64px; object-fit: contain; border: 1px solid #ddd; background: #fff; border-radius: 6px; }
.meta { font-size: .9rem; color: #333; }
.note { font-size: .85rem; color: #666; }
</style>
</head>
<body>
<h1>Editor Manifest PWA</h1>
<div class="status" id="status">Caricamento manifest...</div>
<form id="manifestForm">
<fieldset>
<legend>Campi principali</legend>
<label>name <input type="text" id="name"></label>
<label>short_name <input type="text" id="short_name"></label>
<label>description <textarea id="description" rows="3"></textarea></label>
<label>start_url <input type="text" id="start_url" placeholder="/"></label>
<label>scope <input type="text" id="scope" placeholder="/"></label>
<label>display
<select id="display">
<option value="standalone">standalone</option>
<option value="fullscreen">fullscreen</option>
<option value="minimal-ui">minimal-ui</option>
<option value="browser">browser</option>
</select>
</label>
<div class="row">
<label style="flex:1">background_color <input type="color" id="background_color" value="#ffffff"></label>
<label style="flex:1">theme_color <input type="color" id="theme_color" value="#0d6efd"></label>
</div>
<label>orientation
<select id="orientation">
<option value="">(nessuna)</option>
<option value="any">any</option>
<option value="natural">natural</option>
<option value="portrait">portrait</option>
<option value="landscape">landscape</option>
<option value="portrait-primary">portrait-primary</option>
<option value="landscape-primary">landscape-primary</option>
</select>
</label>
<label>lang <input type="text" id="lang" placeholder="it"></label>
<label>categories (separate da virgola)
<input type="text" id="categories" placeholder="business, shopping">
</label>
</fieldset>
<fieldset>
<legend>Icone</legend>
<div id="iconsContainer" class="icons"></div>
<h3>Aggiungi nuova icona</h3>
<div class="row">
<input type="file" id="iconFile" name="icon" accept="image/png,image/jpeg,image/webp">
<select id="purpose">
<option value="any">any</option>
<option value="any maskable">any maskable</option>
<option value="maskable">maskable</option>
</select>
<button type="button" id="uploadIconBtn">Carica icona</button>
</div>
<small class="note">
Suggerito: carica unimmagine grande (es. 1024×1024) — il server genererà automaticamente 192×192 e 512×512.
</small>
</fieldset>
<div class="row">
<button type="submit" class="primary">Salva manifest</button>
<button type="button" id="reloadManifestBtn">Ricarica manifest</button>
</div>
</form>
<!--
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
-->
<script>
// --- Setup iniziale ---
//console.log('[manifest] href:', window.location.href);
//console.log('[manifest] search:', window.location.search);
const params = new URLSearchParams(window.location.search);
const dir = params.get('dir')?.replace(/^\/+|\/+$/g, '');
//console.log("dir=",dir);
let manifest = {};
const statusEl = document.getElementById('status');
const iconsContainer = document.getElementById('iconsContainer');
async function loadManifest() {
statusEl.textContent = 'Carico manifest...';
const form = new FormData();
form.append('dir', dir);
const res = await fetch('/api/manifest', {
method: 'POST',
body: form
});
//const res = await fetch('/api/manifest');
manifest = await res.json();
statusEl.textContent = 'Manifest caricato';
// Popola campi
document.getElementById('name').value = manifest.name || '';
document.getElementById('short_name').value = manifest.short_name || '';
document.getElementById('description').value = manifest.description || '';
document.getElementById('start_url').value = manifest.start_url || '/';
document.getElementById('scope').value = manifest.scope || '/';
document.getElementById('display').value = manifest.display || 'standalone';
document.getElementById('background_color').value = manifest.background_color || '#ffffff';
document.getElementById('theme_color').value = manifest.theme_color || '#0d6efd';
document.getElementById('orientation').value = manifest.orientation || '';
document.getElementById('lang').value = manifest.lang || '';
document.getElementById('categories').value = (manifest.categories || []).join(', ');
renderIcons();
}
function renderIcons() {
iconsContainer.innerHTML = '';
const icons = manifest.icons || [];
if (icons.length === 0) {
iconsContainer.innerHTML = '<p class="note">Nessuna icona presente nel manifest.</p>';
return;
}
icons.forEach((icon, idx) => {
const card = document.createElement('div');
card.className = 'icon-card';
const previewSrc = icon.src; // supporta sia /icons/... sia http(s)://...
card.innerHTML = `
<div class="row">
<img class="thumb" src="sites/${dir}/${previewSrc}" alt="${icon.sizes || ''}">
<div class="meta">
<div><strong>src:</strong> ${icon.src}</div>
<div><strong>sizes:</strong> ${icon.sizes || ''}</div>
<div><strong>type:</strong> ${icon.type || ''}</div>
<div><strong>purpose:</strong> ${icon.purpose || ''}</div>
</div>
</div>
<div class="row" style="margin-top:.5rem">
<button type="button" data-idx="${idx}" class="removeIconBtn">Rimuovi dal manifest</button>
<button type="button" data-src="${icon.src}" class="deleteIconFileBtn danger">Elimina file + manifest</button>
</div>
`;
iconsContainer.appendChild(card);
});
// Rimuovi solo dal manifest
document.querySelectorAll('.removeIconBtn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const idx = parseInt(e.target.dataset.idx, 10);
manifest.icons.splice(idx, 1);
try {
await saveManifest(false);
renderIcons();
statusEl.textContent = 'Icona rimossa dal manifest';
bumpManifestLink();
} catch (err) {
console.error(err);
statusEl.textContent = 'Errore rimozione icona dal manifest';
}
});
});
// Elimina file (se sotto /icons) e rimuovi dal manifest lato server
document.querySelectorAll('.deleteIconFileBtn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const src = e.target.dataset.src;
statusEl.textContent = 'Elimino icona...';
try {
const res = await fetch(`/api/icons?src=${encodeURIComponent(src)}&removeFile=true&myDir=${dir}`, {
method: 'DELETE'
});
if (!res.ok) throw new Error('Delete failed');
await loadManifest();
statusEl.textContent = 'Icona eliminata (file + manifest)';
bumpManifestLink();
} catch (err) {
console.error(err);
statusEl.textContent = 'Errore eliminazione icona';
}
});
});
}
async function saveManifest(bump = true) {
// Aggiorna manifest con i valori del form
manifest.name = document.getElementById('name').value;
manifest.short_name = document.getElementById('short_name').value;
manifest.description = document.getElementById('description').value;
manifest.start_url = document.getElementById('start_url').value || '/';
manifest.scope = document.getElementById('scope').value || '/';
manifest.display = document.getElementById('display').value;
manifest.background_color = document.getElementById('background_color').value;
manifest.theme_color = document.getElementById('theme_color').value;
const orientation = document.getElementById('orientation').value;
if (orientation) manifest.orientation = orientation; else delete manifest.orientation;
const lang = document.getElementById('lang').value;
if (lang) manifest.lang = lang; else delete manifest.lang;
const cats = document.getElementById('categories').value
.split(',')
.map(c => c.trim())
.filter(Boolean);
manifest.categories = cats;
const form = new FormData();
form.append('manifest', JSON.stringify(manifest));
form.append('dir', dir);
const res = await fetch('/api/manifest', {
method: 'PUT',
//headers: { 'Content-Type': 'application/json' },
//body: JSON.stringify({manifest , dir})
body: form
});
if (!res.ok) throw new Error('Salvataggio manifest fallito');
statusEl.textContent = 'Manifest salvato';
if (bump) bumpManifestLink();
}
function bumpManifestLink() {
const link = document.getElementById('manifestLink');
const url = new URL(link.href, location.origin);
url.searchParams.set('v', Date.now().toString());
link.href = url.toString();
}
document.getElementById('manifestForm').addEventListener('submit', async (e) => {
e.preventDefault();
try {
await saveManifest(true);
} catch (err) {
console.error(err);
statusEl.textContent = 'Errore salvataggio manifest';
}
});
document.getElementById('reloadManifestBtn').addEventListener('click', loadManifest);
document.getElementById('uploadIconBtn').addEventListener('click', async () => {
const fileInput = document.getElementById('iconFile');
const file = fileInput.files[0];
const purpose = document.getElementById('purpose').value;
if (!file) {
statusEl.textContent = 'Seleziona un file icona';
return;
}
statusEl.textContent = 'Caricamento icona...';
const formData = new FormData();
formData.append('icon', file);
formData.append('purpose', purpose);
formData.append('mydir', dir);
try {
const res = await fetch('/api/upload-icon', { method: 'POST', body: formData });
if (!res.ok) throw new Error('Upload fallito');
const data = await res.json(); // { ok, icons: [...] }
manifest.icons = mergeIcons(manifest.icons || [], data.icons);
await saveManifest(true);
await loadManifest();
statusEl.textContent = 'Icona caricata e manifest aggiornato';
} catch (err) {
console.error(err);
statusEl.textContent = 'Errore upload icona';
}
});
function mergeIcons(existing, added) {
const key = i => `${i.src}|${i.sizes}`;
const map = new Map(existing.map(i => [key(i), i]));
for (const a of added) map.set(key(a), a);
return Array.from(map.values());
}
// Avvio
loadManifest();
</script>
</body>
</html>