Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
# Beispielkonfiguration fuer die Flask-gestuetzte FPP-Steuerung
# Werte nach Bedarf anpassen und als ".env" verwenden.

SITE_NAME="Brauns Lichtershow"
SITE_SUBTITLE="Fernsteuerung für den Falcon Player"
SITE_NAME="🎄 Meine Lichtershow 🎄"
SITE_SUBTITLE="📅 Dezember 2025 📅\n⏰ Täglich 17-21 Uhr ⏰\n🏠 Musterstraße 1, Musterstadt 🏠"
FPP_BASE_URL="http://fpp.local"
FPP_PLAYLIST_SHOW="show 1"
FPP_PLAYLIST_KIDS="show 2"
FPP_PLAYLIST_REQUESTS="all songs"
FPP_BACKGROUND_EFFECT="background"
FPP_PLAYLIST_SHOW="show"
FPP_PLAYLIST_KIDS="kids"
FPP_PLAYLIST_REQUESTS="wishlist"
FPP_PLAYLIST_IDLE="background"
FPP_POLL_INTERVAL_MS=15000
CLIENT_STATUS_POLL_MS=10000
DONATION_PAYPAL="spender@example.com"
DONATION_TEXT="Hilf uns, die Show am Laufen zu halten!"
FPP_SHOW_START_DATE=2025-12-01
FPP_SHOW_END_DATE=2026-01-06
DONATION_POOL_ID='abc123?sr=example'
DONATION_CAMPAIGN_NAME="Spendenaktion Lichtershow"
DONATION_SUBTITLE="Unterstütze unsere Lichtershow!"
DONATION_TEXT="Deine Spende hilft uns, die Lichtershow auch im nächsten Jahr zu ermöglichen."
PREVIEW_MODE=false
ACCESS_CODE=1234
ACCESS_CODE=12345

# Social Media Links (optional, leer lassen wenn nicht genutzt)
# Icons werden im Footer angezeigt, wenn die Variable einen Wert hat
SOCIAL_FACEBOOK=
SOCIAL_INSTAGRAM=
SOCIAL_TIKTOK=
SOCIAL_WHATSAPP=
SOCIAL_YOUTUBE=
SOCIAL_WEBSITE=
SOCIAL_EMAIL=
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
# Falcon Player Weihnachts-Steuerung
# Falcon Player Web Control

Serverseitige (Python/Flask) Steuer-Seite für den Falcon Player (FPP). Der Container kapselt alle API-Aufrufe, verwaltet Wunsch-Queue und Scheduling und liefert die festliche Mobil-Oberfläche direkt aus.

**Netzwerk-Hinweis:** Diese Web-Applikation sollte im gleichen LAN wie der Falcon Player betrieben werden. Über Port-Weiterleitungen im Router und DynDNS kann die lokal betriebene Web-Applikation für Besucher über das Internet erreichbar gemacht werden, ohne dass diese direkt mit dem Falcon Player kommunizieren müssen.

```
┌─────────────────────────────────────────────────┐
│ Lokales Netzwerk (LAN) │
│ │
┌──────────────┐ │ ┌─────────────────┐ ┌─────────────────┐ │
│ Besucher │ │ │ FPP Web │ │ Falcon Player │ │
│ (Handy/ │ ──────────► │ │ Control │ ──► │ (FPP) │ │
│ Browser) │ Internet │ │ Container │ │ 192.168.x.x │ │
└──────────────┘ │ │ :8080 │ └─────────────────┘ │
│ │ └─────────────────┘ │
│ │ ▲ │
│ └────────────│────────────────────────────────────┘
│ │
│ ┌─────────────────┐ │
└──────► │ Router │ ───────────┘
DynDNS / │ Port-Weiter- │ Port 8080
öffentl. IP │ leitung :8080 │
└─────────────────┘
```

**Vorteile dieser Architektur:**
- Besucher kommunizieren nur mit der Web-App, nicht direkt mit dem FPP
- Der Falcon Player bleibt im geschützten LAN
- Die Web-App übernimmt Authentifizierung und Zugangskontrolle

