Compare commits

...
11 Commits
13 changed files with 624 additions and 112 deletions
+3
View File
@@ -15,3 +15,6 @@ venv/
# Dependencies (just in case they get added later)
node_modules/
package-lock.json
# Environment variables
.env
+72 -18
View File
@@ -202,7 +202,7 @@ document.addEventListener('DOMContentLoaded', () => {
const response = await fetch('feedback_backend.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', id: id, secret: '1234' })
body: JSON.stringify({ action: 'delete', id: id, secret: sessionStorage.getItem('schnappix_admin_secret') || '' })
});
const result = await response.json();
if (result.success) {
@@ -240,7 +240,7 @@ document.addEventListener('DOMContentLoaded', () => {
const response = await fetch('feedback_backend.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'answer', id: id, answer: answer, secret: '1234' })
body: JSON.stringify({ action: 'answer', id: id, answer: answer, secret: sessionStorage.getItem('schnappix_admin_secret') || '' })
});
const result = await response.json();
if (result.success) {
@@ -363,6 +363,49 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// --- Tester Registration Logic ---
const testerForm = document.getElementById('tester-form');
if (testerForm) {
testerForm.addEventListener('submit', async (e) => {
e.preventDefault();
const emailInput = document.getElementById('tester-email');
const submitBtn = document.getElementById('tester-submit-btn');
const successMsg = document.getElementById('tester-success-msg');
const errorMsg = document.getElementById('tester-error-msg');
submitBtn.disabled = true;
submitBtn.textContent = 'Wird gesendet...';
successMsg.style.display = 'none';
errorMsg.style.display = 'none';
try {
const response = await fetch('tester_backend.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: emailInput.value })
});
const result = await response.json();
if (result.success) {
successMsg.style.display = 'block';
testerForm.reset();
} else {
errorMsg.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> ' + (result.error || 'Unbekannter Fehler');
errorMsg.style.display = 'block';
}
} catch (err) {
console.error(err);
errorMsg.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> Netzwerkfehler. Bitte später erneut versuchen.';
errorMsg.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Als Tester anmelden';
}
});
}
// --- Modal Management for Impressum & Privacy ---
const modalTriggers = document.querySelectorAll('.legal-trigger');
const modals = document.querySelectorAll('.modal-overlay');
@@ -424,26 +467,37 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (adminLoginForm) {
adminLoginForm.addEventListener('submit', (e) => {
adminLoginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const password = adminPasswordInput.value;
// Admin password matching the default app settings: "1234"
if (password === '1234') {
isAdmin = true;
sessionStorage.setItem('schnappix_is_admin', 'true');
adminPasswordInput.value = '';
adminLoginError.style.display = 'none';
try {
const response = await fetch('feedback_backend.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'verify_password', secret: password })
});
// Switch Modals
adminLoginModal.classList.remove('open');
adminPanelModal.classList.add('open');
// Re-render to show trash cans
renderFeedback(getActiveFilter());
} else {
if (response.ok) {
isAdmin = true;
sessionStorage.setItem('schnappix_is_admin', 'true');
sessionStorage.setItem('schnappix_admin_secret', password);
adminPasswordInput.value = '';
adminLoginError.style.display = 'none';
// Switch Modals
adminLoginModal.classList.remove('open');
adminPanelModal.classList.add('open');
// Re-render to show trash cans
renderFeedback(getActiveFilter());
} else {
adminLoginError.style.display = 'block';
adminPasswordInput.value = '';
}
} catch (err) {
console.error(err);
adminLoginError.style.display = 'block';
adminPasswordInput.value = '';
adminPasswordInput.focus();
}
});
}
+10 -2
View File
@@ -24,6 +24,7 @@
<a href="upload.html">Upload</a>
<a href="faq.html">FAQ</a>
<a href="feedback.html">Feedback Hub</a>
<a href="tester.html">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
@@ -43,6 +44,9 @@
<h4>Allgemeine Hinweise</h4>
<p>Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.</p>
<h4>Verantwortliche Stelle</h4>
<p>Verantwortlich für die Datenverarbeitung auf dieser Website ist die im Impressum genannte Person.</p>
<h3>2. Datenerfassung auf unserer Website</h3>
<h4>Cookies & Local Storage</h4>
<p>Diese Webseite nutzt den lokalen Speicher (Local Storage) Ihres Browsers, um die von Ihnen geschriebenen Feedbacks sowie Ihre Stimmen (Upvotes) zu speichern. Dies geschieht ausschließlich auf Ihrem Endgerät und wird nicht an externe Server übertragen, es sei denn, Sie senden explizit ein Feedback ab.</p>
@@ -50,8 +54,12 @@
<h4>Server-Logfiles</h4>
<p>Der Provider der Seiten erhebt und speichert automatisch Informationen in sogenannten Server-Logfiles, die Ihr Browser automatisch an uns übermittelt. Dies sind Browsertyp, Betriebssystem, Referrer URL, Hostname des zugreifenden Rechners und Uhrzeit der Serveranfrage. Diese Daten sind nicht bestimmten Personen zuzuordnen.</p>
<h3>3. Feedback Hub</h3>
<p>Wenn Sie den Feedback Hub nutzen, werden Ihre Eingaben (Name, Titel, Text, Kategorie) gespeichert, um die Funktionalität der Feedback-Plattform bereitzustellen. Bitte geben Sie keine sensiblen personenbezogenen Daten im Freitextfeld an.</p>
<h3>3. Feedback Hub & Uploads</h3>
<p><strong>Feedback Hub:</strong> Wenn Sie den Feedback Hub nutzen, werden Ihre Eingaben (Name, Titel, Text, Kategorie) gespeichert, um die Funktionalität der Feedback-Plattform bereitzustellen. Bitte geben Sie keine sensiblen personenbezogenen Daten im Freitextfeld an.</p>
<p><strong>Upload-Funktion:</strong> Wenn Sie Bilddateien (Rahmen) über die Upload-Funktion hochladen, werden diese gespeichert. Nach erfolgreicher Prüfung werden diese Rahmen öffentlich in die App integriert und sind somit für alle Nutzer der App sichtbar. Bitte stellen Sie sicher, dass Sie keine privaten Bilder oder urheberrechtlich geschützten Inhalte ohne entsprechende Rechte hochladen.</p>
<h3>4. Ihre Rechte</h3>
<p>Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder Löschung dieser Daten zu verlangen. Hierzu sowie zu weiteren Fragen zum Thema Datenschutz können Sie sich jederzeit an uns unter der im Impressum angegebenen E-Mail-Adresse wenden. Des Weiteren steht Ihnen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu.</p>
</div>
</div>
</div>
+217 -65
View File
@@ -1,17 +1,22 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="icon" type="image/png" href="favicon.png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schnappix - Häufig gestellte Fragen (FAQ)</title>
<meta name="description" content="Antworten auf Fragen zum Kiosk-Modus, IPP-Drucker-Einrichtung, Fotorahmen-Konfiguration und Foto-Speicherung.">
<meta name="description"
content="Antworten auf Fragen zum Kiosk-Modus, IPP-Drucker-Einrichtung, Fotorahmen-Konfiguration und Foto-Speicherung.">
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@500;700;800&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<!-- Header Navigation -->
@@ -25,6 +30,7 @@
<a href="upload.html">Upload</a>
<a href="faq.html" class="active-nav">FAQ</a>
<a href="feedback.html">Feedback Hub</a>
<a href="tester.html">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
@@ -36,8 +42,10 @@
<!-- Page Title -->
<div style="padding: 140px 0 20px;">
<div class="container">
<h1 class="section-title" style="margin-bottom: 20px;">Häufig gestellte <span class="text-gradient">Fragen</span></h1>
<p style="text-align: center; color: var(--text-muted); max-width: 600px; margin: 0 auto 40px;">Alles Wissenswerte zur Einrichtung, Hardware-Kompatibilität und Anpassung der Photobooth App.</p>
<h1 class="section-title" style="margin-bottom: 20px;">Häufig gestellte <span
class="text-gradient">Fragen</span></h1>
<p style="text-align: center; color: var(--text-muted); max-width: 600px; margin: 0 auto 40px;">Alles
Wissenswerte zur Einrichtung, Hardware-Kompatibilität und Anpassung der Schnappix App.</p>
</div>
</div>
@@ -45,59 +53,17 @@
<section class="faq-section" style="padding-top: 20px;">
<div class="container faq-container">
<div class="accordion">
<!-- Voraussetzungen & Kompatibilität -->
<div class="accordion-item">
<button class="accordion-header">
<span>Wie aktiviere ich den Kiosk-Modus auf dem Tablet?</span>
<span>Ist die App auch für Smartphones geeignet?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Damit der Kiosk-Modus (LockTask) ohne Bestätigungsdialoge funktioniert, muss die App als <strong>Device Owner</strong> festgelegt werden. Führe dazu folgenden ADB-Befehl aus:</p>
<code>adb shell dpm set-device-owner de.orfel.schnappix/de.orfel.schnappix.KioskDeviceAdminReceiver</code>
<p>Danach kannst du den Kiosk-Modus einfach im passwortgeschützten Admin-Bereich ein- und ausschalten.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Wie füge ich eigene Fotorahmen hinzu?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Erstelle ein transparentes PNG im Format <strong>1500 x 1000 px</strong> (3:2 Querformat, max. 500 KB) und lege es im Ordner <code>assets/frames/</code> als <code>frame_name.png</code> ab. Trage das Template anschließend in <code>src/data/frames.ts</code> ein und führe einen neuen Build aus.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Welche Drucker werden unterstützt?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Es werden alle Drucker unterstützt, die das <strong>IPP (Internet Printing Protocol)</strong> im lokalen Netzwerk unterstützen. Beliebte Fotodrucker wie der Canon CP1300 lassen sich direkt über ihre lokale IP-Adresse ansteuern.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Wo werden die Fotos gespeichert?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Schnappix nutzt die moderne Android MediaStore API und speichert Collagen sowie Originale direkt in der Galerie deines Tablets unter den Ordnern:</p>
<ul>
<li><code>Schnappix - &lt;EventName&gt; - Collagen</code></li>
<li><code>Schnappix - &lt;EventName&gt; - Originale</code></li>
</ul>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Benötige ich eine Internetverbindung für die Fotobox?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Nein, für das Aufnehmen von Fotos, die Speicherung auf dem Gerät und den lokalen Druck (z.B. über ein lokales WLAN) wird <strong>keine</strong> aktive Internetverbindung benötigt.</p>
<p>Die Benutzeroberfläche von Schnappix ist primär für die Nutzung auf <strong>Tablets</strong>
optimiert. Tablets bieten den Gästen auf Events einen größeren Bildschirm als
Vorschau-Spiegel und eine angenehmere Bedienung für den Sticker-Editor.
Allerdings funktioniert die App auch auf jedem Android Smartphone.</p>
</div>
</div>
@@ -107,7 +73,20 @@
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Schnappix benötigt mindestens <strong>Android 10 (API Level 29)</strong>, um die volle Unterstützung für die moderne CameraX-API und das aktuelle Dateisystem (Scoped Storage) zu gewährleisten.</p>
<p>Schnappix benötigt mindestens <strong>Android 10 (API Level 29)</strong>, um die volle
Unterstützung für die moderne CameraX-API und das aktuelle Dateisystem (Scoped Storage) zu
gewährleisten.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Benötige ich eine Internetverbindung für die Fotobox?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Nein, für das Aufnehmen von Fotos, die Speicherung auf dem Gerät und den lokalen Druck (z.B.
über ein lokales WLAN) wird <strong>keine</strong> aktive Internetverbindung benötigt.</p>
</div>
</div>
@@ -117,7 +96,31 @@
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Ja, Schnappix unterstützt neben der eingebauten Tablet-Frontkamera auch externe USB-Kameras (UVC). Schließe einfach eine USB-Webcam an dein Tablet an, um für eine noch bessere Bildqualität zu sorgen.</p>
<p>Ja, Schnappix unterstützt neben der eingebauten Tablet-Frontkamera auch externe USB-Kameras
(UVC). Schließe einfach eine USB-Webcam an dein Tablet an, um für eine noch bessere
Bildqualität zu sorgen. Sollte dies nicht sofort funktionieren, probiere es mit einem
USB-C-Hub.
Wenn eine externe Kamera verbunden wird, muss der Schnappix App in den
Admineinstellungen die Frontkamera deaktiviert werden, damit die USB-Kamera funktioniert,
da dies im Hintergrund nicht automatisch geschieht.
<br>Wichtig: Die Foto-Qualität hängt von der verbundenen Kamera ab.
</p>
</div>
</div>
<!-- Funktionen der Fotobox -->
<div class="accordion-item">
<button class="accordion-header">
<span>Wie viele Fotos werden für eine Collage aufgenommen?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Die App ist für Serienbilder ausgelegt und nimmt automatisch 1 bis 5 Fotos hintereinander
auf. Aus dieser Serie wird im Anschluss direkt eine fertige Collage zum Drucken oder
Speichern generiert.
Wie viel Bilder aufgenommen werden sollen, kann in den Admineinstellungen voreingestellt
werden. In einer Serie kann immer nur die eingestellte Anzahl an Fotos aufgenommen werden.
</p>
</div>
</div>
@@ -127,27 +130,103 @@
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Schnappix bietet einen integrierten Editor, mit dem die Gäste ihre Collagen vor dem Speichern oder Drucken verzieren können. Emojis, Datumsanzeigen und der Eventname können dabei wie auf Social Media frei auf dem Bildschirm verschoben, gedreht und skaliert werden.</p>
<p>Schnappix bietet einen integrierten Editor, mit dem die Gäste ihre Collagen vor dem Speichern
oder Drucken verzieren können. Emojis, Datumsanzeigen und der Eventname können dabei wie auf
Social Media frei auf dem Bildschirm verschoben, gedreht und skaliert werden.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Wie viele Fotos werden für eine Collage aufgenommen?</span>
<span>Wie füge ich eigene Fotorahmen hinzu?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Die App ist für Serienbilder ausgelegt und nimmt automatisch 1 bis 5 Fotos hintereinander auf. Aus dieser Serie wird im Anschluss direkt eine fertige Collage zum Drucken oder Speichern generiert.</p>
<p>Erstelle ein transparentes PNG im Format <strong>1500 x 1000 px</strong> (3:2 Querformat,
max. 5 MB)
<strong>Variante 1 - Für besondere Anlässe: </strong>Das erstelle Bild kann auf der Seite
unter Upload
an den Entwickler übermittelt werden. Dieser kann das Template dann in die App integrieren.
Wichtig hierbei ist, dass nicht alle Anfragen berücksichtigt werden können.
Außerdem werden alle Rahmen öffentlich für alle sichtbar sein, sobald sie in die App
integriert wurden.
Die Bildrechte der hochgeladenen Rahmen müssen frei von Rechten Dritter sein (z.B. selber
gemalt oder selbst erstellt).
Der Entwickler übernimmt keine Haftung für die Nutzung der hochgeladenen Rahmen.
<strong>Variante 2 - Nur für Entwickler: </strong>Lege das erstellte PNG im Ordner
<code>assets/frames/</code> als <code>frame_name.png</code> ab.
Trage das Template anschließend in <code>src/data/frames.ts</code> ein
und führe einen neuen Build aus.
</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Wie kann ich die Fotobox-App wieder verlassen?</span>
<span>Welche Drucker werden unterstützt?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Da Schnappix für den sicheren Betrieb auf Events entwickelt wurde, sperrt der Kiosk-Modus das Tablet zuverlässig ab. Gäste können die App nicht beenden. Das Verlassen ist nur durch die Eingabe deines sicheren Passworts im versteckten Admin-Bereich möglich.</p>
<p>Es werden alle Drucker unterstützt, die das <strong>IPP (Internet Printing Protocol)</strong>
im lokalen Netzwerk unterstützen. Beliebte Fotodrucker wie der Canon CP1300 lassen sich
direkt über ihre lokale IP-Adresse ansteuern.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Kann ich die App auch ohne Drucker nutzen?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Ja, die App lässt sich auch <strong>komplett digital</strong> nutzen! Lässt du im
Admin-Bereich das Feld für die Drucker-IP-Adresse leer, schaltet die App automatisch in
einen digitalen Modus um und alle Druck-Buttons in der Benutzeroberfläche werden für die
Gäste ausgeblendet. Die Fotos werden dann rein lokal auf dem Gerät gespeichert.</p>
</div>
</div>
<!-- Speicherung & Event-Verwaltung -->
<div class="accordion-item">
<button class="accordion-header">
<span>Wie erstelle ich ein neues Event?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>In Schnappix gibt es keine komplizierte Account- oder Event-Verwaltung. Öffne einfach den
versteckten Admin-Bereich in der App und trage dort unter "Event-Name" den Titel deiner
Veranstaltung ein (z. B. "Hochzeit Anna & Paul"). Ab diesem Moment werden alle Fotos und
Collagen automatisch unter diesem Namen verknüpft und gespeichert.
Bei Neuinstallation der App wird automatisch nach dem Event gefragt, welcher in der
Admin-Übersicht jederzeit eingesehen und geändert werden kann.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Kann ich den Event-Namen nachträglich ändern?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Ja, du kannst den Namen im Admin-Bereich jederzeit ändern. Beachte aber: Diese Namensänderung
gilt nur für zukünftige Aufnahmen. Bereits gespeicherte Fotos verbleiben in der
Galerie-Ordnerstruktur unter dem alten Event-Namen, damit deine bisherigen Dateien nicht
durcheinandergeraten.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Wo werden die Fotos gespeichert?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Schnappix nutzt die moderne Android MediaStore API und speichert Collagen sowie Originale
direkt in der Galerie deines Tablets unter den Ordnern:</p>
<ul>
<li><code>Schnappix - &lt;EventName&gt; - Collagen</code></li>
<li><code>Schnappix - &lt;EventName&gt; - Originale</code></li>
</ul>
</div>
</div>
@@ -157,17 +236,86 @@
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Ja, alle aufgenommenen Originalfotos und die fertigen Collagen werden automatisch und lokal in der Standard-Galerie-App deines Tablets (unter dem jeweiligen Event-Namen) abgespeichert und können jederzeit eingesehen oder exportiert werden.</p>
<p>Ja, alle aufgenommenen Originalfotos und die fertigen Collagen werden automatisch und lokal
in der Standard-Galerie-App deines Gerätes (unter dem jeweiligen Event-Namen) abgespeichert
und können jederzeit eingesehen oder exportiert werden.</p>
</div>
</div>
<!-- Sicherheit & Administration -->
<div class="accordion-item">
<button class="accordion-header">
<span>Wie aktiviere ich den Kiosk-Modus auf dem Tablet?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p><strong>Wichtiger Hinweis: </strong>Die Kiosk-Funktion ist nur für Entwickler gedacht!
Damit der Kiosk-Modus (LockTask) ohne Bestätigungsdialoge funktioniert, muss die App als
<strong>Device Owner</strong> festgelegt werden. Führe dazu folgenden ADB-Befehl aus:
</p>
<code>adb shell dpm set-device-owner de.orfel.schnappix/de.orfel.schnappix.KioskDeviceAdminReceiver</code>
<p>Danach kannst du den Kiosk-Modus einfach im passwortgeschützten Admin-Bereich ein- und
ausschalten.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Ist die App auch für Smartphones geeignet?</span>
<span>Wie kann ich die Fotobox-App wieder verlassen?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Die Benutzeroberfläche von Schnappix ist primär für die Nutzung auf <strong>Tablets</strong> optimiert. Tablets bieten den Gästen auf Events einen größeren Bildschirm als Vorschau-Spiegel und eine angenehmere Bedienung für den Sticker-Editor.</p>
<p>Da Schnappix für den sicheren Betrieb auf Events entwickelt wurde, sperrt der Kiosk-Modus das
Tablet zuverlässig ab. Gäste können die App nicht beenden. Das Verlassen ist nur durch die
Eingabe deines sicheren Passworts im versteckten Admin-Bereich möglich.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Wo finde ich die Einstellungen in der Fotobox-App?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Die eigentliche Schnappix-App verfügt über einen versteckten Admin-Bereich.
Dieser ist durch ein Passwort (Standardmäßig: 1234) geschützt und ermöglicht es dir,
grundlegende
Parameter wie Event-Namen, Drucker-IP-Adressen oder Fotorahmen einzustellen und den
Kiosk-Modus zu steuern.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span>Welche Einstellungsfelder gibt es im Admin-Bereich der App?</span>
<i class="fa-solid fa-chevron-down"></i>
</button>
<div class="accordion-content">
<p>Der passwortgeschützte Admin-Bereich der App steuert das gesamte Verhalten der
Fotobox. Folgende Felder und Schalter stehen dir zur Verfügung:</p>
<ul>
<li style="margin-bottom: 8px;"><strong>Event-Name:</strong> Der Titel der Veranstaltung.
Dieser wird für die Datei-Ordnerstruktur genutzt und steht den Gästen als Sticker zur
Verfügung.</li>
<li style="margin-bottom: 8px;"><strong>Admin-Passwort:</strong> Erlaubt dir, das Passwort
für diesen Bereich zu ändern, damit Gäste sich keinen Zugriff verschaffen können.
Standardmäßig ist das Passwort: 1234.
Bei Vergessen ist ein Rücksetzen nur durch Neuinstallation der App möglich.</li>
<li style="margin-bottom: 8px;"><strong>Kiosk-Modus (LockTask):</strong> Ein Schalter, der
das Tablet sicher sperrt. Ist er aktiviert, gibt es keinen Zurück- oder Home-Button
mehr. Die App lässt sich erst nach Eingabe des Passworts wieder verlassen.</li>
<li style="margin-bottom: 8px;"><strong>Drucker-IP-Adresse:</strong> Trage hier die lokale
WLAN-Adresse (IPv4) deines Fotodruckers ein. Lässt du das Feld leer, schaltet die App in
einen rein digitalen Modus und versteckt alle Druck-Buttons.</li>
<li style="margin-bottom: 8px;"><strong>Fotorahmen / Template:</strong> Hier kannst du
festlegen, welche Rahmen als Überlagerung für die Collagen genutzt werden sollen.</li>
<li style="margin-bottom: 8px;"><strong>Log-Funktion:</strong> Ein Diagnose-Tool, das im
Hintergrund Ereignisse und potenzielle Abstürze protokolliert. Die Fehlerberichte
werden nicht automatisch an die Entwickler gesendet. Falls es zu Problemen kommt,
kannst du die Log-Funktion hier jederzeit aktivieren. Die Logs können dann gern
direkt mit dem Betreff "Schnappix Crash-Report" und einer kurzen Beschreibung des
Problems per E-Mail an <code>jannik.schnappix@gmail.com</code> gesendet werden.</li>
</ul>
</div>
</div>
</div>
@@ -195,7 +343,9 @@
<label for="admin-password">Admin Passwort</label>
<input type="password" id="admin-password" placeholder="Passwort eingeben" required>
</div>
<div id="admin-login-error" style="color: #ff4466; font-size: 14px; margin-bottom: 15px; display: none;">Falsches Passwort!</div>
<div id="admin-login-error"
style="color: #ff4466; font-size: 14px; margin-bottom: 15px; display: none;">Falsches Passwort!
</div>
<button type="submit" class="btn btn-primary btn-block">Anmelden</button>
</form>
</div>
@@ -212,7 +362,8 @@
<button id="admin-export-btn" class="btn btn-primary">Einträge-Code exportieren</button>
</div>
<div id="export-container" style="display: none; margin-top: 20px;">
<textarea id="export-textarea" rows="6" readonly style="width: 100%; font-family: monospace; font-size: 12px; background: rgba(5,5,16,0.8); border: 1px solid var(--border-color); color: var(--accent-color); padding: 10px; border-radius: 8px;"></textarea>
<textarea id="export-textarea" rows="6" readonly
style="width: 100%; font-family: monospace; font-size: 12px; background: rgba(5,5,16,0.8); border: 1px solid var(--border-color); color: var(--accent-color); padding: 10px; border-radius: 8px;"></textarea>
</div>
</div>
</div>
@@ -220,4 +371,5 @@
<script src="app.js"></script>
</body>
</html>
</html>
+1
View File
@@ -25,6 +25,7 @@
<a href="upload.html">Upload</a>
<a href="faq.html">FAQ</a>
<a href="feedback.html">Feedback Hub</a>
<a href="tester.html">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
+1
View File
@@ -25,6 +25,7 @@
<a href="upload.html">Upload</a>
<a href="faq.html">FAQ</a>
<a href="feedback.html" class="active-nav">Feedback Hub</a>
<a href="tester.html">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
+35 -3
View File
@@ -5,6 +5,29 @@ header('Content-Type: application/json');
$dataDir = __DIR__ . '/data/';
$dataFile = $dataDir . 'feedback.json';
function getAdminPassword() {
$envVar = getenv('ADMIN_PASSWORD');
if ($envVar !== false && $envVar !== '') {
return $envVar;
}
$envFile = __DIR__ . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
$parts = explode('=', $line, 2);
if (count($parts) === 2) {
if (trim($parts[0]) === 'ADMIN_PASSWORD') {
return trim($parts[1]);
}
}
}
}
return '1234';
}
$adminPassword = getAdminPassword();
// Create data directory if not exists
if (!is_dir($dataDir)) {
@mkdir($dataDir, 0755, true);
@@ -114,12 +137,21 @@ switch ($_SERVER['REQUEST_METHOD']) {
echo json_encode(['error' => 'Feedback not found']);
}
}
elseif ($action === 'verify_password') {
$secret = $input['secret'] ?? '';
if ($secret === $adminPassword) {
echo json_encode(['success' => true]);
} else {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
}
exit;
}
elseif ($action === 'delete') {
$id = $input['id'] ?? '';
$secret = $input['secret'] ?? '';
// Simple authentication via secret "1234" (as defined in frontend)
if ($secret !== '1234') {
if ($secret !== $adminPassword) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
@@ -139,7 +171,7 @@ switch ($_SERVER['REQUEST_METHOD']) {
$answer = $input['answer'] ?? '';
$secret = $input['secret'] ?? '';
if ($secret !== '1234') {
if ($secret !== $adminPassword) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
+10 -10
View File
@@ -24,6 +24,7 @@
<a href="upload.html">Upload</a>
<a href="faq.html">FAQ</a>
<a href="feedback.html">Feedback Hub</a>
<a href="tester.html">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
@@ -39,21 +40,20 @@
<h1 class="text-gradient" style="font-family: var(--font-titles); font-size: 36px; margin-bottom: 24px; border-bottom: 1px solid var(--border-color); padding-bottom: 12px;">Impressum</h1>
<h3 style="margin-top: 20px; font-size: 18px;">Angaben gemäß § 5 TMG</h3>
<p style="color: var(--text-muted); margin-bottom: 15px;">Jannik [Nachname]<br>
[Straße und Hausnummer]<br>
[PLZ und Ort]</p>
<p style="color: var(--text-muted); margin-bottom: 15px;">Jannik Müller<br>
Rothenthaler Straße 2<br>
09526 Olbernhau</p>
<h3 style="margin-top: 20px; font-size: 18px;">Kontakt</h3>
<p style="color: var(--text-muted); margin-bottom: 15px;">E-Mail: info@schnappix.de<br>
Telefon: [Telefonnummer - optional]</p>
<p style="color: var(--text-muted); margin-bottom: 15px;">E-Mail: jannik.schnappix@gmail.com<br>
Telefon: 015678579661</p>
<h3 style="margin-top: 20px; font-size: 18px;">Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h3>
<p style="color: var(--text-muted); margin-bottom: 15px;">Jannik [Nachname]<br>
[Adresse wie oben]</p>
<p style="color: var(--text-muted); margin-bottom: 15px;">Jannik Müller<br>
Rothenthaler Straße 2<br>
09526 Olbernhau</p>
<h3 style="margin-top: 20px; font-size: 18px;">Streitschlichtung</h3>
<p style="color: var(--text-muted); margin-bottom: 15px;">Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: <a href="https://ec.europa.eu/consumers/odr" target="_blank" rel="noopener" style="color: var(--accent-color);">https://ec.europa.eu/consumers/odr</a>.<br>
Unsere E-Mail-Adresse finden Sie oben im Impressum. Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>
</div>
</div>
</section>
+26 -9
View File
@@ -1,17 +1,22 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="icon" type="image/png" href="favicon.png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schnappix - Die ultimative Photobooth App</title>
<meta name="description" content="Schnappix ist eine Tablet-optimierte Photobooth-App mit Kiosk-Modus, Direktdruck, Filtern, Stickern und anpassbaren Rahmen für dein perfektes Event.">
<meta name="description"
content="Schnappix ist eine Tablet-optimierte Photobooth-App mit Kiosk-Modus, Direktdruck, Filtern, Stickern und anpassbaren Rahmen für dein perfektes Event.">
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@500;700;800&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<!-- Header Navigation -->
@@ -25,6 +30,7 @@
<a href="upload.html">Upload</a>
<a href="faq.html">FAQ</a>
<a href="feedback.html">Feedback Hub</a>
<a href="tester.html">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
@@ -38,9 +44,11 @@
<div class="hero-bg-glow"></div>
<div class="container hero-container">
<div class="hero-content">
<div class="badge">🚀 Version 23.01.X</div>
<div class="badge">🚀 Version 23.01.17</div>
<h1>Erstelle unvergessliche Momente mit <span class="text-gradient">Schnappix</span></h1>
<p class="hero-lead">Die professionelle, Tablet-optimierte Photobooth-App für dein Event. Kiosk-Modus, Direktdruck, kreative Sticker und maßgeschneiderte Rahmen vereint in einem atemberaubenden Synthwave-Design.</p>
<p class="hero-lead">Die professionelle, Tablet-optimierte Photobooth-App für dein Event. Kiosk-Modus,
Direktdruck, kreative Sticker und maßgeschneiderte Rahmen vereint in einem atemberaubenden
Synthwave-Design.</p>
<div class="hero-actions">
<a href="features.html" class="btn btn-primary">Features entdecken</a>
<a href="feedback.html" class="btn btn-secondary">Feedback abgeben</a>
@@ -90,7 +98,9 @@
</div>
</div>
<div style="display: flex; justify-content: center; gap: 8px; margin-bottom: 5px;">
<button class="btn btn-secondary" id="demo-btn-reset" style="padding: 6px 12px; font-size: 11px; border-radius: 6px;"><i class="fa-solid fa-rotate-left"></i> Neues Foto aufnehmen</button>
<button class="btn btn-secondary" id="demo-btn-reset"
style="padding: 6px 12px; font-size: 11px; border-radius: 6px;"><i
class="fa-solid fa-rotate-left"></i> Neues Foto aufnehmen</button>
</div>
</div>
</div>
@@ -121,7 +131,9 @@
<label for="admin-password">Admin Passwort</label>
<input type="password" id="admin-password" placeholder="Passwort eingeben" required>
</div>
<div id="admin-login-error" style="color: #ff4466; font-size: 14px; margin-bottom: 15px; display: none;">Falsches Passwort!</div>
<div id="admin-login-error"
style="color: #ff4466; font-size: 14px; margin-bottom: 15px; display: none;">Falsches Passwort!
</div>
<button type="submit" class="btn btn-primary btn-block">Anmelden</button>
</form>
</div>
@@ -139,8 +151,12 @@
<button id="admin-export-btn" class="btn btn-primary">Einträge-Code exportieren</button>
</div>
<div id="export-container" style="display: none; margin-top: 20px;">
<label style="color: var(--text-muted); font-size: 13px; display: block; margin-bottom: 8px;">Kopiere diesen Code und ersetze damit den <code>defaultFeedback</code> Block in <code>app.js</code>, um Änderungen dauerhaft für alle Besucher zu speichern:</label>
<textarea id="export-textarea" rows="6" readonly style="width: 100%; font-family: monospace; font-size: 12px; background: rgba(5,5,16,0.8); border: 1px solid var(--border-color); color: var(--accent-color); padding: 10px; border-radius: 8px;"></textarea>
<label
style="color: var(--text-muted); font-size: 13px; display: block; margin-bottom: 8px;">Kopiere
diesen Code und ersetze damit den <code>defaultFeedback</code> Block in <code>app.js</code>, um
Änderungen dauerhaft für alle Besucher zu speichern:</label>
<textarea id="export-textarea" rows="6" readonly
style="width: 100%; font-family: monospace; font-size: 12px; background: rgba(5,5,16,0.8); border: 1px solid var(--border-color); color: var(--accent-color); padding: 10px; border-radius: 8px;"></textarea>
</div>
</div>
</div>
@@ -148,4 +164,5 @@
<script src="app.js"></script>
</body>
</html>
</html>
+141
View File
@@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="icon" type="image/png" href="favicon.png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schnappix - App Testing Anmeldung</title>
<meta name="description" content="Melde dich als App Tester für Schnappix an und hilf uns, die App für den Release vorzubereiten.">
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<!-- Header Navigation -->
<header class="main-header">
<div class="container nav-container">
<div class="logo">
<a href="index.html" style="text-decoration: none;"><span class="logo-glow">Schnappix</span></a>
</div>
<nav class="nav-links">
<a href="features.html">Features</a>
<a href="upload.html">Upload</a>
<a href="faq.html">FAQ</a>
<a href="feedback.html">Feedback Hub</a>
<a href="tester.html" class="active-nav">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
<i class="fa-solid fa-bars"></i>
</button>
</div>
</header>
<!-- Page Title -->
<div style="padding: 140px 0 20px;">
<div class="container">
<h1 class="section-title" style="margin-bottom: 20px;">Werde <span class="text-gradient">App Tester</span></h1>
<p style="text-align: center; color: var(--text-muted); max-width: 600px; margin: 0 auto 40px;">Hilf uns beim finalen Feinschliff! Trage dich als Tester ein und erhalte exklusiven Vorabzugang zu Schnappix im Google Play Store.</p>
</div>
</div>
<!-- Tester Registration Section -->
<section class="feedback-section" style="border-top: none; padding-top: 20px; padding-bottom: 80px;">
<div class="container">
<div class="feedback-grid" style="grid-template-columns: 1fr; max-width: 800px; margin: 0 auto;">
<!-- Info Card -->
<div class="glass" style="padding: 30px; border-radius: 20px; border: 1px solid rgba(139, 92, 246, 0.3); margin-bottom: 30px; background: rgba(5, 5, 16, 0.6);">
<h3 style="color: var(--accent-color); margin-bottom: 15px; display: flex; align-items: center; gap: 10px;">
<i class="fa-solid fa-circle-info"></i> Wichtige Hinweise für Tester
</h3>
<ul style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 15px; color: var(--text-muted);">
<li style="display: flex; gap: 15px; align-items: flex-start;">
<i class="fa-brands fa-google" style="color: #fff; font-size: 20px; margin-top: 2px;"></i>
<div>
<strong style="color: #fff; display: block; margin-bottom: 5px;">GoogleMail-Adresse erforderlich</strong>
Bitte gib unbedingt die GoogleMail (Gmail) Adresse an, welche fest mit deinem Android-Gerät und dem Google Play Store verknüpft ist. Nur so können wir dich für das Testing freischalten.
</div>
</li>
<li style="display: flex; gap: 15px; align-items: flex-start;">
<i class="fa-solid fa-calendar-check" style="color: #fff; font-size: 20px; margin-top: 2px;"></i>
<div>
<strong style="color: #fff; display: block; margin-bottom: 5px;">Mindestens 14 Tage installiert lassen</strong>
Aufgrund der Google Play Console Richtlinien müssen 20 Tester die App für mindestens 14 Tage durchgehend installiert behalten, damit wir die Freigabe für den öffentlichen Store (Production Access) erhalten. Bitte deinstalliere die App in dieser Zeit nicht!
</div>
</li>
</ul>
</div>
<!-- Submit Form -->
<div class="feedback-form-card glass">
<h3 style="margin-bottom: 20px;">Registrierung</h3>
<form id="tester-form">
<div class="form-group">
<label for="tester-email">Deine Google Play Store E-Mail Adresse</label>
<input type="email" id="tester-email" placeholder="deine.adresse@gmail.com" required>
</div>
<div class="form-group" style="display: flex; gap: 10px; align-items: flex-start;">
<input type="checkbox" id="tester-consent" required style="margin-top: 5px; accent-color: var(--accent-color);">
<label for="tester-consent" style="font-size: 13px; font-weight: normal; margin-bottom: 0;">Ich habe die Hinweise gelesen und werde die App nach der Installation für mindestens 14 Tage nicht deinstallieren.</label>
</div>
<div id="tester-success-msg" style="display: none; background: rgba(34, 197, 94, 0.2); color: #4ade80; padding: 15px; border-radius: 8px; margin-bottom: 20px; text-align: center; border: 1px solid rgba(74, 222, 128, 0.3);">
<i class="fa-solid fa-check-circle"></i> Erfolgreich eingereicht! Nach erfolgreicher Prüfung senden wir dir eine Mail mit den Registrierungslinks.
</div>
<div id="tester-error-msg" style="display: none; background: rgba(2ef, 68, 102, 0.2); color: #ff4466; padding: 15px; border-radius: 8px; margin-bottom: 20px; text-align: center; border: 1px solid rgba(255, 68, 102, 0.3);">
<i class="fa-solid fa-triangle-exclamation"></i> Es gab einen Fehler. Bitte versuche es später noch einmal.
</div>
<button type="submit" class="btn btn-primary btn-block" id="tester-submit-btn">Als Tester anmelden</button>
</form>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="main-footer">
<div class="container footer-container">
<p>&copy; 2026 Schnappix. Alle Rechte vorbehalten.</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="datenschutz.html">Datenschutzerklärung</a>
</div>
</div>
</footer>
<!-- Admin Login Modal -->
<div id="admin-login-modal" class="modal-overlay">
<div class="modal-content glass">
<button class="modal-close" aria-label="Schließen">&times;</button>
<h2>Admin Login</h2>
<form id="admin-login-form">
<div class="form-group">
<label for="admin-password">Admin Passwort</label>
<input type="password" id="admin-password" placeholder="Passwort eingeben" required>
</div>
<div id="admin-login-error" style="color: #ff4466; font-size: 14px; margin-bottom: 15px; display: none;">Falsches Passwort!</div>
<button type="submit" class="btn btn-primary btn-block">Anmelden</button>
</form>
</div>
</div>
<!-- Admin Panel Control Modal (If Logged In) -->
<div id="admin-panel-modal" class="modal-overlay">
<div class="modal-content glass">
<button class="modal-close" aria-label="Schließen">&times;</button>
<h2>Admin Panel</h2>
<div class="modal-scroll-area">
<p>Sie sind als Administrator angemeldet.</p>
<div style="margin-top: 20px; display: flex; gap: 15px;">
<button id="admin-logout-btn" class="btn btn-secondary">Abmelden</button>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
<?php
error_reporting(0);
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$inputJSON = file_get_contents('php://input');
$input = json_decode($inputJSON, true);
if (!$input || empty($input['email'])) {
http_response_code(400);
echo json_encode(['error' => 'Ungültige Eingabe']);
exit;
}
$email = filter_var($input['email'], FILTER_SANITIZE_EMAIL);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['error' => 'Ungültige E-Mail Adresse']);
exit;
}
// Send notification to Ntfy
$ntfyUrl = 'https://ntfy.orfel.de/Schnappix-App-Testing';
$message = "E-Mail: $email";
$ch = curl_init($ntfyUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Title: Neue Tester-Anmeldung',
'Tags: calling,bust_in_silhouette'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
echo json_encode(['success' => true]);
exit;
}
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
+23 -1
View File
@@ -159,6 +159,7 @@
<a href="upload.html" class="active-nav">Upload</a>
<a href="faq.html">FAQ</a>
<a href="feedback.html">Feedback Hub</a>
<a href="tester.html">App Testing</a>
<a href="#" id="admin-login-trigger"><i class="fa-solid fa-user-gear"></i> Admin</a>
</nav>
<button class="menu-toggle" aria-label="Menu öffnen">
@@ -172,7 +173,7 @@
<div class="container">
<div class="upload-card glass">
<h1 style="font-family: var(--font-titles); font-size: 32px; margin-bottom: 10px;">Hintergrund <span class="text-gradient">Upload</span></h1>
<p style="color: var(--text-muted); font-size: 15px;">Lade dein eigenes PNG hoch. Es wird uns per E-Mail gesendet und wir binden es in deine App ein.</p>
<p style="color: var(--text-muted); font-size: 15px;">Lade dein eigenes PNG hoch. Nach erfolgreicher Prüfung wird dein Rahmen in die App eingebaut und ist dort für alle Nutzer sichtbar.</p>
<!-- FormSubmit.co is used to send the email directly with attachment without a backend -->
<form id="bg-upload-form">
@@ -184,6 +185,12 @@
<input type="file" id="file-input" name="background_image" accept="image/png" required>
</div>
<div class="form-group" style="text-align: left; display: none;" id="frame-info">
<label for="frame-name">Name des Rahmens (nur Kleinbuchstaben & Zahlen)</label>
<input type="text" id="frame-name" name="frame_name" pattern="[a-z0-9_]+" placeholder="z.B. summer1" required>
<div class="upload-hint" style="margin-top: 5px;">Wird gespeichert als: <strong style="color: var(--primary-color);">frame_<span id="frame-preview-name">...</span>.png</strong></div>
</div>
<div class="form-group" style="text-align: left; display: none;" id="customer-info">
<label for="customer-email">Deine E-Mail Adresse (für Rückfragen)</label>
<input type="email" id="customer-email" name="email" placeholder="mail@example.com" required>
@@ -245,8 +252,17 @@
const previewBox = document.getElementById('preview-box');
const imagePreview = document.getElementById('image-preview');
const submitBtn = document.getElementById('submit-btn');
const frameInfo = document.getElementById('frame-info');
const frameNameInput = document.getElementById('frame-name');
const framePreviewName = document.getElementById('frame-preview-name');
const customerInfo = document.getElementById('customer-info');
frameNameInput.addEventListener('input', (e) => {
const val = e.target.value.trim().toLowerCase().replace(/[^a-z0-9_]/g, '');
e.target.value = val;
framePreviewName.textContent = val || '...';
});
// Drag and Drop Effects
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
@@ -295,6 +311,7 @@
validationBox.style.display = 'block';
previewBox.style.display = 'none';
submitBtn.style.display = 'none';
frameInfo.style.display = 'none';
customerInfo.style.display = 'none';
valFormat.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Überprüfe Dateiformat...`;
@@ -327,6 +344,7 @@
// Show Preview and Submit Button
imagePreview.src = e.target.result;
previewBox.style.display = 'block';
frameInfo.style.display = 'block';
customerInfo.style.display = 'block';
submitBtn.style.display = 'block';
} else {
@@ -364,8 +382,10 @@
console.error('Server antwortete nicht mit gültigem JSON:', responseText);
alert('Ein Server-Fehler ist aufgetreten (siehe Konsole). Das Bild wurde eventuell trotzdem hochgeladen.');
form.reset();
framePreviewName.textContent = '...';
previewBox.style.display = 'none';
submitBtn.style.display = 'none';
frameInfo.style.display = 'none';
customerInfo.style.display = 'none';
validationBox.style.display = 'none';
return;
@@ -374,8 +394,10 @@
if (response.ok) {
alert('Erfolgreich hochgeladen!');
form.reset();
framePreviewName.textContent = '...';
previewBox.style.display = 'none';
submitBtn.style.display = 'none';
frameInfo.style.display = 'none';
customerInfo.style.display = 'none';
validationBox.style.display = 'none';
} else {
+42 -4
View File
@@ -36,10 +36,48 @@ if ($mimeType !== 'image/png') {
exit;
}
// Generate unique filename
$filename = uniqid('frame_') . '_' . time() . '.png';
$rawFrameName = isset($_POST['frame_name']) ? trim($_POST['frame_name']) : '';
$frameName = preg_replace('/[^a-z0-9_]/', '', strtolower($rawFrameName));
if (empty($frameName)) {
http_response_code(400);
echo json_encode(['error' => 'Ungültiger oder fehlender Frame-Name.']);
exit;
}
$filename = 'frame_' . $frameName . '.png';
$destination = $uploadDir . $filename;
// Check local uniqueness (already uploaded via web)
if (file_exists($destination)) {
http_response_code(400);
echo json_encode(['error' => 'Ein Rahmen mit diesem Namen existiert bereits auf dem Server. Bitte wähle einen anderen.']);
exit;
}
// Check Gitea uniqueness
$giteaUrl = 'https://git.orfel.de/api/v1/repos/Jannik/Schnappix/contents/assets/frames';
$chGitea = curl_init($giteaUrl);
curl_setopt($chGitea, CURLOPT_RETURNTRANSFER, true);
curl_setopt($chGitea, CURLOPT_TIMEOUT, 5);
curl_setopt($chGitea, CURLOPT_HTTPHEADER, ['User-Agent: Schnappix-Web Backend']);
$giteaResponse = curl_exec($chGitea);
$giteaCode = curl_getinfo($chGitea, CURLINFO_HTTP_CODE);
curl_close($chGitea);
if ($giteaCode === 200 && $giteaResponse) {
$framesData = json_decode($giteaResponse, true);
if (is_array($framesData)) {
foreach ($framesData as $frameFile) {
if (isset($frameFile['name']) && strtolower($frameFile['name']) === strtolower($filename)) {
http_response_code(400);
echo json_encode(['error' => 'Ein Rahmen mit diesem Namen existiert bereits in der App. Bitte wähle einen anderen.']);
exit;
}
}
}
}
// Move uploaded file
if (move_uploaded_file($file['tmp_name'], $destination)) {
// Build public URL
@@ -50,14 +88,14 @@ if (move_uploaded_file($file['tmp_name'], $destination)) {
// Send notification to Ntfy
$ntfyUrl = 'https://ntfy.orfel.de/Schnappix-Frame-Upload';
$message = "Ein neuer Rahmen wurde hochgeladen!\n\nVon: $email\nBild-URL: $publicUrl";
$message = "Name: $filename\nVon: $email\nBild-URL: $publicUrl";
$ch = curl_init($ntfyUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Title: Neuer Schnappix Hintergrund',
'Tags: frame_with_picture,tada',
'Tags: framed_picture,tada',
'Click: ' . $publicUrl,
'Attach: ' . $publicUrl
]);