Files
Kinderspiele/Grundsätzliche planung.txt
2026-04-26 09:44:19 +02:00

3174 lines
70 KiB
Plaintext
Raw Permalink 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.
Ich hätte gerne eine kleine Spiele App für Kids auf dem Tablet. Am bestens als Browser Game. Die App soll nach Möglichkeit eingeschrieben sein, dass die Kinder das Spiel nicht verlassen können. Vermutlich ist das schwer bis unmöglich unter Android. Wenn nicht möglich können wir auch eine API bauen. Die Spiele sollen Reaktion, Gefühl für Bedienung und das kindliche wohl hervorheben. Es können einfache Games wie etwas suchen, einfach Rätsel, etwas ausmalen, nachmalen von zahlen und Buchstaben. Wichtig due Anforderungen sollten den alter entsprechen. Aktuell sind die Kinder 2 und 5. Aber sie werden älter und das Talent steigt. Auch Sprachausgabe und Spracherkennung wären toll. Wir können gerne KI Tools über ollama Einbinden. Magst du mir ein erstes Konzept bauen?
profile
kimi-k2.5:cloud
Nachgedacht für 26 Sekunden
Hier ist ein umfassendes Konzept für "KinderWelt" eine skalierbare, browserbasierte Spiele-Plattform mit Kiosk-Modus und KI-Integration.
1. Technische Architektur
Kern-Stack
Frontend: PWA (Progressive Web App) mit Vanilla JS/TypeScript + Canvas API/WebGL
UI-Framework: Kein schweres Framework rein CSS-Animationen für 60fps auf Tablets
Audio: Web Speech API (TTS) + Web Audio API + optional TensorFlow.js (für lokale Spracherkennung)
Backend (optional): Node.js/Express als Proxy zu Ollama (localhost)
Speicher: IndexedDB für Offline-Fortschritte & Cache
Der Kiosk-Modus (Android-Strategie)
Da reiner Browser-Lock schwierig ist, hier eine zweistufige Lösung:
Option A: PWA + Android Screen Pinning (Einfach)
PWA im Fullscreen (display: standalone im Manifest)
Android "Bildschirm fixieren" (Screen Pinning) aktivieren
Keine Hardware-Buttons sichtbar, aber Eltern können es mit Tastenkombination entsperren
Option B: Dedicated Wrapper-App (Empfohlen)
Einfache Android-App (Kotlin) mit WebView
Überschreibt onBackPressed(), onPause(), blockiert Home-Button (mit Systemrechten)
Startet automatisch den Browser im Kiosk-Modus
Vorteil: Kind kann wirklich nicht raus, außer Eltern geben Passwort ein
2. Spielkonzept & Altersstufen
Modul A: "Entdecker" (2-3 Jahre)
Ziel: Grobmotorik, Ursache-Wirkung, Freude am Tippen
Blubber-Blasen: Bunte Kreise erscheinen, platzen bei Berührung mit "Pop"-Sound
Tier-Farm: Großes Tier antippen → realistischer Sound + Animation (TTS: "Das ist eine Kuh!")
Farben-Explosion: Ganzer Bildschirm färbt sich bei Touch, leichte Vibration (haptisches Feedback)
Fang den Stern: Langsam fallende Sterne einsammeln (kein Game Over, nur positive Verstärkung)
Modul B: "Kleine Forscher" (4-5 Jahre)
Ziel: Feinmotorik, Mustererkennung, erste Logik
Zahlen-Folgen: Connect-the-Dots mit Audio ("Tippe auf die 3!")
Memory-Leicht: 3x3 Karten mit Tieren, keine Zeitbegrenzung
Formen-Sortierer: Drag & Drop (Kreis in runde Löcher, Viereck in eckige)
Einfaches Malen: "Zauberstift" (automatisch innerhalb von Linien bleiben)
Modul C: "Smart Kids" (6+ Jahre) [Zukunftssicher]
Ziel: Kognitive Entwicklung, Kreativität
KI-Mitmachgeschichten: Kind sagt ein Wort, Ollama generiert nächsten Satz (lokal, datensicher!)
Buchstaben-Nachzeichnen: Pfadverfolgung mit Präzisions-Messung
Logik-Rätsel: "Welches Tier passt nicht dazu?" mit Sprachausgabe der Fragen
3. KI-Integration (Ollama)
Da Datenschutz bei Kindern kritisch ist, läuft alles lokal auf dem Tablet/Server:
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
// Beispiel: Bildbeschreibung für "Was ist das?" Spiel
const ollamaRequest = {
model: "llava-phi3", // Leichtes Vision-Modell
prompt: "Beschreibe dieses Bild für ein 5-jähriges Kind in 3 einfachen Sätzen.",
images: [base64Image]
};
// Spracherkennung via Whisper (lokal)
// TTS via Piper (Ollama Integration) oder Web Speech API
KI-Spiele-Ideen:
"Erzähl mir was": Kind malt etwas, KI errät es (Bild-zu-Text)
Intelligenter Helfer: Wenn Kind bei einem Rätsel 3x falsch tippt, gibt die KI keineswegs die Lösung, sondern einen kindgerechten Hinweis ("Schau mal, die Form hat runde Ecken...")
4. UI/UX Design-Prinzipien
Visuelles Design
Keine Text-Menüs: Nur Icons + Sprache (multilingual)
Große Touch-Targets: Mindestens 80x80px (Kindliche Motorik!)
Hoher Kontrast: Saturierte Farben, aber nicht grell (Material You für Kids)
Keine Werbung/Popups: Vollständig abgeschottete Umgebung
Interaktions-Muster
Immediate Feedback: <100ms Reaktionszeit auf Touch
Positive Verstärkung: Sternchen-Animationen, Applaus-Sounds (keine negativen Sounds bei Fehlern)
Parental Gate: Für Einstellungen: "Halte beide Ohren gleichzeitig gedrückt" (physisches Rätsel statt Mathe-Aufgabe)
5. Implementierungs-Roadmap
Phase 1: MVP (4-6 Wochen)
PWA-Grundgerüst mit Fullscreen-API
Modul A (2 Jahre): Blubber-Blasen + Tier-Farm
Einfacher Kiosk-Mode via Android Screen Pinning
Web Speech API (TTS)
Phase 2: Erweiterung (8-10 Wochen)
Wrapper-App für echten Lock-Mode
Modul B (5 Jahre): Zahlen-Folgen + Memory
Zeichen-Canvas mit simplem "Stay-in-lines" Algorithmus
Offline-Speicherung der Fortschritte
Phase 3: KI-Integration (12+ Wochen)
Ollama-Setup auf lokalem Server/Tablet
Bilderkennungs-Spiele
Adaptive Schwierigkeit (Spiel merkt sich, wenn Kind schnell ist)
6. Schnelle Starter-Datei (Proof of Concept)
Hier ein erster HTML-Prototyp für den Kiosk-Modus + einfaches Spiel:
html
Einklappen
Speichern
Kopieren
Vorschau
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>KinderWelt</title>
<style>
* { margin: 0; padding: 0; touch-action: none; user-select: none; }
body { overflow: hidden; background: #FFE5B4; font-family: 'Comic Sans MS', cursive; }
#gameCanvas { position: fixed; top: 0; left: 0; }
.bubble {
position: absolute;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.8), rgba(255,255,255,0.2));
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.1s;
}
.bubble:active { transform: scale(0.9); }
</style>
</head>
<body>
<div id="gameArea"></div>
<script>
// Vollbild erzwingen
document.documentElement.requestFullscreen().catch(e => console.log(e));
// Verhindert Zoom/Scroll
document.addEventListener('touchmove', function(e) { e.preventDefault(); }, { passive: false });
// Einfaches Blasen-Spiel für 2-Jährige
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
const synth = window.speechSynthesis;
function createBubble() {
const bubble = document.createElement('div');
bubble.className = 'bubble';
const size = Math.random() * 100 + 80; // 80-180px (gut für Kinderfinger)
bubble.style.width = bubble.style.height = size + 'px';
bubble.style.left = Math.random() * (window.innerWidth - size) + 'px';
bubble.style.top = Math.random() * (window.innerHeight - size) + 'px';
bubble.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
bubble.addEventListener('touchstart', (e) => {
e.preventDefault();
// Sound + Sprache
const utter = new SpeechSynthesisUtterance("Pop!");
utter.rate = 0.8; // Langsamer für Kinder
utter.pitch = 1.2; // Fröhlicher
synth.speak(utter);
bubble.style.transform = 'scale(1.2)';
setTimeout(() => bubble.remove(), 100);
createBubble(); // Neue Blase
});
document.body.appendChild(bubble);
}
// Starte mit 3 Blasen
for(let i=0; i<3; i++) createBubble();
// Verhindert Verlassen via Zurück-Taste (nur im Browser begrenzt wirksam)
history.pushState(null, null, location.href);
window.onpopstate = function() {
history.pushState(null, null, location.href);
};
</script>
</body>
</html>
7. Empfehlung für Android-Tablet
Für deinen Use Case empfehle ich:
Samsung Kids Mode oder Google Family Link als erste Barriere
Darüber: Die Wrapper-App (APK), die den Browser im Immersive Mode startet
Ollama läuft im Hintergrund auf dem Tablet (wenn leistungsstark genug, z.B. Samsung Tab S9) oder auf einem Raspberry Pi im Netzwerk
Nächste Schritte:
Soll ich den Code für die Android-Wrapper-App (Kotlin) liefern?
Ollama-Setup-Anleitung für lokale Sprachverarbeitung?
Detaillierte Spielmechanik für einen bestimmten Bereich (z.B. das Nachzeichnen)?
Das System wächst mit den Kindern mit du kannst einfach neue Module als JavaScript-Dateien hinzufügen, ohne die App neu zu installieren!
Lass uns die App als Docker hinter einem recerced Proxy planen. Das ermöglicht die Nutzung von überall, und die ki liegt im gleichen Netzwerk wie das Game selbst. Am besten wir verifizieren die Anmeldung via Authentficator App push. Das spart uns den Umweg Benutzername und Passwort freizugeben. Weitere Idee eine jellyfin Integration. Was hältst du von der Idee. Bestimmte Serien können direkt gestreamt werden.
profile
kimi-k2.5:cloud
Nachgedacht für 3 Minuten
Das ist eine ** Enterprise-Grade-Architektur** für eine Kinder-App! Die Kombination aus öffentlicher Erreichbarkeit, KI-Pipeline und Medien-Streaming erfordert sorgfältige Sicherheitsplanung besonders bei Minderjährigen.
Hier ist das erweiterte Cloud-native Konzept mit Docker-Stack:
1. Netzwerk-Architektur (Zero-Trust)
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
Internet → Traefik (SSL/Let's Encrypt) → Authelia (Auth/2FA) → Services
┌──────────────────────┼──────────────────────┐
↓ ↓ ↓
Game-App (Nginx) Ollama (AI) Jellyfin
(Port 3000) (Port 11434) (Port 8096)
↑ ↑ ↑
Shared Docker Network: kids-zone (isoliert, intern)
Warum diese Trennung?
Ollama ist nicht direkt exposed (nur intern über http://ollama:11434 erreichbar)
Jellyfin wird über die Game-UI "verpackt" (kein direkter Zugriff auf Port 8096 von außen)
Alle externen Requests müssen durch Authelia (Auth-Layer)
2. Authentifizierungs-Strategie: "Passwordless + 2FA Push"
Da du "Authenticator App Push" und "kein Passwort freigeben" willst, empfehle ich WebAuthn/Passkeys mit Authentik (nicht Authelia, da Authentik bessere Passwordless-Flows hat):
Ablauf für Eltern:
Registrierung: Scan QR-Code mit Authenticator-App (Authy/Google Authenticator) → TOTP Secret wird gespeichert
Login:
Option A: Passkey/FIDO2 (FaceID/Fingerprint auf Handy → Push-Benachrichtigung zum Bestätigen)
Option B: Magic Link per Email + TOTP-Code aus App
Option C: Session-Token (einmalig QR-Code scannen für dauerhaftes Tablet im Haus)
Wichtig: Kinder sollen gar nicht authentifiziert werden sie landen direkt im Game nach dem Öffnen der URL. Der Auth-Layer schützt nur vor externen Zugriffen.
3. Jellyfin-Integration (Kinder-Safe)
Technische Umsetzung:
Proxy-Route: /api/media/* → Jellyfin API
UI-Integration: Custom Video-Player im Game-Canvas
Content-Filter: Nur "Kids"-Bibliothek sichtbar (Jellyfin User-Management)
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
// Beispiel: Serie abrufen aus Jellyfin
const getKidsEpisode = async () => {
const response = await fetch('/api/jellyfin/Items?ParentId=KIDS_LIBRARY_ID', {
headers: { 'X-Emby-Token': '${JELLYFIN_API_KEY}' }
});
// Direkter Stream-URL für HTML5-Player
return `/api/jellyfin/Videos/${itemId}/stream.mp4?Static=true`;
};
⚠️ Rechtliche & Sicherheits-Warnung
Jellyfin öffentlich exposen ist hochriskant:
Urheberrecht: Streaming eigener gerippter DVDs ist legal im Privathaushalt, aber öffentlich erreichbar? Grauzone.
Bandbreite: Unverschlüsselte Streams können abgegriffen werden
Kinder-Inhalte: Öffentliche Verfügbarkeit von Kinder-Medien zieht unerwünschte Aufmerksamkeit an
Empfehlung:
VPN-Tunnel (WireGuard/Tailscale) statt öffentlichem Reverse Proxy für Jellyfin
Oder: Cloudflare Tunnel mit Access-Policies (nur deine IP-Adressen erlauben)
4. Docker-Compose Stack
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
version: '3.8'
networks:
kids-zone:
driver: bridge
internal: false # Ollama & Jellyfin nur intern
services:
# Reverse Proxy + SSL
traefik:
image: traefik:v2.10
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=admin@deinedomain.de"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- kids-zone
# Authentik (Identity Provider mit 2FA/Push)
authentik:
image: ghcr.io/goauthentik/server:2023.10
environment:
- AUTHENTIK_SECRET_KEY=${AUTHENTIK_KEY}
- AUTHENTIK_ERROR_REPORTING__ENABLED=false
# PostgreSQL & Redis für Authentik (hier weggelassen für Kürze)
labels:
- "traefik.enable=true"
- "traefik.http.routers.authentik.rule=Host(`auth.deinedomain.de`)"
- "traefik.http.routers.authentik.tls.certresolver=letsencrypt"
networks:
- kids-zone
# Kinder-Spiel-App
kids-game:
build: ./game-app # Node.js/Nginx Image
environment:
- OLLAMA_URL=http://ollama:11434
- JELLYFIN_URL=http://jellyfin:8096
- JELLYFIN_API_KEY=${JELLYFIN_KEY}
labels:
- "traefik.enable=true"
- "traefik.http.routers.kids-game.rule=Host(`spiele.deinedomain.de`)"
- "traefik.http.routers.kids-game.tls.certresolver=letsencrypt"
# Authelia/Authentik Middleware für Schutz
- "traefik.http.routers.kids-game.middlewares=authentik@file"
networks:
- kids-zone
depends_on:
- ollama
- jellyfin
# Ollama (KI) - nur intern erreichbar!
ollama:
image: ollama/ollama:latest
volumes:
- ./ollama-models:/root/.ollama
environment:
- OLLAMA_ORIGINS=*
networks:
- kids-zone
# Keine Ports exposed! Nur über kids-game erreichbar
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu] # Für GPU-Beschleunigung
# Jellyfin (Media) - nur intern!
jellyfin:
image: jellyfin/jellyfin:latest
volumes:
- ./media:/media:ro
- ./jellyfin-config:/config
networks:
- kids-zone
# Kein Port-Mapping! Zugriff nur über kids-game Proxy
5. Sicherheitskonzept für öffentliche Erreichbarkeit
Da es um Kinder geht und öffentlicher Zugriff besteht:
A. Network-Policies
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
# In der compose zusätzlich:
ollama:
networks:
kids-zone:
aliases:
- ollama-internal
# Firewall: Nur kids-game darf zugreifen
B. Content Security Policy (CSP)
Im Game-Frontend (Nginx-Header):
Einklappen
Speichern
Kopieren
1
2
3
4
Content-SSecurity-Policy: default-src 'self';
connect-src 'self' https://auth.deinedomain.de;
media-src 'self' blob:;
script-src 'self' 'unsafe-inline'; # Für Canvas-Spiele nötig
C. Rate-Limiting (Traefik)
yaml
Einklappen
Speichern
Kopieren
1
2
3
labels:
- "traefik.http.middlewares.ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.ratelimit.ratelimit.burst=50"
6. Jellyfin-Integration im Game
"Medien-Ecke" für die Kinder
Eltern-UI: /admin → Serien freischalten (Authentik-geschützt)
Kind-UI: Großer Button "Cartoons schauen" → Lädt Thumbnails von Jellyfin API → HTML5-Player im Vollbild
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Game-Client Code
class MediaPlayer {
async loadEpisodes() {
// Proxy durch eigenes Backend (sicher, da API-Key nicht im Client)
const res = await fetch('/api/media/kids');
const episodes = await res.json();
// Zeige Cover-Buttons (groß für Touch)
}
play(videoId) {
const player = document.createElement('video');
player.src = `/api/media/stream/${videoId}`;
player.controls = false; // Eigene kindgerechte Controls (große Play-Buttons)
player.requestFullscreen();
}
}
7. Kritische Empfehlungen
❌ Was ich nicht empfehle:
Jellyfin direkt öffentlich: Zu riskant. Nutze stattdessen Cloudflare Access oder Tailscale für "von überall"
Kamera/Mikrofon für 2-Jährige im Browser: DSGVO-Problem. Speichere keine Daten, verarbeite KI lokal (was du ja planst - gut!)
Authenticator-Push ohne Backup: Wenn das Handy kaputt ist, kommt niemand rein → Recovery-Codes drucken
✅ Bessere Alternative zu "öffentlich erreichbar":
Tailscale/Headscale (Mesh VPN):
Tablet ist im VPN-Netzwerk
Keine öffentlichen Ports nötig
Du hast "von überall" Zugriff (vom Handy aus)
Jellyfin bleibt intern
Kein Authentik nötig, einfacher JWT-Auth reicht
Zusammenfassung
Deine Architektur ist machbar, aber over-engineered für eine Kinder-App. Für 2+5 Jahre alte Kinder würde ich empfehlen:
Lokale Instanz im Haus (kein öffentlicher Zugriff)
Tailscale für Eltern-Zugriff von unterwegs
Einfacher JWT statt Authentik (nur für Eltern-Login)
Jellyfin separat im VPN, nicht im Game integriert (Kinder sollen spielen, nicht streamen 😉)
Willst du wirklich den Full-Enterprise-Stack mit öffentlicher Domain? Dann liefere ich dir die vollständige Traefik + Authentik + Compose-Konfiguration. Aber überlege dir die Datenschutz-Implikationen (DSGVO bei öffentlicher Kinder-App).
Nächster Schritt: Soll ich die Tailscale-Alternative (sicherer, einfacher) oder den öffentlichen Reverse-Proxy (komplexer, flexibler) detaillieren?
Ok, npm ist bereits vorhanden. Die App ist nur für eigenen Gebrauch. Als Sicherheitsbedenken zur Kenntnis genommen. Es geht um Urlaub oder besuch bei Freunden.
Freigabe war mir wichtig, das die Kinder die App zwar starten können aber halt nur nach Freigabe durch die Eltern nutzen können. Auch jellyfin ist vorhanden. Und soll einfach mit integriert werden. Im Idealfall über einen Nutzer mit den Rechten des Kindes ( nicht altersgerecht=nicht auswählbar).
VPN ist eine Möglichkeit ist aber mehr Traffic und fordert mehr Bandbreite als eine einfache ssl Verbindung über Reversed Proxy.
Offline Variante ohne KI und ohne Jellyfin klingt super. Vielleicht mit lokalen Medien
profile
kimi-k2.5:cloud
Nachgedacht für eine Minute
Perfekt! Das ist ein pragmatisches Homelab-Setup deutlich sinnvoller als Enterprise-Overhead. Mit NPM (Nginx Proxy Manager) hast du bereits das beste Tool für SSL-Offloading.
Hier ist das pragmatische Konzept für den Familien-URLaubs-Modus:
1. Architektur: "Freigabe-Schleuse" mit NPM
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
Internet → NPM (443) → [Entscheidung]:
┌─────────────────┼─────────────────┐
│ │
Kein Token Token vorhanden
│ │
├─ Static ├─ Game API
├─ "Warte auf ├─ Jellyfin Proxy
│ Freigabe" └─ Ollama (optional)
└─ Parental-Gate
(QR-Code / TOTP)
Warum kein VPN doch besser ist (kurz):
Dein Bandbreiten-Argument stimmt für das Streaming, aber für Jellyfin im Urlaub ist ein WireGuard-Tunnel oft stabiler bei Hotel-WLANs (die oft HTTPS filtern/throtteln). Aber wir machen es mit NPM wie gewünscht.
2. Das "Parental Gate" System
Da die Kinder die App starten können, aber erst nach Freigabe spielen:
Option A: TOTP-Daily-Code (Einfachst)
Eltern haben Google Authenticator mit einem Secret (z.B. FAMILIE2024)
Code wechselt alle 30 Minuten (oder täglich für Urlaub)
Kinder sehen nur: "Frage Mama nach dem Code"
Eingabe im Tablet entsperrt Session-Cookie für 2 Stunden
Option B: Push-Freigabe (Eleganter)
Tablet zeigt QR-Code (Session-ID)
Eltern scannen mit Handy → Web-App öffnet sich → Button "Freigeben für 30 Min"
Tablet pollt alle 5 Sekunden /api/status → Status ändert sich zu "grün"
Implementierung (Node.js/Express):
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Backend (ein Teil deiner Game-App)
const sessions = new Map();
// Eltern-Endpunkt (geschützt via NPM Basic Auth oder separate Domain)
app.post('/api/parent/approve', (req, res) => {
const { sessionId, duration } = req.body;
sessions.set(sessionId, {
approved: true,
expires: Date.now() + (duration * 60000),
user: 'kind-max' // Für Jellyfin-User-Auswahl
});
});
// Tablet checkt Status
app.get('/api/status/:sessionId', (req, res) => {
const session = sessions.get(req.params.sessionId);
if (!session) return res.json({ status: 'pending' });
if (session.expires < Date.now()) return res.json({ status: 'expired' });
res.json({
status: 'approved',
jellyfinUser: session.user,
token: generateJWT(session)
});
});
3. NPM (Nginx Proxy Manager) Konfiguration
A. SSL-Zertifikate
kinder-tablet.deinedomain.de (oder DuckDNS/Cloudflare-Tunnel)
Let's Encrypt über NPM GUI
B. Proxy Hosts
1. Haupt-Game (Standard)
Einklappen
Speichern
Kopieren
1
2
3
4
Domain: spiele.zuhause.de
Forward: http://kids-game:3000
SSL: Aktiv
Access List: Öffentlich (für Kinder-Startseite)
2. Admin/Eltern-Freigabe (Geschützt)
Einklappen
Speichern
Kopieren
1
2
3
4
5
Domain: freigabe.zuhause.de
Forward: http://kids-game:3000/admin
SSL: Aktiv
Access List: IP-Whitelist (nur euer Handy/WLAN)
ODER HTTP Basic Auth (User/Pass für Eltern)
3. Jellyfin-Proxy (Intern)
Einklappen
Speichern
Kopieren
1
2
3
4
Domain: media-intern.zuhause.de (nicht exposed!)
Forward: http://jellyfin:8096
Block Common Exploits: An
Access List: Nur localhost/Intern (über Docker-Netzwerk)
Wichtig: Jellyfin bekommt keinen öffentlichen DNS-Eintrag! Nur über die Game-App erreichbar.
4. Jellyfin-Integration: Altersfilter
Setup in Jellyfin:
User kind-2jahre anlegen → Max Parental Rating: "Toddlers"
User kind-5jahre anlegen → Max Parental Rating: "Children"
Bibliotheken taggen mit entsprechenden Ratings
API-Integration:
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Game-Backend (Proxy zu Jellyfin)
app.get('/api/media/episodes', async (req, res) => {
// Token kommt aus der Freigabe-Session (siehe oben)
const userId = req.session.jellyfinUser; // 'kind-2jahre' oder 'kind-5jahre'
const response = await fetch(`http://jellyfin:8096/Users/${userId}/Items`, {
headers: {
'X-Emby-Token': process.env.JELLYFIN_API_KEY
}
});
const data = await response.json();
// Filtere nur "Continue Watching" oder "Next Up"
const safeItems = data.Items.filter(item =>
item.ParentalRating <= (userId.includes('2') ? 1 : 5)
);
res.json(safeItems);
});
// Video-Stream (kein Direct-Expose)
app.get('/api/media/stream/:id', async (req, res) => {
// Proxy den Stream durch dein Backend (kein CORS, kein direkter Jellyfin-Zugang)
const stream = await fetch(`http://jellyfin:8096/Videos/${req.params.id}/stream?Static=true`, {
headers: { 'X-Emby-Token': process.env.JELLYFIN_API_KEY }
});
stream.body.pipe(res);
});
UI im Game:
Große Kacheln: Nur Serien-Titel-Bilder (keine Beschreibung, die Kinder können nicht lesen)
Auto-Play: Nächste Folge startet automatisch (kein "Was soll ich gucken?"-Stress)
Maximale Zeit: Game-Timer zeigt Sanduhr (wenn Eltern nur 20 Min freigegeben)
5. Offline-Modus: "Urlaubs-Paket"
Da du weniger Bandbreite willst: Pre-Download für Offline
Technik:
Service Worker (PWA) cached Spiele-Logik
IndexedDB speichert 3-4 Folgen Peppa Wutz lokal (als Blob)
Sync API: Wenn wieder online, sync Fortschritt (optional)
Docker-Volume Strategie:
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
services:
kids-game:
volumes:
- ./offline-media:/app/public/media:ro # Lokale MP4s für Offline-Modus
- ./game-cache:/app/cache # Service Worker Cache
Offline-Jellyfin (lokal only):
Ein zusätzlicher Jellyfin-Container mit lokalen Dateien (nicht NAS-gemountet)
Nur kleine, transkodierte Dateien (720p, 2MBit) für Tablet-Speicher
Im Urlaub: Switch auf localhost:8096 statt Domain
6. Docker-Compose (Pragmatik-Version)
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
version: '3.8'
services:
# Die Game-App (dein NPM-Target)
kids-app:
image: node:18-alpine
working_dir: /app
volumes:
- ./game-code:/app
- ./downloads:/app/public/downloads:ro # Offline-Videos
command: sh -c "npm install && npm start"
environment:
- JELLYFIN_URL=http://jellyfin:8096
- JELLYFIN_API_KEY=${JELLYFIN_KEY}
- SESSION_SECRET=${SECRET}
networks:
- kidsnet
# Jellyfin (nur intern!)
jellyfin:
image: jellyfin/jellyfin
volumes:
- ./jellyfin-config:/config
- ./media-kids:/media:ro # Nur Kids-Content mounten!
networks:
- kidsnet
# Keine ports! Nur über kids-app erreichbar
# Optional: Ollama für Zuhause
ollama:
image: ollama/ollama
volumes:
- ./ollama:/root/.ollama
networks:
- kidsnet
deploy:
resources:
limits:
memory: 2G # Für schwaches Urlaubs-Tablet begrenzt
networks:
kidsnet:
driver: bridge
7. Der "Urlaubs-Workflow"
Zuhause vorbereiten:
Tablet mit NPM-URL konfiguriert
5 Folgen Lieblingsserie herunterladen (via Jellyfin Download-Funktion)
Offline-Cache leeren, neu füllen
Im Hotel:
Tablet an: Zeigt "Warte auf Freigabe" mit QR-Code
Papa scannt Code → "Freigeben für 30 Min" → Kinder spielen
Nach 30 Min: Screen lockt wieder (oder wechselt zu "Pause machen" Animation)
Kein Internet im Hotel?:
App erkennt navigator.onLine === false
Zeigt nur Offline-Spiele + heruntergeladene Videos
Keine Freigabe nötig (Eltern sind ja da)
Nächster konkreter Schritt: Soll ich die Service-Worker-Implementierung für den Offline-Modus codieren, oder das Parental-Gate mit QR-Code skripten? Beides ist reiner Vanilla-JS/Node.js Code, passt in dein NPM-Setup.
Wie läuft das ganze denn jetzt ab. Docker + APK? Ja bereite gerne alles vor
profile
kimi-k2.5:cloud
Nachgedacht für 46 Sekunden
Hier ist der komplette Bauplan mit allen Dateien, die du brauchst. Wir haben einen Hybrid-Stack: Der Docker-Server ist das "Gehirn", die APK ist der "Kiosk-Rahmen" mit Offline-Cache.
Architektur-Überblick
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────┐ WiFi/4G ┌──────────────────┐
│ Tablet │ ═══════════════════════►│ Docker-Server │
│ (Android APK) │ │ (Zuhause/VPS) │
│ │ │ │
│ ┌─────────────┐ │ HTTP/HTTPS │ ┌──────────────┐ │
│ │ WebView │ │◄═══════════════════════════│ │ Node.js │ │
│ │ (PWA Mode) │ │ │ │ Game-API │ │
│ │ │ │ (Fallback: Offline) │ │ │ │
│ │ • Spiele │ │ │ │ • Jellyfin │ │
│ │ • Videos │ │ │ │ • Ollama │ │
│ │ • Auth │ │ │ │ • Freigabe │ │
│ └─────────────┘ │ └──────────────────┘
└─────────────────┘
Phase 1: Docker-Server (Das Backend)
Erstelle diese Struktur auf deinem Server:
docker-compose.yml
yaml
Einklappen
Speichern
Kopieren
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
version: '3.8'
kids-game:
build: ./game-server
container_name: kids-game
restart: unless-stopped
environment:
- NODE_ENV=production
- JELLYFIN_URL=http://jellyfin:8096
- JELLYFIN_API_KEY=${JELLYFIN_API_KEY}
- SESSION_SECRET=${SESSION_SECRET:-random-secret-change-me}
- OFFLINE_MODE=false
volumes:
- ./downloads:/app/public/downloads:ro # Offline-Videos
networks:
- kids-net
# Kein Port nach außen! Nur via NPM erreichbar
# Jellyfin (nur intern)
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin-kids
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
volumes:
- ./jellyfin-config:/config
- ./media-kids:/media:ro # Nur Kids-Content!
networks:
- kids-net
# Keine Ports exposed!
# Optional: Ollama für KI
ollama:
image: ollama/ollama:latest
container_name: ollama-kids
restart: unless-stopped
volumes:
- ./ollama-models:/root/.ollama
networks:
- kids-net
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
# NPM (Reverse Proxy) - Der einzige öffentliche Zugang
npm:
image: jc21/nginx-proxy-manager:latest
container_name: npm
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81" # Admin-UI
volumes:
- ./npm-data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- kids-net
- default
networks:
kids-net:
driver: bridge
internal: true # Jellyfin & Ollama nur intern!
game-server/Dockerfile
dockerfile
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
game-server/server.js (Schnittstelle)
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const express = require('express');
const path = require('path');
const app = express();
// Statische Dateien (PWA)
app.use(express.static('public'));
// Health-Check für APK
app.get('/api/health', (req, res) => res.json({ status: 'online', timestamp: Date.now() }));
// Session-Status für Parental Gate
const sessions = new Map();
app.post('/api/session/create', (req, res) => {
const id = Math.random().toString(36).substring(7);
sessions.set(id, { status: 'pending', created: Date.now() });
res.json({ sessionId: id, qrData: `kids://auth/${id}` });
});
app.get('/api/session/:id', (req, res) => {
const session = sessions.get(req.params.id);
if (!session) return res.status(404).json({ error: 'not found' });
res.json(session);
});
// Freigabe durch Eltern
app.post('/api/session/:id/approve', express.json(), (req, res) => {
const { duration = 30, jellyfinUser = 'kind-default' } = req.body;
sessions.set(req.params.id, {
status: 'approved',
approvedAt: Date.now(),
expires: Date.now() + (duration * 60000),
jellyfinUser
});
res.json({ success: true });
});
// Jellyfin Proxy (sicher, ohne direkten Zugriff)
app.get('/api/media/list', async (req, res) => {
try {
const response = await fetch(`${process.env.JELLYFIN_URL}/Users`, {
headers: { 'X-Emby-Token': process.env.JELLYFIN_API_KEY }
});
const users = await response.json();
res.json(users);
} catch (e) {
res.status(503).json({ error: 'jellyfin offline', offline: true });
}
});
// Offline-Modus Detection
app.get('/api/config', (req, res) => {
res.json({
serverUrl: process.env.OFFLINE_MODE === 'true' ? null : 'https://deinedomain.de',
version: '1.0.0'
});
});
app.listen(3000, () => console.log('Kids Game Server running'));
game-server/public/index.html (PWA-Grundgerüst)
html
Einklappen
Speichern
Kopieren
Vorschau
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<!DOCTYPE html>
// QR Code generieren
new QRious({
element: document.getElementById('qrCanvas'),
value: data.qrData,
size: 200
});
// Polling nach Freigabe
this.checkInterval = setInterval(() => this.checkApproval(), 5000);
} catch (e) {
console.log('Server nicht erreichbar - Offline Modus');
this.showOfflineMode();
}
},
async checkApproval() {
try {
const res = await fetch(`/api/session/${this.sessionId}`);
const data = await res.json();
if (data.status === 'approved') {
clearInterval(this.checkInterval);
this.unlock(data.expires);
}
} catch (e) {
// Server weg -> bleibt gesperrt oder Offline-Modus
}
},
unlock(expires) {
document.getElementById('lockScreen').classList.remove('active');
document.getElementById('gameScreen').classList.add('active');
// Countdown
const updateTimer = () => {
const left = Math.floor((expires - Date.now()) / 1000);
if (left <= 0) location.reload(); // Resperren
const min = Math.floor(left / 60);
const sec = left % 60;
// Timer anzeigen...
};
setInterval(updateTimer, 1000);
},
showOfflineMode() {
// Einfache Offline-Spiele ohne Freigabe (nur für Notfall)
if (confirm('Server nicht erreichbar. Offline-Spiele starten?')) {
document.getElementById('lockScreen').style.display = 'none';
document.getElementById('gameScreen').classList.add('active');
document.body.classList.add('offline');
}
},
startGame() {
// Simples Blubber-Spiel
const canvas = document.getElementById('gameCanvas');
const createBubble = () => {
const b = document.createElement('div');
b.className = 'bubble';
b.style.left = Math.random() * 80 + 10 + '%';
b.style.top = Math.random() * 80 + 10 + '%';
b.style.backgroundColor = ['#FF6B6B', '#4ECDC4', '#45B7D1'][Math.floor(Math.random() * 3)];
b.ontouchstart = () => {
b.remove();
new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmwhBTGH0fPTjjkIHGu8+OGbURE...').play();
};
canvas.appendChild(b);
setTimeout(() => b.remove(), 5000);
};
setInterval(createBubble, 2000);
},
requestClose() {
// Sendet Nachricht an Android-App zum Schließen
if (window.AndroidInterface) {
window.AndroidInterface.exitApp();
}
}
};
app.init();
</script>
</body>
</html>
game-server/public/manifest.json (PWA)
json
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "KinderWelt",
"short_name": "Kinder",
"start_url": "/",
"display": "standalone",
"background_color": "#667eea",
"theme_color": "#764ba2",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192" },
{ "src": "/icon-512.png", "sizes": "512x512" }
]
}
game-server/public/sw.js (Service Worker für Offline)
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const CACHE_NAME = 'kids-game-v1';
const urlsToCache = [
'/',
'/index.html',
'/style.css', // falls ausgelagert
'/offline-game.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache first, then network
if (response) return response;
return fetch(event.request).catch(() => {
// Fallback für Bilder/Videos
if (event.request.url.includes('/media/')) {
return new Response('Offline', { status: 503 });
}
});
})
);
});
Phase 2: Android APK (Der Kiosk-Rahmen)
Projekt-Struktur (Android Studio)
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
KidsKiosk/
├── app/
│ ├── src/main/
│ │ ├── java/com/kids/kiosk/
│ │ │ ├── MainActivity.kt
│ │ │ └── AdminReceiver.kt
│ │ ├── res/
│ │ │ ├── layout/activity_main.xml
│ │ │ └── xml/device_admin.xml
│ │ └── AndroidManifest.xml
│ └── build.gradle
└── build.gradle
build.gradle (Module: app)
gradle
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.kids.kiosk'
compileSdk 34
defaultConfig {
applicationId "com.kids.kiosk"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.webkit:webkit:1.9.0'
}
AndroidManifest.xml
xml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kids.kiosk">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- Für Kiosk-Modus -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="KinderWelt"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:usesCleartextTraffic="false">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:configChanges="orientation|screenSize|smallestScreenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Device Admin für Lock Task Mode -->
<receiver
android:name=".AdminReceiver"
android:description="@string/device_admin_description"
android:label="@string/device_admin_label"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
</application>
</manifest>
res/xml/device_admin.xml
xml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
<force-lock />
<limit-password />
</uses-policies>
</device-admin>
MainActivity.kt (Der Kiosk-Controller)
kotlin
Einklappen
Speichern
Kopieren
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import android.webkit.*
}
// Ansonsten: Nichts tun (Blockieren)
// Eltern können via 5-fach Tap oben links entsperren
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Blockiere Home, Recent Apps, etc.
return when (keyCode) {
KeyEvent.KEYCODE_HOME -> true
KeyEvent.KEYCODE_APP_SWITCH -> true
else -> super.onKeyDown(keyCode, event)
}
}
private fun hideSystemUI() {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) hideSystemUI()
// Re-Enable Kiosk wenn Fokus zurück
if (hasFocus && isDeviceOwner() && !isKioskMode) {
enableKioskMode()
}
}
// Eltern-Entsperrung: 5-fach Tap oben links
private var tapCount = 0
private var lastTapTime = 0L
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
if (event.y < 100 && event.x < 100) {
val now = System.currentTimeMillis()
if (now - lastTapTime < 500) {
tapCount++
if (tapCount >= 5) {
disableKioskMode()
finish() // App beenden
}
} else {
tapCount = 1
}
lastTapTime = now
}
}
return super.onTouchEvent(event)
}
// JavaScript Interface
class WebAppInterface(private val mContext: Context) {
@JavascriptInterface
fun exitApp() {
(mContext as MainActivity).disableKioskMode()
(mContext as MainActivity).finish()
}
@JavascriptInterface
fun isOnline(): Boolean {
return (mContext as MainActivity).isOnline()
}
}
}
AdminReceiver.kt
kotlin
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
package com.kids.kiosk
import android.app.admin.DeviceAdminReceiver
import android.content.Context
import android.content.Intent
class AdminReceiver : DeviceAdminReceiver() {
override fun onEnabled(context: Context, intent: Intent) {
super.onEnabled(context, intent)
}
}
res/layout/activity_main.xml
xml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Phase 3: Deployment & Einrichtung
Schritt 1: Server starten
bash
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
# Auf deinem Server
mkdir kids-world && cd kids-world
# Kopiere alle Dateien von oben
docker-compose up -d
# NPM einrichten (Web-UI auf Port 81)
# 1. Proxy Host anlegen:
# Domain: kinder.deinedomain.de
# Forward: http://kids-game:3000
# SSL: Lets Encrypt
# 2. Access List für /admin (Eltern-Bereich) optional
Schritt 2: Android App bauen
bash
Einklappen
Speichern
Kopieren
1
2
3
4
# In Android Studio
Build → Generate Signed Bundle/APK
# ODER für Entwicklung:
./gradlew assembleDebug
Schritt 3: Tablet als "Device Owner" einrichten (Wichtig für Kiosk!)
Achtung: Das Tablet muss dafür neu/werksreset sein oder ihr müsst den bestehenden Account entfernen.
bash
Einklappen
Speichern
Kopieren
1
2
# Über ADB (USB-Debugging aktivieren)
adb shell dpm set-device-owner com.kids.kiosk/.AdminReceiver
Oder via QR-Code Enrollment (für neue Geräte):
bash
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
# Erstelle QR-Code mit:
# {
# "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME": "com.kids.kiosk/.AdminReceiver",
# "android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM": "DEIN_HASH",
# "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION": "https://deinedomain.de/app.apk"
# }
Schritt 4: Offline-Inhalte vorbereiten
Erstelle app/src/main/assets/offline/index.html mit lokalen Spielen (einfaches HTML/JS), das geladen wird wenn kein Internet.
Zusammenfassung Ablauf
Zuhause:
Docker startet (Jellyfin, Game-Server)
Tablet verbindet sich mit WiFi → zeigt QR-Code
Papa scannt → "Freigeben für 30 Min" → Kinder spielen
Unterwegs/Im Hotel:
Kein Internet? → APK zeigt Offline-Spiele aus Assets
Hotel-WiFi? → Verbindet zu deinem Server (wenn exposed) oder bleibt Offline
Videos vorab runtergeladen in /downloads (Jellyfin Sync)
Sicherheit:
Kinder können nicht raus (Lock Task Mode)
Nur du kannst entsperren (5-Tap oben links)
Keine externen Links möglich
Brauchst du jetzt: Die build.gradle für die Eltern-Companion-App (zum Freigeben via QR-Code scannen)? Oder reicht ein einfacher Web-Link für die Freigabe (https://deinedomain.de/admin/freigabe?code=XXX)?
Ich empfehle Letzteres: Einfache Webseite für die Freigabe, die nur für deine IP erreichbar ist oder mit HTTP-Auth geschützt.
Oh, lass uns auch direkt einen Emulator für Sega Mega Drive Spiele und nes sowie SNES Spiele einbauen. Zur Steuerung nehmen wir einen Bluetooth Controller
profile
kimi-k2.5:cloud
Nachgedacht für 29 Sekunden
Das ist der Game-Changer! Retro-Emulation passt perfekt zu "Reaktion & Bedienung" für Kinder, und Bluetooth-Controller sind robust gegenüber "rabiater" Kinder-Bedienung (im Gegensatz zu Touchscreen).
Hier ist die Erweiterte Architektur mit Emulation:
1. Technologie-Stack: EmulatorJS + WebView
Wir nutzen EmulatorJS (WASM-Emulatoren) direkt in der WebView das ist performanter als native Android-Emulatoren und bleibt in unserer Kiosk-Architektur.
Unterstützte Systeme:
NES (Nintendo Entertainment System)
SNES (Super Nintendo)
Sega Mega Drive/Genesis
Optional: Game Boy, Game Boy Advance
2. Backend-Erweiterung (ROM-Management)
docker-compose.yml Erweiterung
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
services:
# ... (bestehende Services) ...
# ROM-Server (einfacher nginx für ROMs)
rom-server:
image: nginx:alpine
container_name: roms
volumes:
- ./roms:/usr/share/nginx/html/roms:ro
- ./roms/nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- kids-net
# Nur intern erreichbar (über kids-game proxy)
roms/nginx.conf (Schützt ROMs vor direktem Zugriff)
nginx
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 80;
root /usr/share/nginx/html;
# ROMs nur via Referrer-Check oder Auth-Header (simpler Schutz)
location /roms/ {
internal; # Nur interne Redirects erlaubt
alias /usr/share/nginx/html/roms/;
}
# Cover-Bilder dürfen extern angezeigt werden
location /covers/ {
alias /usr/share/nginx/html/covers/;
}
}
game-server/server.js (ROM-Proxy mit Lizenz-Check)
javascript
Einklappen
Speichern
Kopieren
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// ... bestehende Routes ...
file: 'sonic.md',
cover: '/covers/sonic.jpg',
allowed: true, // Nur eigene Backups!
description: 'Hilf Sonic die Ringe zu sammeln'
},
'mario': {
title: 'Super Mario Bros',
system: 'nes',
file: 'mario.nes',
cover: '/covers/mario.jpg',
allowed: true
},
'zelda': {
title: 'Zelda: Link to the Past',
system: 'snes',
file: 'zelda.sfc',
cover: '/covers/zelda.jpg',
allowed: true
}
};
// API: Verfügbare Spiele (nur erlaubte)
app.get('/api/games/library', (req, res) => {
const games = Object.entries(ROMS_LIBRARY)
.filter(([_, data]) => data.allowed)
.map(([id, data]) => ({
id,
title: data.title,
system: data.system,
cover: data.cover,
description: data.description
}));
res.json(games);
});
// API: ROM laden (geschützt)
app.get('/api/games/rom/:id', async (req, res) => {
const game = ROMS_LIBRARY[req.params.id];
if (!game || !game.allowed) {
return res.status(404).json({ error: 'not found' });
}
// Proxy zu internem ROM-Server
try {
const response = await fetch(`http://rom-server/roms/${game.file}`);
if (!response.ok) throw new Error('ROM not found');
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${game.file}"`);
response.body.pipe(res);
} catch (e) {
res.status(404).json({ error: 'ROM nicht verfügbar (nur eigene Backups verwenden!)' });
}
});
// Save-State speichern (Cloud-Saves!)
app.post('/api/games/save/:id', express.raw({ type: 'application/octet-stream', limit: '10mb' }), (req, res) => {
const { id } = req.params;
const saveData = req.body; // EmulatorJS State
// Speichere in Datei oder DB
const fs = require('fs').promises;
const path = `./saves/${id}_${Date.now()}.state`;
fs.mkdir('./saves', { recursive: true })
.then(() => fs.writeFile(path, saveData))
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Save-State laden
app.get('/api/games/save/:id/latest', async (req, res) => {
// Letzten Save-State zurückgeben
const fs = require('fs').promises;
const files = await fs.readdir('./saves').catch(() => []);
const latest = files
.filter(f => f.startsWith(req.params.id))
.sort().reverse()[0];
if (!latest) return res.status(404).json({ error: 'no save' });
const data = await fs.readFile(`./saves/${latest}`);
res.send(data);
});
3. Frontend: Retro-Spiele Bereich
game-server/public/emulator.html (Emulator-Page)
html
Einklappen
Speichern
Kopieren
Vorschau
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
<!DOCTYPE html>
async saveState() {
if (!this.instance) return;
const data = await this.instance.getSaveState();
fetch(`/api/games/save/${this.currentGame}`, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: data
});
this.showNotification('💾 Gespeichert!');
},
async loadSaveState(gameId) {
try {
const res = await fetch(`/api/games/save/${gameId}/latest`);
if (res.ok) {
const data = await res.arrayBuffer();
this.instance.loadSaveState(new Uint8Array(data));
this.showNotification('🎮 Willkommen zurück!');
}
} catch(e) {
console.log('Kein Save-State vorhanden');
}
},
showNotification(text) {
const div = document.createElement('div');
div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#48bb78;padding:20px 40px;border-radius:10px;font-size:24px;z-index:9999;';
div.textContent = text;
document.body.appendChild(div);
setTimeout(() => div.remove(), 2000);
},
// Gamepad API Setup
setupGamepad() {
window.addEventListener('gamepadconnected', (e) => {
console.log('Controller verbunden:', e.gamepad.id);
this.updateControllerStatus(true, e.gamepad.id);
this.pollGamepad();
});
window.addEventListener('gamepaddisconnected', (e) => {
this.updateControllerStatus(false, null);
});
// Prüfe ob bereits verbunden
const gamepads = navigator.getGamepads();
if (gamepads[0]) {
this.updateControllerStatus(true, gamepads[0].id);
this.pollGamepad();
}
},
updateControllerStatus(connected, name) {
const dot = document.getElementById('controller-dot');
const text = document.getElementById('controller-text');
if (connected) {
dot.classList.add('connected');
text.textContent = `🎮 ${name || 'Controller'} bereit`;
} else {
dot.classList.remove('connected');
text.textContent = 'Controller verbinden...';
}
},
pollGamepad() {
// Wird von EmulatorJS intern gehandelt, aber wir können zusätzliche Features machen
// z.B. Combo für "Eltern-Menü": START + SELECT gleichzeitig
const check = () => {
const gp = navigator.getGamepads()[0];
if (gp) {
// START (9) + SELECT (8) = Beenden
if (gp.buttons[9]?.pressed && gp.buttons[8]?.pressed) {
this.quit();
}
}
requestAnimationFrame(check);
};
check();
}
};
// Initialisierung
emulator.init();
</script>
</body>
</html>
4. Android-Anpassungen (Bluetooth & Gamepad)
Die WebView unterstützt standardmäßig die Gamepad API, aber wir müssen sicherstellen, dass Bluetooth-Permissions korrekt sind.
AndroidManifest.xml Erweiterung
xml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
<!-- Bluetooth für Controller -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <!-- Android 12+ -->
<!-- Für Android 12+ -->
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
MainActivity.kt Erweiterung (Gamepad-Optimierung)
kotlin
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In onCreate, nach WebView Setup:
webView.settings.userAgentString = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
// Bluetooth-Activity Result Handler (für Pairing)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_ENABLE_BT) {
Toast.makeText(this, "Bluetooth bereit für Controller", Toast.LENGTH_SHORT).show()
}
}
// Companion Object
companion object {
const val REQUEST_ENABLE_BT = 1
}
5. Controller-Setup Guide für Eltern
Für die Kinder optimal:
Controller koppeln (einmalig):
Xbox/PS Controller per Bluetooth mit Tablet koppeln
Oder: 8BitDo SN30 Pro (hat direkten XInput-Modus)
Automatisches Mapping:
EmulatorJS erkennt automatisch Standard-Layout:
A/B/X/Y = A/B/X/Y
D-Pad = Steuerkreuz
L/R = Schultertasten
Start/Select = Menü
Sonderfunktionen:
Start + Select gleichzeitig = Spiel beenden (zurück zur Auswahl)
L + R = Schnell-Speichern (Save-State)
6. Ordner-Struktur auf dem Server
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kids-world/
├── docker-compose.yml
├── game-server/
│ ├── server.js
│ └── public/
│ ├── index.html (Hauptspiel)
│ └── emulator.html (Retro-Bereich)
├── roms/ # ⚠️ Nur eigene Backups!
│ ├── sonic.md
│ ├── mario.nes
│ └── covers/
│ ├── sonic.jpg
│ └── mario.jpg
├── saves/ # Cloud-Saves
└── jellyfin-config/
7. Wichtige rechtliche Hinweise
⚠️ ROMs:
Nur eigene Backups von Spielen verwenden, die ihr physisch besitzt
Keine Download-ROMs aus dem Internet (Urheberrecht)
Die Cover-Bilder sollten selbst erstellt oder aus offiziellen Quellen stammen
BIOS-Dateien:
Einige Emulatoren (z.B. PS1) brauchen BIOS-Dateien → nur aus eigener Konsole dumpen
8. Integration in Haupt-App
Füge im Hauptmenü (index.html) einen neuen Button hinzu:
html
Einklappen
Speichern
Kopieren
Vorschau
1
2
3
4
5
6
<div class="menu-item" onclick="location.href='/emulator.html'"
style="background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%);">
<span style="font-size: 60px;">🎮</span>
<h2>Retro-Spiele</h2>
<p>Sega, Nintendo & mehr</p>
</div>
Nächster Schritt: Soll ich das Controller-Mapping UI bauen (falls die Tasten vertauscht sind), oder eine Bewegungssteuerung als Fallback (wenn kein Controller da ist - z.B. Tablet neigen für Steuerung)?
https://ebay.us/m/KJdYqO
2x Kabelloser Bluetooth-Gamepad-Controller für Android-Handys und TV-Boxen T ]⭐
Ist der dafür geeignet?
profile
kimi-k2.5:cloud
Nachgedacht für 12 Sekunden
Da ich den eBay-Link nicht direkt öffnen kann, bewerte ich ihn anhand der Beschreibung "2x Kabelloser Bluetooth-Gamepad für Android".
Kurz-Antwort: Wahrscheinlich ja, aber mit Einschränkungen für deine Altersgruppe (2+5 Jahre).
Was das wahrscheinlich ist
Das klingt nach einem generischen iPEGA-Clone oder T3/T4 Bluetooth-Controller (die billigen mit 2 Stück für 15-25€). Die sind technisch okay, aber...
Checkliste: Ist der für euch geeignet?
✅ Was spricht dafür:
Android-Kompatibilität: Werden erkannt als Gamepad (meistens)
Preis: Zwei Stück = gut für Geschwister (kein Streit)
Bluetooth: Funktioniert mit der HTML5 Gamepad API im Browser
⚠️ Warnsignale (typisch für diese Controller):
Problem
Check beim Testen
Falsches Mapping
Im Spiel springt er statt zu schießen?
"Tastatur-Modus"
Manchmal melden sie sich als Tastatur statt Controller → Gamepad API sieht nichts
Sleep-Modus
Geht nach 3 Minuten aus und verliert Verbindung?
Analog-Stick-Drift
Billige Analogsticks können nach 2 Monaten spinnen
Für 2 & 5 Jahre spezifische Probleme
Für das 2-jährige Kind:
Wahrscheinlich zu groß/schwer (die Controller sind meist Standard-Größe)
Zu viele Tasten (Kind drückt willkürlich)
Besser: Einfacher Einzel-Button oder Touchscreen für dieses Alter
Für das 5-jährige Kind:
Ideal, wenn es ein einfacher Controller ist (D-Pad + 4 Buttons)
Test-Protokoll (nach Lieferung)
Prüfe das sofort beim Empfang:
javascript
Einklappen
Speichern
Kopieren
1
2
3
// Im Tablet-Browser öffnen: about:blank
// Dann in Konsole (F12) oder als Lesezeichen:
javascript:(function(){window.addEventListener("gamepadconnected",e=>alert("Gamepad: "+e.gamepad.id+"\nButtons: "+e.gamepad.buttons.length));alert("Controller anschalten und warten...")})()
Oder einfacher Test:
Tablet: gamepad-tester.com im Browser öffnen
Controller verbinden (Bluetooth)
Tasten drücken → Werden sie erkannt?
Alternativ-Empfehlung (falls der nicht gut ist)
Falls der von eBay enttäuscht:
Für Kinder (5+):
8BitDo Lite 2 (~30€) - kleiner, robuster, perfektes Mapping
Xbox Controller (wenn ihr einen habt) - funktioniert sofort
Für das 2-jährige:
Gar kein Controller - Touch-Bubbles sind besser für die Feinmotorik
Oder: Arcade-Stick (große bunte Tasten, z.B. "Arcade1Up" Mini)
Konfiguration im Code (Fallback)
Falls der Controller komisches Mapping hat, erweitere die emulator.js:
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
// In deinem emulator.html, im setupGamepad():
const buttonMap = {
// Falls A/B vertauscht sind:
0: 'B', // Xbox A = Nintendo B
1: 'A', // Xbox B = Nintendo A
2: 'X',
3: 'Y'
};
Fazit: Kauf es, teste es, aber habe einen Plan B (Touch-Steuerung bleibt aktiv im Code). Für 15-20€ für 2 Stück ist das Risiko vertretbar, aber erwarte keine Wunder-Qualität.
Soll ich einen QR-Code Generator bauen, den ihr auf das Tablet klebt, um den Controller schnell zu testen (ohne Tippen)?
Der 2jährige wird noch keinen Controller bekommen. Aber um die kritik mit den Controllern ist gut.
Ich habe hier noch einen:
Merkmale & Details
🎮 BLITZSCHNELLE 1000HZ-POLLING-RATE: Sowohl im kabelgebundenen als auch im kabellosen Modus wird eine blitzschnelle Abfragerate von 1000 Hz erreicht. Entwickelt für Wettkampfspieler. In FPS- und Rennspielen wird jede Eingabe mit höchster Präzision und blitzschneller Reaktion verarbeitet, was Ihnen einen entscheidenden Vorteil verschafft.
🎮 PRAKTISCHE LADESTATION: Der 1000mAh Akku und der All-in-One-Hub Magnetische Dock mit Sofortladefunktion sorgen dafür, dass Ihr Wireless Controller immer voll aufgeladen ist. Ein 2,4-GHz-Dongle minimiert Störungen und verbessert die Stabilität, ideal für PC-Spiele. Genießen Sie ununterbrochenes Gaming-Vergnügen.
🎮 PRÄZISE HALL-JOYSTICKS UND -TRIGGER: Verbessere Ihr Gaming-Erlebnis mit unserem Quad-Hall-System, das doppelte Hall-Effekt-Joysticks und Hall-Trigger bietet. Diese Technologie gewährleistet präzise Verfolgung und Druckempfindlichkeit, die eine reibungslose Steuerung für jede Bewegung ermöglicht.
🎮 PROFESSIONELLE ANPASSUNG: ① 2 anpassbare Tasten auf der Rückseite unterstützen bis zu 32 Aktionen. ② TURBO+A/B/X/Y/LB/LT/RB/RT ermöglicht Schnellfeuerfunktion. ③ Umschalten der ABXY-Tastenbelegung: Halten Sie FN+A 3 Sekunden lang gedrückt, um die Switch- oder PC/Xbox-Belegung anzupassen. ④ APP-Anpassung für Vollspektrum-RGB-Beleuchtung, Vibrationsmodi und mehr.
🎮 VIERSTUFIGE VIBRATIONSFUNKTION: Asymmetrische Motoren simulieren vielfältige Spielempfindungen und sorgen für Fahrvibrationen bei Rennspielen und authentisches Rückstoß-Feedback bei Shootern. Tauchen Sie ein in die Spielwelt, in der jede Aktion eine lebensechte Reaktion erhält.
🎮 DUAL-CORE-EMPFÄNGER 2-SPIELER-KOOP: Ein Zweikanal-Empfänger, der zwei Griffe gleichzeitig sperrt. Das Wohnzimmer verwandelt sich in eine Arena. Freunde / Paare / Eltern und Kinder können jederzeit Seite an Seite zusammenarbeiten. Sich von den Einschränkungen durch zusätzliche USB-Anschlüsse und -Kabel verabschieden und das "Zusammengehen" zur effizientesten Taktik machen. Der USB Receiver / Dongle ist in der Lieferung enthalten.
🎮 UNTERSTÜTZT MEHRERE GERÄTE: Kompatibel mit PC, Switch, Android und Apple. Der Controller ist für FPS-Spiele geeignet. (Hinweis: Nicht kompatibel mit Xbox oder PlayStation. Für nicht erwähnte Spielplattformen wenden Sie sich bitte vor dem Kauf an den Kundendienst.)
profile
kimi-k2.5:cloud
Heute um 18:26
Nachgedacht für 47 Sekunden
Das ist perfekt! (auch wenn er "Overkill" für ein 5-jähriges Kind ist).
Das klingt nach einem GameSir T4 Kaleid oder ähnlichem High-End-Controller. Für dein Projekt besonders wichtig:
✅ Warum der ideal ist:
1. Android-Modus aktivieren (Wichtig!)
Der Controller hat vermutlich mehrere Modi:
Xbox-Modus (Start + X) → Funktioniert meist, aber manchmal komisches Mapping
Android-Modus (meist Start + A oder Start + B für 3 Sekunden gedrückt halten) → Perfektes Mapping für die Web Gamepad API
Test: Wenn der Controller als "Wireless Controller" oder "Android Gamepad" erkannt wird (nicht als Xbox 360 Controller), ist es korrekt.
2. Hall-Sensoren = Kinder-sicher
Normale Controller haben Potentiometer (die kaputt gehen wenn Kinder dran ziehen/zwicken). Hall-Sensoren sind magnetisch - die können die Kinder nicht kaputt machen. Sehr gut für das 5-jährige Kind!
3. Die 2 Rückseiten-Tasten = Eltern-Shortcut
Nutze die programmierbaren Tasten auf der Rückseite für Sonderfunktionen:
Links hinten: Schnell-Speichern (Save-State)
Rechts hinten: Zurück zum Menü (beendet Emulator sicher)
Im Code erweitern:
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
// In deinem gamepad-polling:
if (gp.buttons[6]?.pressed && gp.buttons[7]?.pressed) { // Rückseitentasten
emulator.saveState();
this.showNotification('💾 Gespeichert!');
}
4. Dual-Core für 2 Spieler?
Du hast 2 Controller (2 Stück im Paket?). Perfekt für:
Co-op (wenn das 5-jährige mit Papa spielt)
Hot-Swap (einer lädt in der Station, einer wird gespielt)
⚠️ Was du deaktivieren solltest:
Vibration AUS
Für Retro-Spiele und Kinder:
Batterie sparen (wichtig!)
Weniger ablenkend
In der Hersteller-APP (die du einmalig installierst zum Konfigurieren) oder via Tastenkombination:
Meistens: Turbo + D-Pad runter drücken um Vibration zu toggeln.
1000Hz Polling egal
Das ist für Competitive FPS-Spieler. Für Retro-Spiele (60 FPS) ist 125Hz völlig ausreichend. Spart Batterie, wenn du es runterstellen kannst (optional).
RGB-Beleuchtung aus oder gedimmt
In der App auf "aus" stellen oder sehr dunkel. Spart Strom und ist abends besser für die Augen.
🔧 Konfiguration für dein Projekt
Layout-Einstellung:
Für Retro-Spiele (SNES, NES): Xbox-Layout bevorzugen (nicht Nintendo-Layout)
Oder: Im Emulator-Code das Mapping anpassen (siehe unten)
Test-Setup:
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
// Erweiterte Gamepad-Detection für deinen spezifischen Controller
window.addEventListener('gamepadconnected', (e) => {
console.log('Controller:', e.gamepad.id);
// Sollte anzeigen: "Wireless Controller" oder ähnlich
// Mapping-Check:
const gp = e.gamepad;
alert(`Buttons: ${gp.buttons.length}, Axes: ${gp.axes.length}`);
// Erwartet: 16-18 Buttons, 4 Achsen
});
🎮 Code-Anpassung (Eltern-Menü)
Nutze die Extra-Buttons für das Eltern-Menü statt Start+Select (das könnte das Kind versehentlich drücken):
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// In emulator.pollGamepad():
pollGamepad() {
const check = () => {
const gp = navigator.getGamepads()[0];
if (!gp) return requestAnimationFrame(check);
// Standard-Buttons für Spiel
const isStart = gp.buttons[9]?.pressed; // Xbox-Start
const isSelect = gp.buttons[8]?.pressed; // Xbox-Back
// Rückseiten-Buttons (meist Index 10+11 oder 6+7 je nach Modus)
const isBackLeft = gp.buttons[6]?.pressed; // Paddel links
const isBackRight = gp.buttons[7]?.pressed; // Paddel rechts
// Kombi: Beide Paddel = Sofort-Speichern + Beenden
if (isBackLeft && isBackRight) {
this.saveState();
setTimeout(() => this.quit(), 500);
return; // Stop polling
}
// Einzelnes Paddel: Schnell-Speichern
if (isBackLeft && !this.saveTriggered) {
this.saveState();
this.saveTriggered = true;
setTimeout(() => this.saveTriggered = false, 1000);
}
requestAnimationFrame(check);
};
check();
}
📱 Ladestation-Tipp
Die magnetische Ladestation ist perfekt für das Kinder-Tablet-Setup:
Kind legt Controller rein → lädt automatisch
Kein "Wo ist das Kabel?"-Chaos
1000mAh = ca. 20-30 Stunden Retro-Spielen
Fazit: Der Controller ist über-speckt für ein Kind, aber die Hall-Sensoren und robusten Sticks machen ihn ideal für langlebigen Einsatz. Die Extra-Tasten auf der Rückseite nutzt du für die Eltern-Steuerung (unsichtbar für das Kind).
Brauchst du die Tastenkombination um den Controller in den Android-Modus zu bekommen? (Meistens: Start + A für 3 Sekunden, LED blinkt dann blau oder grün).