## Funktionen
- Drei große Aktions-Buttons: "Show starten", "Kids-Show starten" und "Lied wünschen".
- Header mit konfigurierbarem Namen (z.B. "Brauns Lichtershow").
Expand All @@ -25,7 +52,7 @@ FPP_BASE_URL=http://fpp.local
FPP_PLAYLIST_SHOW=show 1
FPP_PLAYLIST_KIDS=show 2
FPP_PLAYLIST_REQUESTS=all songs
FPP_BACKGROUND_EFFECT=background
FPP_PLAYLIST_IDLE=background
FPP_SHOW_START_DATE=2024-12-01
FPP_SHOW_END_DATE=2025-01-06
FPP_POLL_INTERVAL_MS=15000
Expand All @@ -37,6 +64,14 @@ DONATION_SUBTITLE=Unterstütze die Lichtershow
DONATION_TEXT=
PREVIEW_MODE=false
ACCESS_CODE=1234
# Social Media Links
SOCIAL_FACEBOOK=https://facebook.com/example
SOCIAL_INSTAGRAM=https://instagram.com/example
SOCIAL_TIKTOK=
SOCIAL_WHATSAPP=
SOCIAL_YOUTUBE=https://youtube.com/@example
SOCIAL_WEBSITE=https://example.com
SOCIAL_EMAIL=kontakt@example.com
```

Eine ausfüllbare Vorlage liegt als `.env.example` bei.
Expand All @@ -47,7 +82,7 @@ Parameter im Überblick:
- `FPP_BASE_URL`: Basis-URL des FPP (z.B. `http://fpp.local`).
- `FPP_PLAYLIST_SHOW`, `FPP_PLAYLIST_KIDS`: Namen der regulären Shows.
- `FPP_PLAYLIST_REQUESTS`: Playlist mit allen verfügbaren Liedern für Wünsche.
- `FPP_BACKGROUND_EFFECT`: Name der Background-Sequence/Effect, die außerhalb von Shows/Wünschen laufen soll.
- `FPP_PLAYLIST_IDLE`: Name der Idle-Playlist, die außerhalb von Shows/Wünschen laufen soll.
- `FPP_SHOW_START_DATE`, `FPP_SHOW_END_DATE`: Optionales Start-/Enddatum (`YYYY-MM-DD`) für das automatische Stundenscheduling. Außerhalb des Fensters werden keine Shows automatisch gestartet.
- `FPP_POLL_INTERVAL_MS`: Server-seitiges Status-Abfrageintervall in Millisekunden.
- `CLIENT_STATUS_POLL_MS`: Polling-Intervall, mit dem der Browser den Server nach dem Status fragt.
Expand All @@ -58,6 +93,13 @@ Parameter im Überblick:
- `DONATION_TEXT`: Freier Beschreibungstext auf der Spendenseite. Leer lassen, wenn kein Text eingeblendet werden soll.
- `PREVIEW_MODE`: `true`, um generierte Beispielinhalte (Status, Countdown, Wunschliste) anzuzeigen, falls kein FPP angebunden ist oder nur ein schneller Screenshot benötigt wird.
- `ACCESS_CODE`: Optionaler Zugangscode. Wenn gesetzt, zeigt die Startseite zunächst ein großes Eingabefeld; nach korrektem Code wird die Steuerung freigeschaltet (wird pro Gerät im `localStorage` gemerkt).
- `SOCIAL_FACEBOOK`: URL zur Facebook-Seite. Wenn gesetzt, wird im Footer ein Facebook-Icon angezeigt.
- `SOCIAL_INSTAGRAM`: URL zum Instagram-Profil. Wenn gesetzt, wird im Footer ein Instagram-Icon angezeigt.
- `SOCIAL_TIKTOK`: URL zum TikTok-Profil. Wenn gesetzt, wird im Footer ein TikTok-Icon angezeigt.
- `SOCIAL_WHATSAPP`: URL/Link zu WhatsApp (z.B. `https://wa.me/491234567890`). Wenn gesetzt, wird im Footer ein WhatsApp-Icon angezeigt.
- `SOCIAL_YOUTUBE`: URL zum YouTube-Kanal. Wenn gesetzt, wird im Footer ein YouTube-Icon angezeigt.
- `SOCIAL_WEBSITE`: URL zur eigenen Website. Wenn gesetzt, wird im Footer ein Website-Icon angezeigt.
- `SOCIAL_EMAIL`: E-Mail-Adresse für Kontakt. Wenn gesetzt, wird im Footer ein E-Mail-Icon mit `mailto:`-Link angezeigt.

