"Add legal page (rechtliches.html) with impressum and privacy policy content in German, including styling and structure"

This commit is contained in:
iTob 2026-03-10 22:52:32 +01:00
parent a973b40789
commit d0db293bb8
5 changed files with 430 additions and 63 deletions

View File

@ -38,6 +38,12 @@ StartBackgroundServices();
_serviceProvider.GetRequiredService<MainWindow>().Show();
}
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
TryShutdownCamera("OnSessionEnding");
base.OnSessionEnding(e);
}
protected override void OnExit(ExitEventArgs e)
{
try
@ -50,6 +56,8 @@ catch (Exception ex)
System.Diagnostics.Debug.WriteLine($"Error stopping upload service: {ex.Message}");
}
TryShutdownCamera("OnExit");
try
{
(_serviceProvider as IDisposable)?.Dispose();
@ -129,4 +137,17 @@ catch (Exception ex)
System.Diagnostics.Debug.WriteLine($"Error starting background services: {ex.Message}");
}
}
private void TryShutdownCamera(string source)
{
try
{
var cameraService = _serviceProvider?.GetService<CameraService>();
cameraService?.CloseSession();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error closing camera session ({source}): {ex.Message}");
}
}
}

View File

@ -1,4 +1,5 @@
using System.IO;
using Timer = System.Timers.Timer;
using System.Threading.Tasks;
using System.Windows;
using CamBooth.App.Core.AppSettings;
@ -21,6 +22,7 @@ public class CameraService : IDisposable
private ICamera? _mainCamera;
private List<ICamera>? _camList;
private bool _isConnected;
private Timer? _shutdownKeepAliveTimer;
/// <summary>Fires whenever the camera delivers a new live-view frame.</summary>
public event Action<Stream>? LiveViewUpdated;
@ -99,6 +101,7 @@ public class CameraService : IDisposable
{
if (_mainCamera?.SessionOpen == true)
{
StopShutdownKeepAlive();
_mainCamera.CloseSession();
_logger.Info("Camera session closed");
}
@ -209,6 +212,9 @@ public class CameraService : IDisposable
_mainCamera.StateChanged += MainCamera_StateChanged;
_mainCamera.DownloadReady += MainCamera_DownloadReady;
_mainCamera.LiveViewUpdated += MainCamera_LiveViewUpdated;
_mainCamera.LiveViewStopped += MainCamera_LiveViewStopped;
_mainCamera.CameraHasShutdown += MainCamera_CameraHasShutdown;
StartShutdownKeepAlive();
}
@ -225,8 +231,6 @@ public class CameraService : IDisposable
{
if (!_mainCamera!.IsLiveViewOn)
_mainCamera.StartLiveView();
else
_mainCamera.StopLiveView();
}
catch (Exception ex)
{
@ -247,11 +251,29 @@ public class CameraService : IDisposable
private void MainCamera_LiveViewUpdated(ICamera sender, Stream img) =>
LiveViewUpdated?.Invoke(img);
private void MainCamera_LiveViewStopped(ICamera sender)
{
if (!_isConnected) return;
_logger.Warning("LiveView stopped; attempting restart.");
try
{
sender.StartLiveView();
}
catch (Exception ex)
{
_logger.Error($"Failed to restart LiveView: {ex.Message}");
}
}
private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter)
{
try
{
if (eventID == StateEventID.WillSoonShutDown || eventID == StateEventID.ShutDownTimerUpdate)
ExtendShutdownTimer();
if (eventID == StateEventID.Shutdown && _isConnected)
Application.Current.Dispatcher.Invoke(CloseSession);
}
@ -294,5 +316,49 @@ public class CameraService : IDisposable
private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) =>
_logger.Error(ex.Message);
#endregion
private void MainCamera_CameraHasShutdown(ICamera sender)
{
try
{
StopShutdownKeepAlive();
}
catch (Exception ex)
{
_logger.Error($"Failed stopping shutdown keep-alive: {ex.Message}");
}
}
#endregion
private void StartShutdownKeepAlive()
{
if (_shutdownKeepAliveTimer != null) return;
_shutdownKeepAliveTimer = new Timer(20_000);
_shutdownKeepAliveTimer.AutoReset = true;
_shutdownKeepAliveTimer.Elapsed += (_, _) => ExtendShutdownTimer();
_shutdownKeepAliveTimer.Start();
}
private void StopShutdownKeepAlive()
{
if (_shutdownKeepAliveTimer == null) return;
_shutdownKeepAliveTimer.Stop();
_shutdownKeepAliveTimer.Dispose();
_shutdownKeepAliveTimer = null;
}
private void ExtendShutdownTimer()
{
if (_mainCamera == null || !_mainCamera.SessionOpen) return;
try
{
_mainCamera.SendCommand(CameraCommand.ExtendShutDownTimer);
}
catch (Exception ex)
{
_logger.Warning($"ExtendShutDownTimer failed: {ex.Message}");
}
}
}

View File

@ -1,12 +1,15 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using CamBooth.App.Core.Logging;
using CamBooth.App.Features.Camera;
using EOSDigital.API;
namespace CamBooth.App.Features.LiveView;
@ -16,6 +19,10 @@ public partial class LiveViewPage : Page, IDisposable
private readonly CameraService _cameraService;
private readonly Logger _logger;
private readonly ImageBrush _bgBrush = new();
private int _targetPixelWidth;
private readonly object _frameLock = new();
private byte[]? _latestFrame;
private int _workerRunning;
public LiveViewPage(Logger logger, CameraService cameraService)
@ -29,12 +36,20 @@ _bgBrush.Stretch = Stretch.UniformToFill;
_bgBrush.AlignmentX = AlignmentX.Center;
_bgBrush.AlignmentY = AlignmentY.Center;
LVCanvas.Background = _bgBrush;
if (LVCanvas.ActualWidth > 0)
_targetPixelWidth = (int)Math.Round(LVCanvas.ActualWidth);
RenderOptions.SetBitmapScalingMode(LVCanvas, BitmapScalingMode.LowQuality);
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 });
transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 });
LVCanvas.RenderTransform = transformGroup;
LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
LVCanvas.SizeChanged += (_, e) =>
{
var width = (int)Math.Round(e.NewSize.Width);
if (width > 0) _targetPixelWidth = width;
};
try
{
@ -59,21 +74,93 @@ _cameraService.Dispose();
private void OnLiveViewUpdated(Stream img)
{
if (img == null) return;
if (!img.CanRead) return;
try
{
using WrapStream s = new(img);
img.Position = 0;
var evfImage = new BitmapImage();
evfImage.BeginInit();
evfImage.StreamSource = s;
evfImage.CacheOption = BitmapCacheOption.OnLoad;
evfImage.EndInit();
evfImage.Freeze();
Application.Current.Dispatcher.BeginInvoke(() => _bgBrush.ImageSource = evfImage);
if (img.CanSeek) img.Position = 0;
using var ms = new MemoryStream();
img.CopyTo(ms);
lock (_frameLock)
{
_latestFrame = ms.ToArray();
}
TryStartWorker();
}
catch (Exception ex)
{
_logger.Error(ex.Message);
_logger.Error($"LiveView update failed: {ex}");
}
}
private void TryStartWorker()
{
if (Interlocked.Exchange(ref _workerRunning, 1) == 1)
return;
_ = Task.Run(ProcessFramesAsync);
}
private async Task ProcessFramesAsync()
{
const int targetFps = 60;
const double minFrameMs = 1000d / targetFps;
try
{
while (true)
{
var sw = Stopwatch.StartNew();
byte[]? frame;
lock (_frameLock)
{
frame = _latestFrame;
_latestFrame = null;
}
if (frame == null)
break;
BitmapImage? evfImage = null;
try
{
using var ms = new MemoryStream(frame);
evfImage = new BitmapImage();
evfImage.BeginInit();
evfImage.StreamSource = ms;
evfImage.CacheOption = BitmapCacheOption.OnLoad;
if (_targetPixelWidth > 0)
evfImage.DecodePixelWidth = _targetPixelWidth;
evfImage.EndInit();
evfImage.Freeze();
}
catch (Exception ex)
{
_logger.Error($"LiveView decode failed: {ex}");
}
if (evfImage != null)
{
await Application.Current.Dispatcher.InvokeAsync(
() => _bgBrush.ImageSource = evfImage,
DispatcherPriority.Render);
}
sw.Stop();
var remainingMs = minFrameMs - sw.Elapsed.TotalMilliseconds;
if (remainingMs > 0)
await Task.Delay((int)remainingMs);
}
}
finally
{
Interlocked.Exchange(ref _workerRunning, 0);
lock (_frameLock)
{
if (_latestFrame != null)
TryStartWorker();
}
}
}
}

View File

@ -160,7 +160,7 @@ public partial class PictureGalleryPage : Page
public async Task ShowPhotoDialogAsync(string picturePath)
{
var imageToShow = new System.Windows.Controls.Image
var imageToShow = new Image
{
MaxHeight = 570,
MaxWidth = 800,

View File

@ -0,0 +1,193 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>Rechtliches Impressum & Datenschutz</title>
<style>
:root { color-scheme: light; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #0f172a;
background: #f8fafc;
}
.wrap {
max-width: 920px;
margin: 28px auto;
padding: 0 16px;
}
header {
background: linear-gradient(135deg, #0f172a 0%, #1f2937 55%, #111827 100%);
color: #fff;
border-radius: 14px;
padding: 22px 22px;
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.18);
}
header h1 {
margin: 0 0 6px 0;
font-size: 28px;
letter-spacing: 0.2px;
}
header p {
margin: 0;
opacity: 0.9;
}
main {
margin-top: 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 20px 22px;
box-shadow: 0 8px 18px rgba(2, 6, 23, 0.06);
}
h2 {
margin: 22px 0 10px 0;
font-size: 18px;
padding-top: 10px;
border-top: 1px solid #eef2f7;
}
h2:first-of-type { border-top: none; padding-top: 0; }
h3 {
margin: 16px 0 6px 0;
font-size: 15px;
color: #111827;
}
.grid {
display: table;
width: 100%;
border-spacing: 0;
}
.row { display: table-row; }
.cell {
display: table-cell;
padding: 6px 0;
vertical-align: top;
}
.k { width: 160px; color: #334155; font-weight: 700; }
.v { color: #0f172a; }
.badge {
display: inline-block;
font-size: 12px;
padding: 2px 10px;
border-radius: 999px;
background: #eef2ff;
color: #3730a3;
border: 1px solid #e0e7ff;
margin-left: 8px;
vertical-align: middle;
}
.callout {
margin: 14px 0;
padding: 12px 14px;
border-radius: 12px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
}
.callout strong { display: block; margin-bottom: 4px; }
ul { margin: 6px 0 10px 18px; }
li { margin: 4px 0; }
code {
background: #f8fafc;
border: 1px solid #e5e7eb;
padding: 0 6px;
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.95em;
}
a { color: #1d4ed8; text-decoration: none; }
a:hover { text-decoration: underline; }
footer {
margin: 12px 2px 0 2px;
color: #475569;
font-size: 12.5px;
}
.muted { color: #475569; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Rechtliches</h1>
<p>Impressum & Datenschutzerklärung für die private FotoboxOnlineGalerie</p>
</header>
<main>
<h2>Impressum <span class="badge">Deutschland</span></h2>
<div class="grid" aria-label="Impressum">
<div class="row">
<div class="cell k">Betreiber</div>
<div class="cell v">Tobias Wohlleben</div>
</div>
<div class="row">
<div class="cell k">Anschrift</div>
<div class="cell v">Grethener Str. 32, 04668 Parthenstein</div>
</div>
<div class="row">
<div class="cell k">EMail</div>
<div class="cell v">info@fgrimma-fotobox.de</div>
</div>
</div>
<h2>Datenschutzerklärung</h2>
<h3>1. Verantwortlicher</h3>
<p>Verantwortlich für die Verarbeitung personenbezogener Daten im Sinne der DatenschutzGrundverordnung (DSGVO) ist die im Impressum genannte Person.</p>
<h3>2. Zweck der Verarbeitung</h3>
<p>Diese Webseite dient der Bereitstellung einer privaten OnlineGalerie (PhotoPrism). Nutzer/Teilnehmer einer Veranstaltung können die mit der Fotobox aufgenommenen Fotos ansehen und herunterladen. Der Zugriff erfolgt über individuelle AlbumShareLinks (z.B. per QRCode).</p>
<h3>3. Art der verarbeiteten Daten</h3>
<ul>
<li><strong>Bilddaten (Fotos)</strong>, auf denen Personen erkennbar sein können.</li>
<li><strong>Technische Zugriffsdaten</strong> (ServerLogfiles), z.B. IPAdresse, Datum/Uhrzeit, aufgerufene Ressourcen, UserAgent.</li>
<li><strong>Nutzungsdaten</strong> (aggregierte Statistikdaten) im Rahmen der Webanalyse mit Umami (selfhosted).</li>
</ul>
<h3>4. Rechtsgrundlagen</h3>
<ul>
<li><strong>Bereitstellung der Galerie / Verarbeitung der Fotos:</strong> Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Die erforderlichen Informationen werden zusätzlich direkt an der Fotobox bereitgestellt.</li>
<li><strong>ServerLogfiles:</strong> Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO) an ITSicherheit, Stabilität, Fehleranalyse und Missbrauchsabwehr.</li>
<li><strong>Webanalyse (Umami):</strong> Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO) an datensparsamer Reichweitenmessung und technischer Optimierung. Umami wird ohne Cookies betrieben.</li>
</ul>
<h3>5. Zugriff auf die Alben / Empfängerkreis</h3>
<p>Die Fotoalben sind <strong>nicht öffentlich indexierbar</strong>. Zugriff erhalten nur Personen, die den jeweiligen AlbumShareLink/QRCode besitzen (ggf. zusätzlich passwortgeschützt). Eine Veröffentlichung der Fotos außerhalb der Galerie erfolgt nicht durch den Betreiber.</p>
<h3>6. Speicherdauer / Löschung</h3>
<p>Die Fotos werden für <strong>6 Monate ab dem Folgetag nach dem Event</strong> vorgehalten und anschließend <strong>automatisch gelöscht</strong>, sofern keine Gründe für eine längere Speicherung bestehen (z.B. zur Abwehr von Rechtsansprüchen im Einzelfall).</p>
<p>ServerLogfiles werden nur so lange gespeichert, wie es für den technischen Betrieb und die Sicherheit erforderlich ist (in der Regel kurzzeitig) und anschließend gelöscht bzw. rotiert.</p>
<h3>7. Hosting / Auftragsverarbeitung</h3>
<p>Die Systeme werden auf einem virtuellen Server (VPS) bei <strong>STRATO</strong> in <strong>Deutschland</strong> betrieben. Der HostingAnbieter verarbeitet dabei technische Daten (z.B. IPAdressen in ServerLogs) im Rahmen der Bereitstellung der Infrastruktur.</p>
<h3>8. Webanalyse mit Umami (selfhosted)</h3>
<p>Zur statistischen Auswertung der Nutzung setzen wir <strong>Umami</strong> ein (selfhosted). Umami wird <strong>ohne Cookies</strong> betrieben. Es werden keine Profile über einzelne Besucher erstellt. Die Auswertung erfolgt in aggregierter Form (z.B. Seitenaufrufe). Da Umami auf dem eigenen Server betrieben wird, werden keine Analysedaten an einen Drittanbieter übermittelt.</p>
<h3>9. Sicherheit (HTTPS)</h3>
<p>Die Verbindung zu dieser Webseite erfolgt verschlüsselt (HTTPS), um Inhalte und Zugangsdaten während der Übertragung zu schützen.</p>
<h3>10. Rechte der betroffenen Personen</h3>
<p>Du hast nach der DSGVO insbesondere folgende Rechte: Auskunft (Art. 15), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20, soweit anwendbar), Widerspruch (Art. 21) sowie Widerruf erteilter Einwilligungen mit Wirkung für die Zukunft (Art. 7 Abs. 3).</p>
<p>Wenn du die Entfernung eines Fotos wünschst, schreibe bitte an <strong>info@grimma-fotobox.de</strong> und nenne (wenn möglich) das Event/Album und das betroffene Foto.</p>
<h3>11. Beschwerderecht</h3>
<p>Du hast das Recht, dich bei einer Datenschutzaufsichtsbehörde zu beschweren (Art. 77 DSGVO).</p>
<h3>12. Stand</h3>
<p>Stand: <span class="mono">10.03.2026</span></p>
</main>
</div>
</body>
</html>