319 lines
12 KiB
HTML
319 lines
12 KiB
HTML
|
||
<!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 un’immagine 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>
|