## Betrieb mit Docker Compose

Expand Down
9 changes: 8 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,12 @@ window.FPP_CONFIG = {
donationSubtitle: 'Unterstütze die Lichtershow',
donationText: 'Vielen Dank für deine Unterstützung!',
previewMode: true,
accessCode: '1234'
accessCode: '1234',
socialFacebook: 'https://facebook.com/example',
socialInstagram: 'https://instagram.com/example',
socialTiktok: '',
socialWhatsapp: '',
socialYoutube: 'https://youtube.com/@example',
socialWebsite: 'https://example.com',
socialEmail: 'kontakt@example.com'
};
9 changes: 8 additions & 1 deletion config.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,12 @@ window.FPP_CONFIG = {
donationSubtitle: '${DONATION_SUBTITLE:-Unterstütze die Lichtershow}',
donationText: '${DONATION_TEXT-}',
previewMode: ${PREVIEW_MODE:-false},
accessCode: '${ACCESS_CODE:-}'
accessCode: '${ACCESS_CODE:-}',
socialFacebook: '${SOCIAL_FACEBOOK:-}',
socialInstagram: '${SOCIAL_INSTAGRAM:-}',
socialTiktok: '${SOCIAL_TIKTOK:-}',
socialWhatsapp: '${SOCIAL_WHATSAPP:-}',
socialYoutube: '${SOCIAL_YOUTUBE:-}',
socialWebsite: '${SOCIAL_WEBSITE:-}',
socialEmail: '${SOCIAL_EMAIL:-}'
};
14 changes: 14 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ set -e
: "${DONATION_TEXT:=}"
: "${PREVIEW_MODE:=false}"
: "${ACCESS_CODE:=}"
: "${SOCIAL_FACEBOOK:=}"
: "${SOCIAL_INSTAGRAM:=}"
: "${SOCIAL_TIKTOK:=}"
: "${SOCIAL_WHATSAPP:=}"
: "${SOCIAL_YOUTUBE:=}"
: "${SOCIAL_WEBSITE:=}"
: "${SOCIAL_EMAIL:=}"

# generate config.js for the frontend
python - <<'PY'
Expand All @@ -39,6 +46,13 @@ config = {
"previewMode": os.getenv("PREVIEW_MODE", "false").lower()
in ["true", "1", "yes", "on"],
"accessCode": os.getenv("ACCESS_CODE", ""),
"socialFacebook": os.getenv("SOCIAL_FACEBOOK", ""),
"socialInstagram": os.getenv("SOCIAL_INSTAGRAM", ""),
"socialTiktok": os.getenv("SOCIAL_TIKTOK", ""),
"socialWhatsapp": os.getenv("SOCIAL_WHATSAPP", ""),
"socialYoutube": os.getenv("SOCIAL_YOUTUBE", ""),
"socialWebsite": os.getenv("SOCIAL_WEBSITE", ""),
"socialEmail": os.getenv("SOCIAL_EMAIL", ""),
}

with open("config.js", "w", encoding="utf-8") as f:
Expand Down
70 changes: 70 additions & 0 deletions donation.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Unterstützung – FPP Weihnachtssteuerung</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="styles.css">
</head>
<body class="festive subpage">
Expand Down Expand Up @@ -31,6 +32,11 @@ <h1 id="headline"></h1>

<button class="link-like" id="back-home">Zurück zur Startseite</button>
</div>

<footer class="social-footer hidden" id="social-footer">
<h3 class="social-footer-title">Unsere Kanäle:</h3>
<div class="social-icons" id="social-icons"></div>
</footer>
</div>

<script src="config.js"></script>
Expand Down Expand Up @@ -88,6 +94,70 @@ <h1 id="headline"></h1>
backHome.addEventListener('click', () => {
window.location.href = '/';
});

// Social Media Footer
function renderSocialFooter() {
const socialFooter = document.getElementById('social-footer');
const socialIcons = document.getElementById('social-icons');
const socialLinks = [
{ key: 'socialFacebook', icon: 'fa-brands fa-facebook-f', label: 'Facebook' },
{ key: 'socialInstagram', icon: 'fa-brands fa-instagram', label: 'Instagram' },
{ key: 'socialTiktok', icon: 'fa-brands fa-tiktok', label: 'TikTok' },
{ key: 'socialWhatsapp', icon: 'fa-brands fa-whatsapp', label: 'WhatsApp' },
{ key: 'socialYoutube', icon: 'fa-brands fa-youtube', label: 'YouTube' },
{ key: 'socialWebsite', icon: 'fa-solid fa-globe', label: 'Website' },
{ key: 'socialEmail', icon: 'fa-solid fa-envelope', label: 'E-Mail', isEmail: true }
];

function isValidUrl(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}

let hasAny = false;
socialLinks.forEach(({ key, icon, label, isEmail }) => {
const value = (config[key] || '').trim();
if (value) {
if (isEmail) {
hasAny = true;
const link = document.createElement('a');
link.className = 'social-icon';
link.href = `mailto:${value}`;
link.setAttribute('aria-label', label);
link.title = label;
const iconEl = document.createElement('i');
iconEl.className = icon;
iconEl.setAttribute('aria-hidden', 'true');
link.appendChild(iconEl);
socialIcons.appendChild(link);
} else if (isValidUrl(value)) {
hasAny = true;
const link = document.createElement('a');
link.className = 'social-icon';
link.href = value;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.setAttribute('aria-label', label);
link.title = label;
const iconEl = document.createElement('i');
iconEl.className = icon;
iconEl.setAttribute('aria-hidden', 'true');
link.appendChild(iconEl);
socialIcons.appendChild(link);
}
}
});

if (hasAny) {
socialFooter.classList.remove('hidden');
}
}

renderSocialFooter();
</script>
</body>
</html>
69 changes: 69 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FPP Weihnachtssteuerung</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="styles.css">
</head>
<body class="festive">
Expand Down Expand Up @@ -54,6 +55,11 @@ <h3>Warteschlange</h3>
</div>
<p class="note" id="note"></p>
</div>

<footer class="social-footer hidden" id="social-footer">
<h3 class="social-footer-title">Unsere Kanäle:</h3>
<div class="social-icons" id="social-icons"></div>
</footer>
</div>

<script src="config.js"></script>
Expand Down Expand Up @@ -338,6 +344,69 @@ <h3>Warteschlange</h3>
}
});

// Social Media Footer
function renderSocialFooter() {
const socialFooter = document.getElementById('social-footer');
const socialIcons = document.getElementById('social-icons');
const socialLinks = [
{ key: 'socialFacebook', icon: 'fa-brands fa-facebook-f', label: 'Facebook' },
{ key: 'socialInstagram', icon: 'fa-brands fa-instagram', label: 'Instagram' },
{ key: 'socialTiktok', icon: 'fa-brands fa-tiktok', label: 'TikTok' },
{ key: 'socialWhatsapp', icon: 'fa-brands fa-whatsapp', label: 'WhatsApp' },
{ key: 'socialYoutube', icon: 'fa-brands fa-youtube', label: 'YouTube' },
{ key: 'socialWebsite', icon: 'fa-solid fa-globe', label: 'Website' },
{ key: 'socialEmail', icon: 'fa-solid fa-envelope', label: 'E-Mail', isEmail: true }
];

function isValidUrl(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}

let hasAny = false;
socialLinks.forEach(({ key, icon, label, isEmail }) => {
const value = (config[key] || '').trim();
if (value) {
if (isEmail) {
hasAny = true;
const link = document.createElement('a');
link.className = 'social-icon';
link.href = `mailto:${value}`;
link.setAttribute('aria-label', label);
link.title = label;
const iconEl = document.createElement('i');
iconEl.className = icon;
iconEl.setAttribute('aria-hidden', 'true');
link.appendChild(iconEl);
socialIcons.appendChild(link);
} else if (isValidUrl(value)) {
hasAny = true;
const link = document.createElement('a');
link.className = 'social-icon';
link.href = value;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.setAttribute('aria-label', label);
link.title = label;
const iconEl = document.createElement('i');
iconEl.className = icon;
iconEl.setAttribute('aria-hidden', 'true');
link.appendChild(iconEl);
socialIcons.appendChild(link);
}
}
});

if (hasAny) {
socialFooter.classList.remove('hidden');
}
}

renderSocialFooter();
validateAccess();
</script>
</body>
Expand Down
Loading