110 lines
5.7 KiB
HTML
110 lines
5.7 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Patch-Bilder · MetalCircle</title>
|
||
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
<style>
|
||
.patch-admin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
|
||
.patch-admin-groups { display:grid; gap:12px; }
|
||
.patch-admin-group { padding:12px; background:#0b0b0b; border:1px solid var(--border); border-radius:10px; }
|
||
.patch-admin-group > summary { color:#fca5a5; cursor:pointer; font-size:1.05rem; font-weight:700; }
|
||
.patch-admin-group .patch-admin-grid { margin-top:14px; }
|
||
.patch-admin-card { display: grid; gap: 12px; align-content: start; }
|
||
.patch-preview { width: 140px; height: 140px; object-fit: contain; padding: 4px; background: #242424; border: 2px dashed #78716c; border-radius: 12px; }
|
||
.patch-placeholder { display: grid; place-items: center; font-size: 2.5rem; }
|
||
.patch-upload { display: grid; gap: 8px; }
|
||
.patch-upload-status { min-height: 1.2em; color: var(--muted); font-size: .85rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>{% include '_user_menu.html' %}</div></header>
|
||
<main>
|
||
<div class="page-title"><h1>🧵 Patch-Bilder</h1><p>Grafiken für die Patches auf der virtuellen Kutte verwalten.</p></div>
|
||
{% set admin_section = 'patches' %}{% include '_admin_nav.html' %}
|
||
<section class="admin-section">
|
||
<div class="patch-admin-groups">
|
||
{% for group in patch_groups %}
|
||
<details class="patch-admin-group">
|
||
<summary>{{ group.label }} · {{ group.patches|length }}</summary>
|
||
<div class="patch-admin-grid">
|
||
{% for patch in group.patches %}
|
||
<article class="admin-row patch-admin-card">
|
||
{% if patch.image_path %}<img class="patch-preview" src="{{ patch.image_path }}" alt="{{ patch.name }}">{% else %}<div class="patch-preview patch-placeholder">{{ patch.icon }}</div>{% endif %}
|
||
<div><strong>{{ patch.name }}</strong><div class="admin-meta">{{ patch.description }} · {{ patch.code }}</div></div>
|
||
<form class="patch-upload" method="post" action="/admin/patches/{{ patch.code }}" enctype="multipart/form-data">
|
||
<input type="file" name="image" accept="image/jpeg,image/png,image/webp" required>
|
||
<span class="patch-upload-status">Das Bild wird vor dem Upload automatisch optimiert.</span>
|
||
<button class="button" type="submit">{% if patch.image_path %}Bild ersetzen{% else %}Bild hochladen{% endif %}</button>
|
||
</form>
|
||
{% if patch.image_path %}<form method="post" action="/admin/patches/{{ patch.code }}/delete" onsubmit="return confirm('Patch-Bild entfernen und wieder das Symbol verwenden?');"><button class="admin-danger" type="submit">Bild entfernen</button></form>{% endif %}
|
||
</article>
|
||
{% endfor %}
|
||
</div>
|
||
</details>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<script>
|
||
const patchForms = document.querySelectorAll(".patch-upload");
|
||
|
||
async function optimizePatchImage(file) {
|
||
const bitmap = await createImageBitmap(file);
|
||
const maxSide = 800;
|
||
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
||
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
||
const context = canvas.getContext("2d");
|
||
context.imageSmoothingEnabled = true;
|
||
context.imageSmoothingQuality = "high";
|
||
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||
bitmap.close();
|
||
|
||
let quality = 0.9;
|
||
let blob;
|
||
do {
|
||
blob = await new Promise(resolve => canvas.toBlob(resolve, "image/webp", quality));
|
||
quality -= 0.1;
|
||
} while (blob && blob.size > 700 * 1024 && quality >= 0.5);
|
||
|
||
if (!blob) {
|
||
throw new Error("Bild konnte nicht verarbeitet werden");
|
||
}
|
||
return new File([blob], "patch.webp", {type: "image/webp"});
|
||
}
|
||
|
||
patchForms.forEach(form => {
|
||
form.addEventListener("submit", async event => {
|
||
if (form.dataset.optimized === "true") return;
|
||
event.preventDefault();
|
||
const input = form.querySelector('input[type="file"]');
|
||
const status = form.querySelector(".patch-upload-status");
|
||
const button = form.querySelector('button[type="submit"]');
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
|
||
button.disabled = true;
|
||
status.textContent = "Bild wird optimiert …";
|
||
try {
|
||
const optimized = await optimizePatchImage(file);
|
||
const transfer = new DataTransfer();
|
||
transfer.items.add(optimized);
|
||
input.files = transfer.files;
|
||
form.dataset.optimized = "true";
|
||
status.textContent = `Optimiert: ${Math.ceil(optimized.size / 1024)} KB – wird hochgeladen …`;
|
||
form.requestSubmit();
|
||
} catch (error) {
|
||
console.error("Patch image optimization failed:", error);
|
||
status.textContent = "Das Bild konnte nicht optimiert werden. Bitte ein kleineres JPG, PNG oder WebP wählen.";
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|