Initial commit: What To Cook (WTC) v4.4 - Digitales Kochbuch, Wochenmenueplan & Buchdruck

This commit is contained in:
2026-10-14 20:24:37 +02:00
commit c01d2d7639
171 changed files with 14073 additions and 0 deletions
+695
View File
@@ -0,0 +1,695 @@
/**
* What To Cook? - Frontend Application
*/
// Category icons mapping
const CATEGORY_ICONS = {
"Reis": "🍚",
"Nudeln": "🍝",
"Kartoffeln": "🥔",
"Teigig": "🍕",
"asiatisch": "🥢",
"Hausmannskost": "🥩",
"Suppen": "🍲",
"Eintöpfe": "🥘",
"Süßspeisen": "🥞",
"Fisch": "🐟"
};
const CATEGORIES = [
"Reis",
"Nudeln",
"Kartoffeln",
"Teigig",
"asiatisch",
"Hausmannskost",
"Suppen",
"Eintöpfe",
"Süßspeisen",
"Fisch"
];
const EFFORTS = ["gering", "mittel", "hoch"];
function getDishCategories(dish) {
if (!dish) return [];
if (Array.isArray(dish.categories) && dish.categories.length > 0) {
return dish.categories;
}
if (dish.category) {
return dish.category.split(",").map(c => c.trim()).filter(Boolean);
}
return [];
}
function renderCategoryBadges(dish) {
const cats = getDishCategories(dish);
if (cats.length === 0) return "";
return cats.map(cat => {
const icon = CATEGORY_ICONS[cat] || "🍲";
return `<span class="badge-tag badge-category"><span>${icon}</span> ${cat}</span>`;
}).join("");
}
// Application State
const state = {
activeTab: "generator",
slots: [
{ slot_number: 1, active: true, category: "", efforts: [], current_dish: null },
{ slot_number: 2, active: true, category: "", efforts: [], current_dish: null },
{ slot_number: 3, active: false, category: "", efforts: [], current_dish: null },
{ slot_number: 4, active: false, category: "", efforts: [], current_dish: null },
{ slot_number: 5, active: false, category: "", efforts: [], current_dish: null },
],
admin: {
dishes: [],
search: "",
category: "alle",
effort: "alle",
deleteTargetId: null
}
};
// --- Initialization ---
document.addEventListener("DOMContentLoaded", () => {
initTabs();
renderSlots();
populateDropdowns();
initGeneratorEvents();
initAdminEvents();
loadAdminDishes();
});
// --- Tab Navigation ---
function initTabs() {
const btnGenerator = document.getElementById("tab-btn-generator");
const btnAdmin = document.getElementById("tab-btn-admin");
const panelGenerator = document.getElementById("tab-generator");
const panelAdmin = document.getElementById("tab-admin");
btnGenerator.addEventListener("click", () => switchTab("generator"));
btnAdmin.addEventListener("click", () => switchTab("admin"));
function switchTab(tabName) {
state.activeTab = tabName;
if (tabName === "generator") {
btnGenerator.classList.add("active");
btnAdmin.classList.remove("active");
panelGenerator.classList.add("active");
panelAdmin.classList.remove("active");
panelAdmin.hidden = true;
panelGenerator.hidden = false;
} else {
btnAdmin.classList.add("active");
btnGenerator.classList.remove("active");
panelAdmin.classList.add("active");
panelGenerator.classList.remove("active");
panelGenerator.hidden = true;
panelAdmin.hidden = false;
loadAdminDishes();
}
}
}
// --- Populate Category Dropdowns ---
function populateDropdowns() {
const adminCatSelect = document.getElementById("admin-filter-category");
const modalCatGroup = document.getElementById("modal-category-group");
if (modalCatGroup) {
modalCatGroup.innerHTML = "";
}
CATEGORIES.forEach(cat => {
const icon = CATEGORY_ICONS[cat] || "🍲";
// Admin filter
if (adminCatSelect) {
const optFilter = document.createElement("option");
optFilter.value = cat;
optFilter.textContent = `${icon} ${cat}`;
adminCatSelect.appendChild(optFilter);
}
// Modal checkbox pill
if (modalCatGroup) {
const label = document.createElement("label");
label.className = "category-checkbox-label";
label.innerHTML = `
<input type="checkbox" name="modal-category" value="${cat}">
<span class="cat-pill">
<span>${icon}</span> ${cat}
</span>
`;
modalCatGroup.appendChild(label);
}
});
}
// --- Generator Slots Rendering ---
function renderSlots() {
const container = document.getElementById("slots-container");
container.innerHTML = "";
state.slots.forEach(slot => {
const card = document.createElement("div");
card.className = `slot-card ${slot.active ? "active" : "inactive"}`;
card.id = `slot-card-${slot.slot_number}`;
card.innerHTML = `
<div class="slot-card-header">
<div class="slot-title-group">
<span class="slot-number-badge">${slot.slot_number}</span>
<span class="slot-name">Gericht ${slot.slot_number}</span>
</div>
<label class="slot-toggle-label">
<span>${slot.active ? "Aktiv" : "Freigelassen"}</span>
<label class="switch">
<input type="checkbox" id="slot-toggle-${slot.slot_number}" ${slot.active ? "checked" : ""}>
<span class="slider"></span>
</label>
</label>
</div>
<div class="slot-body">
<div class="form-group">
<label for="slot-cat-${slot.slot_number}" class="form-label">Kategorie:</label>
<select id="slot-cat-${slot.slot_number}" class="custom-select">
<option value="" ${slot.category === "" ? "selected" : ""}>🎲 Beliebig (Alle Kategorien)</option>
${CATEGORIES.map(cat => {
const icon = CATEGORY_ICONS[cat] || "🍲";
return `<option value="${cat}" ${slot.category === cat ? "selected" : ""}>${icon} ${cat}</option>`;
}).join("")}
</select>
</div>
<div class="form-group">
<label class="form-label">Aufwand (Mehrfachauswahl möglich):</label>
<div class="effort-chips-group" id="effort-chips-${slot.slot_number}">
<button type="button" class="effort-chip ${slot.efforts.includes("gering") ? "selected" : ""}" data-effort="gering">
<span>🟢</span> Gering
</button>
<button type="button" class="effort-chip ${slot.efforts.includes("mittel") ? "selected" : ""}" data-effort="mittel">
<span>🟡</span> Mittel
</button>
<button type="button" class="effort-chip ${slot.efforts.includes("hoch") ? "selected" : ""}" data-effort="hoch">
<span>🔴</span> Hoch
</button>
</div>
</div>
</div>
`;
container.appendChild(card);
// Event: Toggle Active
const toggleInput = card.querySelector(`#slot-toggle-${slot.slot_number}`);
toggleInput.addEventListener("change", (e) => {
slot.active = e.target.checked;
renderSlots();
updateActiveCounter();
});
// Event: Category Change
const catSelect = card.querySelector(`#slot-cat-${slot.slot_number}`);
catSelect.addEventListener("change", (e) => {
slot.category = e.target.value;
});
// Event: Effort Multi-Select Chips
const chips = card.querySelectorAll(`#effort-chips-${slot.slot_number} .effort-chip`);
chips.forEach(chip => {
chip.addEventListener("click", () => {
const effortVal = chip.getAttribute("data-effort");
const idx = slot.efforts.indexOf(effortVal);
if (idx > -1) {
slot.efforts.splice(idx, 1);
chip.classList.remove("selected");
} else {
slot.efforts.push(effortVal);
chip.classList.add("selected");
}
});
});
});
updateActiveCounter();
}
function updateActiveCounter() {
const activeCount = state.slots.filter(s => s.active).length;
const countEl = document.getElementById("active-slots-count");
if (countEl) countEl.textContent = activeCount;
const btnHero = document.getElementById("btn-roll-all");
if (btnHero) {
const btnText = btnHero.querySelector(".btn-text");
if (activeCount === 0) {
btnText.textContent = "Bitte mindestens 1 Gericht aktivieren";
btnHero.disabled = true;
} else if (activeCount === 1) {
btnText.textContent = "1 Gericht auslosen";
btnHero.disabled = false;
} else {
btnText.textContent = `${activeCount} Gerichte auslosen`;
btnHero.disabled = false;
}
}
}
// --- Generator Events (Roll Logic) ---
function initGeneratorEvents() {
const btnRollAll = document.getElementById("btn-roll-all");
const btnRerollAll = document.getElementById("btn-reroll-all");
const btnBack = document.getElementById("btn-back-to-config");
btnRollAll.addEventListener("click", () => executeRoll(false));
btnRerollAll.addEventListener("click", () => executeRoll(true));
btnBack.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
}
async function executeRoll(isReroll = false) {
const activeSlots = state.slots.filter(s => s.active);
if (activeSlots.length === 0) {
showToast("Bitte aktiviere mindestens 1 Gericht.", "error");
return;
}
const btnRollAll = document.getElementById("btn-roll-all");
const btnRerollAll = document.getElementById("btn-reroll-all");
btnRollAll.disabled = true;
btnRerollAll.disabled = true;
try {
const payload = {
slots: activeSlots.map(s => ({
slot_number: s.slot_number,
category: s.category || null,
efforts: s.efforts.length > 0 ? s.efforts : null,
current_dish_id: isReroll && s.current_dish ? s.current_dish.id : null
}))
};
const res = await fetch("/api/roll", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || "Fehler beim Auslosen.");
}
// Update slots state with rolled dishes
data.results.forEach(resItem => {
const slot = state.slots.find(s => s.slot_number === resItem.slot_number);
if (slot) {
slot.current_dish = resItem.dish;
}
});
renderResults(data.results);
showToast(isReroll ? "Alle Gerichte neu ausgelost!" : "Zufallsauswahl erfolgreich!", "success");
// Scroll smoothly to results
const resultsSection = document.getElementById("results-section");
resultsSection.classList.remove("hidden");
resultsSection.scrollIntoView({ behavior: "smooth", block: "start" });
} catch (err) {
showToast(err.message, "error");
} finally {
btnRollAll.disabled = false;
btnRerollAll.disabled = false;
}
}
// Render Results Grid
function renderResults(results) {
const container = document.getElementById("results-list");
const countBadge = document.getElementById("results-count-badge");
container.innerHTML = "";
const validDishes = results.filter(r => r.dish !== null);
countBadge.textContent = `${validDishes.length} ${validDishes.length === 1 ? "Gericht" : "Gerichte"}`;
results.forEach(item => {
const slot = state.slots.find(s => s.slot_number === item.slot_number);
const dish = item.dish;
const card = document.createElement("div");
card.className = "result-card";
card.id = `result-card-${item.slot_number}`;
if (!dish) {
card.innerHTML = `
<div class="result-card-top">
<span class="result-slot-label">Gericht ${item.slot_number}</span>
</div>
<div class="result-dish-name" style="color: var(--text-dim);">
⚠️ Keine passenden Gerichte gefunden
</div>
<p style="font-size: 0.85rem; color: var(--text-muted);">
Für die gewählte Kombination (Kategorie / Aufwand) gibt es noch keine Einträge.
</p>
`;
container.appendChild(card);
return;
}
const effortLabel = dish.effort.charAt(0).toUpperCase() + dish.effort.slice(1);
card.innerHTML = `
<div class="result-card-top">
<span class="result-slot-label">Gericht ${item.slot_number}</span>
</div>
<div class="result-dish-name" id="dish-name-${item.slot_number}">${dish.name}</div>
<div class="result-tags">
${renderCategoryBadges(dish)}
<span class="badge-tag badge-effort-${dish.effort}">
${effortLabel}
</span>
</div>
<div class="result-card-actions">
<button class="btn-reroll-single" data-slot="${item.slot_number}" title="Nur dieses Gericht neu würfeln">
<span class="dice-icon">🎲</span> Neu würfeln
</button>
</div>
`;
container.appendChild(card);
// Event: Single Reroll Button
const btnRerollSingle = card.querySelector(".btn-reroll-single");
btnRerollSingle.addEventListener("click", () => rerollSingleSlot(item.slot_number, btnRerollSingle, card));
});
}
// --- Single Slot Reroll Logic ---
async function rerollSingleSlot(slotNumber, btnElement, cardElement) {
const slot = state.slots.find(s => s.slot_number === slotNumber);
if (!slot || !slot.current_dish) return;
btnElement.classList.add("spinning");
cardElement.classList.add("rerolling");
// Collect other dishes currently on screen so we don't duplicate
const otherDishIds = state.slots
.filter(s => s.active && s.slot_number !== slotNumber && s.current_dish)
.map(s => s.current_dish.id);
try {
const payload = {
slot_number: slotNumber,
category: slot.category || null,
efforts: slot.efforts.length > 0 ? slot.efforts : null,
current_dish_id: slot.current_dish.id,
other_dish_ids: otherDishIds
};
const res = await fetch("/api/roll-single", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Fehler beim Neu-Würfeln.");
if (data.status === "ok" && data.dish) {
slot.current_dish = data.dish;
const effortLabel = data.dish.effort.charAt(0).toUpperCase() + data.dish.effort.slice(1);
// Smoothly update the card content
const nameEl = cardElement.querySelector(`#dish-name-${slotNumber}`);
if (nameEl) nameEl.textContent = data.dish.name;
const tagsEl = cardElement.querySelector(".result-tags");
if (tagsEl) {
tagsEl.innerHTML = `
${renderCategoryBadges(data.dish)}
<span class="badge-tag badge-effort-${data.dish.effort}">
${effortLabel}
</span>
`;
}
showToast(`Gericht ${slotNumber} neu gewürfelt: ${data.dish.name}`, "success");
} else {
showToast("Kein anderes passendes Gericht in dieser Kategorie gefunden.", "error");
}
} catch (err) {
showToast(err.message, "error");
} finally {
setTimeout(() => {
btnElement.classList.remove("spinning");
cardElement.classList.remove("rerolling");
}, 450);
}
}
// --- Admin Section & CRUD ---
function initAdminEvents() {
const searchInput = document.getElementById("admin-search-input");
const clearSearchBtn = document.getElementById("admin-search-clear");
const catFilter = document.getElementById("admin-filter-category");
const effortFilter = document.getElementById("admin-filter-effort");
searchInput.addEventListener("input", (e) => {
state.admin.search = e.target.value.trim();
clearSearchBtn.classList.toggle("hidden", state.admin.search.length === 0);
loadAdminDishes();
});
clearSearchBtn.addEventListener("click", () => {
searchInput.value = "";
state.admin.search = "";
clearSearchBtn.classList.add("hidden");
loadAdminDishes();
});
catFilter.addEventListener("change", (e) => {
state.admin.category = e.target.value;
loadAdminDishes();
});
effortFilter.addEventListener("change", (e) => {
state.admin.effort = e.target.value;
loadAdminDishes();
});
// Modal Open/Close
const dishModal = document.getElementById("dish-modal");
const btnOpenAdd = document.getElementById("btn-open-add-dish");
const btnCloseModal = document.getElementById("btn-close-modal");
const btnCancelModal = document.getElementById("btn-cancel-modal");
const dishForm = document.getElementById("dish-form");
btnOpenAdd.addEventListener("click", () => {
openDishModal();
});
btnCloseModal.addEventListener("click", () => dishModal.close());
btnCancelModal.addEventListener("click", () => dishModal.close());
dishForm.addEventListener("submit", async (e) => {
e.preventDefault();
await saveDishFromModal();
});
// Delete Modal
const deleteModal = document.getElementById("delete-modal");
const btnCancelDelete = document.getElementById("btn-cancel-delete");
const btnConfirmDelete = document.getElementById("btn-confirm-delete");
btnCancelDelete.addEventListener("click", () => deleteModal.close());
btnConfirmDelete.addEventListener("click", async () => {
if (state.admin.deleteTargetId) {
await executeDeleteDish(state.admin.deleteTargetId);
deleteModal.close();
}
});
}
async function loadAdminDishes() {
const listContainer = document.getElementById("admin-dishes-list");
const statsSummary = document.getElementById("admin-stats-summary");
const countLabel = document.getElementById("dishes-count-label");
try {
const params = new URLSearchParams();
if (state.admin.category && state.admin.category !== "alle") {
params.append("category", state.admin.category);
}
if (state.admin.effort && state.admin.effort !== "alle") {
params.append("effort", state.admin.effort);
}
if (state.admin.search) {
params.append("search", state.admin.search);
}
const res = await fetch(`/api/dishes?${params.toString()}`);
const dishes = await res.json();
state.admin.dishes = dishes;
// Load overall stats
const statsRes = await fetch("/api/stats");
const stats = await statsRes.json();
statsSummary.textContent = `${stats.total} Gerichte in der Datenbank`;
countLabel.textContent = `${dishes.length} ${dishes.length === 1 ? "Gericht" : "Gerichte"} angezeigt`;
if (dishes.length === 0) {
listContainer.innerHTML = `
<div class="loading-spinner">Keine Gerichte mit diesen Suchkriterien gefunden.</div>
`;
return;
}
listContainer.innerHTML = "";
dishes.forEach(dish => {
const row = document.createElement("div");
row.className = "admin-dish-row";
const effortLabel = dish.effort.charAt(0).toUpperCase() + dish.effort.slice(1);
row.innerHTML = `
<div class="admin-dish-info">
<div class="admin-dish-name">${dish.name}</div>
<div class="admin-dish-badges">
${renderCategoryBadges(dish)}
<span class="badge-tag badge-effort-${dish.effort}">${effortLabel}</span>
</div>
</div>
<div class="admin-dish-actions">
<button class="btn-action-icon btn-edit" title="Bearbeiten" data-id="${dish.id}">✏️</button>
<button class="btn-action-icon btn-delete" title="Löschen" data-id="${dish.id}">🗑️</button>
</div>
`;
listContainer.appendChild(row);
row.querySelector(".btn-edit").addEventListener("click", () => openDishModal(dish));
row.querySelector(".btn-delete").addEventListener("click", () => confirmDeleteDish(dish));
});
} catch (err) {
listContainer.innerHTML = `<div class="loading-spinner" style="color: var(--danger);">Fehler beim Laden: ${err.message}</div>`;
}
}
function openDishModal(dish = null) {
const modal = document.getElementById("dish-modal");
const modalTitle = document.getElementById("modal-title");
const formId = document.getElementById("dish-form-id");
const nameInput = document.getElementById("dish-name-input");
const catCheckboxes = document.querySelectorAll('input[name="modal-category"]');
if (dish) {
modalTitle.textContent = "Gericht bearbeiten";
formId.value = dish.id;
nameInput.value = dish.name;
const cats = getDishCategories(dish);
catCheckboxes.forEach(cb => {
cb.checked = cats.includes(cb.value);
});
const radio = document.querySelector(`input[name="modal-effort"][value="${dish.effort}"]`);
if (radio) radio.checked = true;
} else {
modalTitle.textContent = "Neues Gericht anlegen";
formId.value = "";
nameInput.value = "";
catCheckboxes.forEach(cb => {
cb.checked = false;
});
const radio = document.querySelector(`input[name="modal-effort"][value="mittel"]`);
if (radio) radio.checked = true;
}
modal.showModal();
}
async function saveDishFromModal() {
const modal = document.getElementById("dish-modal");
const formId = document.getElementById("dish-form-id").value;
const name = document.getElementById("dish-name-input").value.trim();
const selectedCats = Array.from(document.querySelectorAll('input[name="modal-category"]:checked')).map(cb => cb.value);
const effortRadio = document.querySelector('input[name="modal-effort"]:checked');
const effort = effortRadio ? effortRadio.value : "mittel";
if (!name) {
showToast("Bitte gib einen Namen für das Gericht ein.", "error");
return;
}
if (selectedCats.length === 0) {
showToast("Bitte wähle mindestens eine Kategorie aus.", "error");
return;
}
if (!effort) {
showToast("Bitte wähle den Aufwand aus.", "error");
return;
}
const isEdit = !!formId;
const url = isEdit ? `/api/dishes/${formId}` : "/api/dishes";
const method = isEdit ? "PUT" : "POST";
try {
const res = await fetch(url, {
method: method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, categories: selectedCats, effort })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Fehler beim Speichern.");
showToast(isEdit ? "Gericht aktualisiert!" : "Gericht erfolgreich hinzugefügt!", "success");
modal.close();
loadAdminDishes();
} catch (err) {
showToast(err.message, "error");
}
}
function confirmDeleteDish(dish) {
state.admin.deleteTargetId = dish.id;
const deleteModal = document.getElementById("delete-modal");
const textEl = document.getElementById("delete-modal-text");
textEl.innerHTML = `Möchtest du <strong>"${dish.name}"</strong> wirklich löschen?`;
deleteModal.showModal();
}
async function executeDeleteDish(dishId) {
try {
const res = await fetch(`/api/dishes/${dishId}`, { method: "DELETE" });
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Fehler beim Löschen.");
showToast("Gericht wurde gelöscht.", "success");
loadAdminDishes();
} catch (err) {
showToast(err.message, "error");
}
}
// --- Toast System ---
function showToast(message, type = "info") {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
const icon = type === "success" ? "✅" : (type === "error" ? "⚠️" : "ℹ️");
toast.innerHTML = `<span>${icon}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.style.transition = "opacity 0.3s, transform 0.3s";
toast.style.opacity = "0";
toast.style.transform = "translateY(-10px)";
setTimeout(() => toast.remove(), 300);
}, 3500);
}
+2805
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
../style.css
+1
View File
@@ -0,0 +1 @@
../style.css
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

+1
View File
@@ -0,0 +1 @@
../app.js
+1
View File
@@ -0,0 +1 @@
../app.js
+21
View File
@@ -0,0 +1,21 @@
{
"name": "What To Cook? – Essens-Zufallsgenerator",
"short_name": "WhatToCook",
"description": "Zufallsgenerator für Essen mit Kategorien und Aufwandsfilter",
"start_url": "/",
"display": "standalone",
"background_color": "#0f141c",
"theme_color": "#ff6b35",
"icons": [
{
"src": "/static/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
+1085
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB