"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(); _serviceProvider.GetRequiredService<MainWindow>().Show();
} }
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
TryShutdownCamera("OnSessionEnding");
base.OnSessionEnding(e);
}
protected override void OnExit(ExitEventArgs e) protected override void OnExit(ExitEventArgs e)
{ {
try try
@ -50,6 +56,8 @@ catch (Exception ex)
System.Diagnostics.Debug.WriteLine($"Error stopping upload service: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"Error stopping upload service: {ex.Message}");
} }
TryShutdownCamera("OnExit");
try try
{ {
(_serviceProvider as IDisposable)?.Dispose(); (_serviceProvider as IDisposable)?.Dispose();
@ -129,4 +137,17 @@ catch (Exception ex)
System.Diagnostics.Debug.WriteLine($"Error starting background services: {ex.Message}"); 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 System.IO;
using Timer = System.Timers.Timer;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.AppSettings;
@ -21,6 +22,7 @@ public class CameraService : IDisposable
private ICamera? _mainCamera; private ICamera? _mainCamera;
private List<ICamera>? _camList; private List<ICamera>? _camList;
private bool _isConnected; private bool _isConnected;
private Timer? _shutdownKeepAliveTimer;
/// <summary>Fires whenever the camera delivers a new live-view frame.</summary> /// <summary>Fires whenever the camera delivers a new live-view frame.</summary>
public event Action<Stream>? LiveViewUpdated; public event Action<Stream>? LiveViewUpdated;
@ -99,6 +101,7 @@ public class CameraService : IDisposable
{ {
if (_mainCamera?.SessionOpen == true) if (_mainCamera?.SessionOpen == true)
{ {
StopShutdownKeepAlive();
_mainCamera.CloseSession(); _mainCamera.CloseSession();
_logger.Info("Camera session closed"); _logger.Info("Camera session closed");
} }
@ -209,6 +212,9 @@ public class CameraService : IDisposable
_mainCamera.StateChanged += MainCamera_StateChanged; _mainCamera.StateChanged += MainCamera_StateChanged;
_mainCamera.DownloadReady += MainCamera_DownloadReady; _mainCamera.DownloadReady += MainCamera_DownloadReady;
_mainCamera.LiveViewUpdated += MainCamera_LiveViewUpdated; _mainCamera.LiveViewUpdated += MainCamera_LiveViewUpdated;
_mainCamera.LiveViewStopped += MainCamera_LiveViewStopped;
_mainCamera.CameraHasShutdown += MainCamera_CameraHasShutdown;
StartShutdownKeepAlive();
} }
@ -225,8 +231,6 @@ public class CameraService : IDisposable
{ {
if (!_mainCamera!.IsLiveViewOn) if (!_mainCamera!.IsLiveViewOn)
_mainCamera.StartLiveView(); _mainCamera.StartLiveView();
else
_mainCamera.StopLiveView();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -247,11 +251,29 @@ public class CameraService : IDisposable
private void MainCamera_LiveViewUpdated(ICamera sender, Stream img) => private void MainCamera_LiveViewUpdated(ICamera sender, Stream img) =>
LiveViewUpdated?.Invoke(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) private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter)
{ {
try try
{ {
if (eventID == StateEventID.WillSoonShutDown || eventID == StateEventID.ShutDownTimerUpdate)
ExtendShutdownTimer();
if (eventID == StateEventID.Shutdown && _isConnected) if (eventID == StateEventID.Shutdown && _isConnected)
Application.Current.Dispatcher.Invoke(CloseSession); Application.Current.Dispatcher.Invoke(CloseSession);
} }
@ -294,5 +316,49 @@ public class CameraService : IDisposable
private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) => private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) =>
_logger.Error(ex.Message); _logger.Error(ex.Message);
private void MainCamera_CameraHasShutdown(ICamera sender)
{
try
{
StopShutdownKeepAlive();
}
catch (Exception ex)
{
_logger.Error($"Failed stopping shutdown keep-alive: {ex.Message}");
}
}
#endregion #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,79 +1,166 @@
using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Threading;
using CamBooth.App.Core.Logging; using CamBooth.App.Core.Logging;
using CamBooth.App.Features.Camera; using CamBooth.App.Features.Camera;
using EOSDigital.API; using EOSDigital.API;
namespace CamBooth.App.Features.LiveView; namespace CamBooth.App.Features.LiveView;
public partial class LiveViewPage : Page, IDisposable public partial class LiveViewPage : Page, IDisposable
{ {
private readonly CameraService _cameraService; private readonly CameraService _cameraService;
private readonly Logger _logger; private readonly Logger _logger;
private readonly ImageBrush _bgBrush = new(); 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) public LiveViewPage(Logger logger, CameraService cameraService)
{ {
_logger = logger; _logger = logger;
_cameraService = cameraService; _cameraService = cameraService;
InitializeComponent(); InitializeComponent();
_bgBrush.Stretch = Stretch.UniformToFill; _bgBrush.Stretch = Stretch.UniformToFill;
_bgBrush.AlignmentX = AlignmentX.Center; _bgBrush.AlignmentX = AlignmentX.Center;
_bgBrush.AlignmentY = AlignmentY.Center; _bgBrush.AlignmentY = AlignmentY.Center;
LVCanvas.Background = _bgBrush; LVCanvas.Background = _bgBrush;
if (LVCanvas.ActualWidth > 0)
_targetPixelWidth = (int)Math.Round(LVCanvas.ActualWidth);
RenderOptions.SetBitmapScalingMode(LVCanvas, BitmapScalingMode.LowQuality);
var transformGroup = new TransformGroup(); var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 }); transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 });
transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 }); transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 });
LVCanvas.RenderTransform = transformGroup; LVCanvas.RenderTransform = transformGroup;
LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5); 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 try
{ {
cameraService.ConnectCamera(); cameraService.ConnectCamera();
cameraService.LiveViewUpdated += OnLiveViewUpdated; cameraService.LiveViewUpdated += OnLiveViewUpdated;
_logger.Info("LiveViewPage initialized successfully"); _logger.Info("LiveViewPage initialized successfully");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error($"Failed to initialize LiveViewPage: {ex.Message}"); _logger.Error($"Failed to initialize LiveViewPage: {ex.Message}");
throw; throw;
} }
} }
public void Dispose() public void Dispose()
{ {
_cameraService.LiveViewUpdated -= OnLiveViewUpdated; _cameraService.LiveViewUpdated -= OnLiveViewUpdated;
_cameraService.Dispose(); _cameraService.Dispose();
} }
private void OnLiveViewUpdated(Stream img) private void OnLiveViewUpdated(Stream img)
{ {
try if (img == null) return;
{ if (!img.CanRead) return;
using WrapStream s = new(img);
img.Position = 0; try
var evfImage = new BitmapImage(); {
evfImage.BeginInit(); if (img.CanSeek) img.Position = 0;
evfImage.StreamSource = s; using var ms = new MemoryStream();
evfImage.CacheOption = BitmapCacheOption.OnLoad; img.CopyTo(ms);
evfImage.EndInit(); lock (_frameLock)
evfImage.Freeze(); {
Application.Current.Dispatcher.BeginInvoke(() => _bgBrush.ImageSource = evfImage); _latestFrame = ms.ToArray();
} }
catch (Exception ex) TryStartWorker();
{ }
_logger.Error(ex.Message); catch (Exception ex)
} {
} _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) public async Task ShowPhotoDialogAsync(string picturePath)
{ {
var imageToShow = new System.Windows.Controls.Image var imageToShow = new Image
{ {
MaxHeight = 570, MaxHeight = 570,
MaxWidth = 800, 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>