qr code
This commit is contained in:
parent
28ae78cea7
commit
097de823c0
6
src/CamBooth/.run/Publish CamBooth.App to folder.run.xml
Normal file
6
src/CamBooth/.run/Publish CamBooth.App to folder.run.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Publish CamBooth.App to folder" type="DotNetFolderPublish" factoryName="Publish to folder">
|
||||
<riderPublish configuration="Release" platform="Any CPU" target_folder="$PROJECT_DIR$/CamBooth.App/bin/Release/net8.0-windows/publish" target_framework="net8.0-windows" uuid_high="-4544498340232084923" uuid_low="-5774556574018673293" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
|
||||
using CamBooth.App.Core.AppSettings;
|
||||
@ -51,6 +51,7 @@ public partial class App : Application
|
||||
services.AddSingleton<AppSettingsService>();
|
||||
services.AddSingleton<PictureGalleryService>();
|
||||
services.AddSingleton<LycheeUploadService>();
|
||||
services.AddSingleton<UploadQueueService>();
|
||||
services.AddSingleton<CameraService>();
|
||||
|
||||
// Zuerst den Provider bauen, um AppSettings zu laden
|
||||
@ -104,7 +105,42 @@ public partial class App : Application
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Starte UploadQueueService beim Start
|
||||
try
|
||||
{
|
||||
var uploadQueueService = _serviceProvider.GetRequiredService<UploadQueueService>();
|
||||
uploadQueueService.Start();
|
||||
|
||||
// Scan für fehlgeschlagene Uploads beim Start
|
||||
uploadQueueService.ScanAndQueueFailedUploads();
|
||||
|
||||
logger.Info("UploadQueueService initialisiert und gestartet");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Fehler beim Start des UploadQueueService: {ex.Message}");
|
||||
}
|
||||
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
// Stoppe UploadQueueService beim Beenden der App
|
||||
try
|
||||
{
|
||||
var uploadQueueService = _serviceProvider?.GetService<UploadQueueService>();
|
||||
if (uploadQueueService != null)
|
||||
{
|
||||
uploadQueueService.StopAsync().Wait(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Fehler beim Stoppen des UploadQueueService: {ex.Message}");
|
||||
}
|
||||
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
@ -9,62 +9,67 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EDSDKLib\EDSDKLib.csproj" />
|
||||
<ProjectReference Include="..\EDSDKLib\EDSDKLib.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||
<PackageReference Include="WPF-UI" Version="4.0.0-rc.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1"/>
|
||||
<PackageReference Include="Serilog" Version="4.3.1"/>
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Http" Version="9.0.0"/>
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4"/>
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0"/>
|
||||
<PackageReference Include="WPF-UI" Version="4.0.0-rc.3"/>
|
||||
<PackageReference Include="QRCoder" Version="1.4.3"/>
|
||||
<!-- Material Design Icons -->
|
||||
<PackageReference Include="MaterialDesignThemes" Version="5.1.0"/>
|
||||
<PackageReference Include="MaterialDesignColors" Version="3.1.0"/>
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Features\" />
|
||||
<Folder Include="Features\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="artifacts\**\*.cs" />
|
||||
<EmbeddedResource Remove="artifacts\**\*" />
|
||||
<None Remove="artifacts\**\*" />
|
||||
<Page Remove="artifacts\**\*.xaml" />
|
||||
<Compile Remove="artifacts\**\*.cs"/>
|
||||
<EmbeddedResource Remove="artifacts\**\*"/>
|
||||
<None Remove="artifacts\**\*"/>
|
||||
<Page Remove="artifacts\**\*.xaml"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Core\AppSettings\app.settings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Core\AppSettings\app.settings.dev.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Core\AppSettings\app.settings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Core\AppSettings\app.settings.dev.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Features\DebugConsole\DebugConsolePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Features\LiveView\LiveViewPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Features\PictureGallery\PictureGalleryPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Features\DebugConsole\DebugConsolePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Features\LiveView\LiveViewPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Features\PictureGallery\PictureGalleryPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
||||
"Username": "itob",
|
||||
"Password": "VfVyqal&Nv8U&P",
|
||||
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
|
||||
"DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
|
||||
"AutoUploadEnabled": true
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
|
||||
@ -44,8 +44,8 @@
|
||||
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
||||
"Username": "itob",
|
||||
"Password": "VfVyqal&Nv8U&P",
|
||||
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
|
||||
"AutoUploadEnabled": false
|
||||
"DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
|
||||
"AutoUploadEnabled": true
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;"
|
||||
|
||||
@ -6,9 +6,19 @@
|
||||
xmlns:local="clr-namespace:CamBooth.App.Core"
|
||||
mc:Ignorable="d"
|
||||
Title="GenericOverlayWindow" Height="350" Width="600">
|
||||
<Window.Resources>
|
||||
<ResourceDictionary Source="pack://application:,,,/CamBooth.App;component/Resources/ButtonStyles.xaml" />
|
||||
</Window.Resources>
|
||||
<Grid>
|
||||
<TextBlock Name="TbHeader" FontSize="40" HorizontalAlignment="Center">Hoppla</TextBlock>
|
||||
<TextBlock Name="TbContent" FontSize="25" HorizontalAlignment="Center" VerticalAlignment="Center">Sorry, da ging was schief!</TextBlock>
|
||||
<Button Click="ButtonBase_OnClick" Width="300" Height="100" FontSize="25" HorizontalAlignment="Center" VerticalAlignment="Bottom" Content="Schließen"></Button>
|
||||
<Button Click="ButtonBase_OnClick"
|
||||
Width="300"
|
||||
Height="100"
|
||||
FontSize="25"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Content="Schließen"
|
||||
Style="{StaticResource PrimaryActionButtonStyle}"></Button>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@ -68,10 +68,16 @@ public class Logger : IDisposable
|
||||
public Logger(IConfiguration configuration)
|
||||
{
|
||||
var logLevel = configuration["LoggingSettings:LogLevel"] ?? "Information";
|
||||
var logDirectory = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
|
||||
var logDirectoryInput = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
|
||||
var remoteServerUrl = configuration["LoggingSettings:RemoteServerUrl"];
|
||||
var remoteServerApiKey = configuration["LoggingSettings:RemoteServerApiKey"];
|
||||
|
||||
// Konvertiere zu absolutem Pfad
|
||||
var logDirectory = Path.IsPathRooted(logDirectoryInput)
|
||||
? logDirectoryInput
|
||||
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logDirectoryInput);
|
||||
|
||||
// Erstelle Verzeichnis, falls nicht vorhanden
|
||||
if (!Directory.Exists(logDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
|
||||
@ -22,6 +22,8 @@ public class CameraService : IDisposable
|
||||
|
||||
private readonly LycheeUploadService _lycheeUploadService;
|
||||
|
||||
private readonly UploadQueueService _uploadQueueService;
|
||||
|
||||
private readonly ICanonAPI _APIHandler;
|
||||
|
||||
private CameraValue[] AvList;
|
||||
@ -47,6 +49,7 @@ public class CameraService : IDisposable
|
||||
AppSettingsService appSettings,
|
||||
PictureGalleryService pictureGalleryService,
|
||||
LycheeUploadService lycheeUploadService,
|
||||
UploadQueueService uploadQueueService,
|
||||
ICamera mainCamera,
|
||||
ICanonAPI APIHandler)
|
||||
{
|
||||
@ -54,6 +57,7 @@ public class CameraService : IDisposable
|
||||
this._appSettings = appSettings;
|
||||
this._pictureGalleryService = pictureGalleryService;
|
||||
this._lycheeUploadService = lycheeUploadService;
|
||||
this._uploadQueueService = uploadQueueService;
|
||||
this._mainCamera = mainCamera;
|
||||
this._APIHandler = APIHandler;
|
||||
try
|
||||
@ -89,21 +93,43 @@ public class CameraService : IDisposable
|
||||
try
|
||||
{
|
||||
this.RefreshCamera();
|
||||
List<ICamera> cameraList = this._APIHandler.GetCameraList();
|
||||
if (cameraList.Any())
|
||||
|
||||
// Retry logic for camera detection (some systems need time to initialize)
|
||||
int maxRetries = 3;
|
||||
int retryDelay = 500; // milliseconds
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
this.OpenSession();
|
||||
this.SetSettingSaveToComputer();
|
||||
this.StarLiveView();
|
||||
if (this.CamList != null && this.CamList.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
this._logger.Info($"No cameras found on attempt {attempt + 1}/{maxRetries}, retrying in {retryDelay}ms...");
|
||||
System.Threading.Thread.Sleep(retryDelay);
|
||||
this.RefreshCamera();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (this.CamList == null || !this.CamList.Any())
|
||||
{
|
||||
this.ReportError("No cameras / devices found");
|
||||
return;
|
||||
throw new InvalidOperationException("No cameras / devices found after multiple attempts");
|
||||
}
|
||||
|
||||
string cameraDeviceNames = string.Join(", ", cameraList.Select(cam => cam.DeviceName));
|
||||
this._logger.Info($"Found {this.CamList.Count} camera(s)");
|
||||
string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName));
|
||||
this._logger.Info(cameraDeviceNames);
|
||||
|
||||
// Update _mainCamera reference to the freshly detected camera
|
||||
this._mainCamera = this.CamList[0];
|
||||
this._logger.Info($"Selected camera: {this._mainCamera.DeviceName}");
|
||||
|
||||
this.OpenSession();
|
||||
this.SetSettingSaveToComputer();
|
||||
this.StarLiveView();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -161,13 +187,11 @@ public class CameraService : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.CamList == null || this.CamList.Count == 0)
|
||||
if (this._mainCamera == null)
|
||||
{
|
||||
throw new InvalidOperationException("No cameras available in camera list");
|
||||
throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first.");
|
||||
}
|
||||
|
||||
this._mainCamera = this.CamList[0];
|
||||
|
||||
// Check if session is already open
|
||||
if (this._mainCamera.SessionOpen)
|
||||
{
|
||||
@ -377,34 +401,9 @@ public class CameraService : IDisposable
|
||||
this._pictureGalleryService.LoadThumbnailsToCache();
|
||||
});
|
||||
|
||||
// Auto-Upload zu Lychee, falls aktiviert
|
||||
if (this._appSettings.LycheeAutoUploadEnabled)
|
||||
{
|
||||
this._logger.Info("Auto-Upload aktiviert. Starte Upload zu Lychee...");
|
||||
|
||||
// Upload im Hintergrund, damit die Fotobox nicht blockiert wird
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var albumId = this._appSettings.LycheeDefaultAlbumId;
|
||||
var uploadSuccess = await this._lycheeUploadService.UploadImageAsync(savedPhotoPath, albumId);
|
||||
|
||||
if (uploadSuccess)
|
||||
{
|
||||
this._logger.Info($"✅ Lychee-Upload erfolgreich: {Info.FileName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.Warning($"⚠️ Lychee-Upload fehlgeschlagen: {Info.FileName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Error($"❌ Fehler beim Lychee-Upload: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
// Füge neues Foto zur Upload-Queue hinzu (wenn Auto-Upload aktiviert)
|
||||
this._uploadQueueService.QueueNewPhoto(savedPhotoPath);
|
||||
this._logger.Info($"Foto zur Upload-Queue hinzugefügt: {Info.FileName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -75,11 +75,9 @@
|
||||
x:Name="ConnectButton"
|
||||
Content="Verbinden"
|
||||
Click="ConnectButton_Click"
|
||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||
Width="140"
|
||||
Height="45"
|
||||
Background="#D4AF37"
|
||||
Foreground="#1F1A00"
|
||||
FontWeight="SemiBold" />
|
||||
Height="45" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@ -165,18 +163,16 @@
|
||||
<ui:Button x:Name="UploadLastPhotoButton"
|
||||
Content="Letztes Foto hochladen"
|
||||
Click="UploadLastPhotoButton_Click"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
Width="200"
|
||||
Height="45"
|
||||
Margin="0,0,10,0"
|
||||
Appearance="Secondary" />
|
||||
Margin="0,0,10,0" />
|
||||
<ui:Button x:Name="UploadAllPhotosButton"
|
||||
Content="Alle Fotos hochladen"
|
||||
Click="UploadAllPhotosButton_Click"
|
||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||
Width="200"
|
||||
Height="45"
|
||||
Background="#D4AF37"
|
||||
Foreground="#1F1A00"
|
||||
FontWeight="SemiBold" />
|
||||
Height="45" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Media.Imaging;
|
||||
using CamBooth.App.Core.AppSettings;
|
||||
using CamBooth.App.Core.Logging;
|
||||
|
||||
@ -18,6 +19,7 @@ public class LycheeUploadService : IDisposable
|
||||
private readonly AppSettingsService _appSettings;
|
||||
private readonly Logger _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly QRCodeGenerationService _qrCodeGenerationService;
|
||||
private readonly CookieContainer _cookieContainer;
|
||||
private readonly HttpClientHandler _httpClientHandler;
|
||||
private string? _csrfToken;
|
||||
@ -27,6 +29,7 @@ public class LycheeUploadService : IDisposable
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
_logger = logger;
|
||||
_qrCodeGenerationService = new QRCodeGenerationService(logger);
|
||||
|
||||
// CookieContainer für Session-Management
|
||||
_cookieContainer = new CookieContainer();
|
||||
@ -47,6 +50,11 @@ public class LycheeUploadService : IDisposable
|
||||
/// </summary>
|
||||
public bool IsAuthenticated => _isAuthenticated;
|
||||
|
||||
/// <summary>
|
||||
/// Der zuletzt generierte QR-Code
|
||||
/// </summary>
|
||||
public BitmapImage? LastGeneratedQRCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Authentifiziert sich bei Lychee
|
||||
/// </summary>
|
||||
@ -532,6 +540,52 @@ public class LycheeUploadService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generiert einen QR-Code für die Lychee-Galerie-URL
|
||||
/// </summary>
|
||||
/// <returns>BitmapImage des QR-Codes oder null bei Fehler</returns>
|
||||
public BitmapImage? GenerateGalleryQRCode()
|
||||
{
|
||||
try
|
||||
{
|
||||
var lycheeUrl = _appSettings.LycheeApiUrl;
|
||||
var albumId = _appSettings.LycheeDefaultAlbumId;
|
||||
|
||||
if (string.IsNullOrEmpty(lycheeUrl))
|
||||
{
|
||||
_logger.Error("Lychee-URL ist nicht konfiguriert. Kann QR-Code nicht generieren.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(albumId))
|
||||
{
|
||||
_logger.Warning("Lychee DefaultAlbumId ist nicht konfiguriert. QR-Code zeigt nur die Basis-URL.");
|
||||
}
|
||||
|
||||
_logger.Info("Generiere QR-Code für Lychee-Galerie...");
|
||||
|
||||
// Konstruiere die Gallery-URL: ApiUrl + /Gallery + DefaultAlbumId
|
||||
var galleryUrl = $"{lycheeUrl}/gallery/{albumId}";
|
||||
_logger.Debug($"QR-Code URL: {galleryUrl}");
|
||||
|
||||
// Generiere QR-Code mit der Gallery-URL
|
||||
var qrCode = _qrCodeGenerationService.GenerateQRCode(galleryUrl);
|
||||
|
||||
if (qrCode != null)
|
||||
{
|
||||
LastGeneratedQRCode = qrCode;
|
||||
_logger.Info("✅ QR-Code erfolgreich generiert");
|
||||
}
|
||||
|
||||
return qrCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler beim Generieren des QR-Codes: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
<Window x:Class="CamBooth.App.Features.LycheeUpload.QRCodeDisplayWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
mc:Ignorable="d"
|
||||
Title="QR Code - Zum Fotoalbum"
|
||||
Width="900" Height="1000"
|
||||
Background="Black"
|
||||
WindowStyle="None"
|
||||
ResizeMode="NoResize"
|
||||
AllowsTransparency="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Foreground="White">
|
||||
<Window.Resources>
|
||||
<ResourceDictionary Source="pack://application:,,,/CamBooth.App;component/Resources/ButtonStyles.xaml" />
|
||||
</Window.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Close Button (top-right) -->
|
||||
<Button Grid.Row="0"
|
||||
Content="✕"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
Width="60"
|
||||
Height="60"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="10"
|
||||
FontSize="24"
|
||||
Click="CloseButton_Click" />
|
||||
|
||||
<!-- Main Content -->
|
||||
<StackPanel Grid.Row="0" Grid.RowSpan="3"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="40">
|
||||
|
||||
<!-- Top Text: "Scan Me" -->
|
||||
<TextBlock Text="SCAN ME"
|
||||
Foreground="White"
|
||||
FontSize="56"
|
||||
FontWeight="Bold"
|
||||
TextAlignment="Center"
|
||||
Margin="0 0 0 30" />
|
||||
|
||||
<!-- QR Code Image -->
|
||||
<Border Background="White"
|
||||
Padding="20"
|
||||
CornerRadius="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 0 30">
|
||||
<Image x:Name="QRCodeImage"
|
||||
Width="500"
|
||||
Height="500"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Margin="0" />
|
||||
</Border>
|
||||
|
||||
<!-- Bottom Text: "zum Fotoalbum" -->
|
||||
<TextBlock Text="zum Fotoalbum"
|
||||
Foreground="White"
|
||||
FontSize="48"
|
||||
FontWeight="Bold"
|
||||
TextAlignment="Center"
|
||||
Margin="0 30 0 0" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Close Button at the bottom -->
|
||||
<Button Grid.Row="3"
|
||||
Content="Fenster schließen"
|
||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||
Width="300"
|
||||
Height="60"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0 0 0 20"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Click="CloseButton_Click" />
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@ -0,0 +1,28 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace CamBooth.App.Features.LycheeUpload;
|
||||
|
||||
public partial class QRCodeDisplayWindow : Window
|
||||
{
|
||||
public QRCodeDisplayWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setzt den QR-Code für die Anzeige
|
||||
/// </summary>
|
||||
public void SetQRCode(BitmapImage qrCodeImage)
|
||||
{
|
||||
if (qrCodeImage != null)
|
||||
{
|
||||
QRCodeImage.Source = qrCodeImage;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Media.Imaging;
|
||||
using CamBooth.App.Core.Logging;
|
||||
using QRCoder;
|
||||
|
||||
namespace CamBooth.App.Features.LycheeUpload;
|
||||
|
||||
/// <summary>
|
||||
/// Service für die Generierung von QR-Codes als BitmapImage
|
||||
/// </summary>
|
||||
public class QRCodeGenerationService
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public QRCodeGenerationService(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generiert einen QR-Code basierend auf einem URL-String
|
||||
/// </summary>
|
||||
/// <param name="qrData">Die Daten für den QR-Code (z.B. URL)</param>
|
||||
/// <param name="pixelsPerModule">Größe in Pixeln pro QR-Code-Modul</param>
|
||||
/// <returns>BitmapImage des QR-Codes oder null bei Fehler</returns>
|
||||
public BitmapImage? GenerateQRCode(string qrData, int pixelsPerModule = 20)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Debug($"Generiere QR-Code für: {qrData}");
|
||||
|
||||
using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
|
||||
{
|
||||
QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrData, QRCodeGenerator.ECCLevel.Q);
|
||||
using (QRCode qrCode = new QRCode(qrCodeData))
|
||||
{
|
||||
Bitmap qrCodeImage = qrCode.GetGraphic(pixelsPerModule, System.Drawing.Color.Black, System.Drawing.Color.White, true);
|
||||
|
||||
// Konvertiere Bitmap zu BitmapImage
|
||||
BitmapImage bitmapImage = ConvertBitmapToBitmapImage(qrCodeImage);
|
||||
|
||||
_logger.Debug("✅ QR-Code erfolgreich generiert");
|
||||
return bitmapImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler beim Generieren des QR-Codes: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Konvertiert ein System.Drawing.Bitmap zu einem WPF BitmapImage
|
||||
/// </summary>
|
||||
private BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap)
|
||||
{
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.StreamSource = stream;
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Speichert einen QR-Code als Datei
|
||||
/// </summary>
|
||||
/// <param name="qrData">Die Daten für den QR-Code</param>
|
||||
/// <param name="filePath">Zielpath für die QR-Code-Datei</param>
|
||||
/// <param name="pixelsPerModule">Größe in Pixeln pro QR-Code-Modul</param>
|
||||
/// <returns>True wenn erfolgreich, sonst False</returns>
|
||||
public bool SaveQRCodeToFile(string qrData, string filePath, int pixelsPerModule = 20)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Debug($"Speichere QR-Code unter: {filePath}");
|
||||
|
||||
using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
|
||||
{
|
||||
QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrData, QRCodeGenerator.ECCLevel.Q);
|
||||
using (QRCode qrCode = new QRCode(qrCodeData))
|
||||
{
|
||||
Bitmap qrCodeImage = qrCode.GetGraphic(pixelsPerModule, System.Drawing.Color.Black, System.Drawing.Color.White, true);
|
||||
qrCodeImage.Save(filePath, System.Drawing.Imaging.ImageFormat.Png);
|
||||
|
||||
_logger.Debug("✅ QR-Code erfolgreich gespeichert");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler beim Speichern des QR-Codes: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,288 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CamBooth.App.Core.AppSettings;
|
||||
using CamBooth.App.Core.Logging;
|
||||
|
||||
namespace CamBooth.App.Features.LycheeUpload;
|
||||
|
||||
/// <summary>
|
||||
/// Verwaltet die Upload-Queue und führt Uploads im Hintergrund durch
|
||||
/// </summary>
|
||||
public class UploadQueueService : IDisposable
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly AppSettingsService _appSettings;
|
||||
private readonly LycheeUploadService _lycheeUploadService;
|
||||
private readonly UploadTracker _uploadTracker;
|
||||
private readonly Queue<string> _uploadQueue;
|
||||
private Task? _uploadTask;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private bool _isRunning = false;
|
||||
private readonly object _queueLock = new object();
|
||||
|
||||
// Konfigurierbare Parameter
|
||||
private const int UploadRetryDelayMs = 5000; // 5 Sekunden Wartezeit zwischen Retries
|
||||
private const int MaxRetries = 3;
|
||||
private const int CheckIntervalMs = 2000; // Überprüfe Queue alle 2 Sekunden
|
||||
|
||||
public UploadQueueService(Logger logger, AppSettingsService appSettings, LycheeUploadService lycheeUploadService)
|
||||
{
|
||||
_logger = logger;
|
||||
_appSettings = appSettings;
|
||||
_lycheeUploadService = lycheeUploadService;
|
||||
_uploadTracker = new UploadTracker(logger, "UploadTracking");
|
||||
_uploadQueue = new Queue<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Startet den Upload-Service
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
_logger.Warning("Upload-Service läuft bereits");
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_uploadTask = ProcessQueueAsync(_cancellationTokenSource.Token);
|
||||
_logger.Info("Upload-Service gestartet");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stoppt den Upload-Service
|
||||
/// </summary>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = false;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
if (_uploadTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _uploadTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Erwartet
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Upload-Service gestoppt");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scannt das Bild-Verzeichnis und fügt fehlgeschlagene Uploads zur Queue hinzu
|
||||
/// </summary>
|
||||
public void ScanAndQueueFailedUploads()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_appSettings.LycheeAutoUploadEnabled)
|
||||
{
|
||||
_logger.Debug("Lychee Auto-Upload ist deaktiviert");
|
||||
return;
|
||||
}
|
||||
|
||||
var failedFiles = _uploadTracker.GetFailedUploads(_appSettings.PictureLocation);
|
||||
|
||||
lock (_queueLock)
|
||||
{
|
||||
foreach (var filePath in failedFiles)
|
||||
{
|
||||
if (!_uploadQueue.Contains(filePath))
|
||||
{
|
||||
_uploadQueue.Enqueue(filePath);
|
||||
_logger.Debug($"Fehlgeschlagenes Upload in Queue hinzugefügt: {Path.GetFileName(filePath)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failedFiles.Count > 0)
|
||||
{
|
||||
_logger.Info($"{failedFiles.Count} fehlgeschlagene Uploads in Queue hinzugefügt");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler beim Scannen fehlgeschlagener Uploads: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fügt ein neu erstelltes Foto zur Upload-Queue hinzu
|
||||
/// </summary>
|
||||
public void QueueNewPhoto(string photoPath)
|
||||
{
|
||||
if (!_appSettings.LycheeAutoUploadEnabled)
|
||||
{
|
||||
_logger.Debug("Lychee Auto-Upload ist deaktiviert");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_queueLock)
|
||||
{
|
||||
if (!_uploadQueue.Contains(photoPath))
|
||||
{
|
||||
_uploadQueue.Enqueue(photoPath);
|
||||
_logger.Info($"Neues Foto in Upload-Queue hinzugefügt: {Path.GetFileName(photoPath)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt die aktuelle Größe der Upload-Queue zurück
|
||||
/// </summary>
|
||||
public int GetQueueSize()
|
||||
{
|
||||
lock (_queueLock)
|
||||
{
|
||||
return _uploadQueue.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hauptschleife für die Verarbeitung der Upload-Queue
|
||||
/// </summary>
|
||||
private async Task ProcessQueueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Info("Upload-Queue-Verarbeitung gestartet");
|
||||
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? filePath = null;
|
||||
|
||||
lock (_queueLock)
|
||||
{
|
||||
if (_uploadQueue.Count > 0)
|
||||
{
|
||||
filePath = _uploadQueue.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
if (filePath != null)
|
||||
{
|
||||
await ProcessUploadAsync(filePath, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Kurz warten, wenn Queue leer
|
||||
await Task.Delay(CheckIntervalMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler in Upload-Queue-Verarbeitung: {ex.Message}");
|
||||
await Task.Delay(UploadRetryDelayMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Info("Upload-Queue-Verarbeitung wurde abgebrochen");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verarbeitet den Upload einer einzelnen Datei mit Retry-Logik
|
||||
/// </summary>
|
||||
private async Task ProcessUploadAsync(string filePath, CancellationToken cancellationToken)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
// Prüfe, ob Datei noch existiert
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
_logger.Warning($"Bild nicht gefunden, überspringe: {fileName}");
|
||||
_uploadTracker.MarkAsFailedUpload(fileName, "Datei nicht gefunden");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe, ob bereits hochgeladen
|
||||
if (_uploadTracker.IsUploaded(filePath))
|
||||
{
|
||||
_logger.Debug($"Bild bereits hochgeladen: {fileName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Versuche Upload mit Retry
|
||||
bool uploadSuccess = false;
|
||||
string? lastError = null;
|
||||
|
||||
for (int attempt = 1; attempt <= MaxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info($"Upload-Versuch {attempt}/{MaxRetries} für {fileName}...");
|
||||
|
||||
var albumId = _appSettings.LycheeDefaultAlbumId;
|
||||
uploadSuccess = await _lycheeUploadService.UploadImageAsync(filePath, albumId);
|
||||
|
||||
if (uploadSuccess)
|
||||
{
|
||||
_uploadTracker.MarkAsUploaded(filePath);
|
||||
_logger.Info($"✅ Upload erfolgreich: {fileName}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastError = "Upload-Fehler vom Service";
|
||||
_logger.Warning($"Upload fehlgeschlagen (Versuch {attempt}): {fileName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex.Message;
|
||||
_logger.Error($"Fehler bei Upload-Versuch {attempt}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Warte vor nächstem Versuch (nicht nach dem letzten)
|
||||
if (attempt < MaxRetries)
|
||||
{
|
||||
await Task.Delay(UploadRetryDelayMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Versuche fehlgeschlagen
|
||||
_uploadTracker.MarkAsFailedUpload(filePath, lastError ?? "Unbekannter Fehler");
|
||||
_logger.Error($"❌ Upload fehlgeschlagen nach {MaxRetries} Versuchen: {fileName}");
|
||||
|
||||
// Füge wieder zur Queue hinzu für späteren Retry (längerer Abstand)
|
||||
_logger.Info($"Bild wird später erneut versucht: {fileName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Kritischer Fehler bei Upload-Verarbeitung von {fileName}: {ex.Message}");
|
||||
_uploadTracker.MarkAsFailedUpload(filePath, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopAsync().Wait(TimeSpan.FromSeconds(5));
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_lycheeUploadService?.Dispose();
|
||||
}
|
||||
}
|
||||
195
src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadTracker.cs
Normal file
195
src/CamBooth/CamBooth.App/Features/LycheeUpload/UploadTracker.cs
Normal file
@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using CamBooth.App.Core.Logging;
|
||||
|
||||
namespace CamBooth.App.Features.LycheeUpload;
|
||||
|
||||
/// <summary>
|
||||
/// Verfolgt welche Bilder bereits hochgeladen wurden
|
||||
/// </summary>
|
||||
public class UploadTracker
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly string _trackerFilePath;
|
||||
private Dictionary<string, UploadedImageInfo> _uploadedImages;
|
||||
|
||||
public class UploadedImageInfo
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public DateTime UploadedAt { get; set; }
|
||||
public string? LycheeId { get; set; }
|
||||
public int RetryCount { get; set; }
|
||||
public DateTime? LastRetryAt { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public UploadTracker(Logger logger, string trackerDirectory = "")
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// Verwende absoluten Pfad
|
||||
if (string.IsNullOrWhiteSpace(trackerDirectory))
|
||||
{
|
||||
trackerDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UploadTracking");
|
||||
}
|
||||
else if (!Path.IsPathRooted(trackerDirectory))
|
||||
{
|
||||
trackerDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, trackerDirectory);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(trackerDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(trackerDirectory);
|
||||
}
|
||||
|
||||
_trackerFilePath = Path.Combine(trackerDirectory, "upload_history.json");
|
||||
_uploadedImages = new Dictionary<string, UploadedImageInfo>();
|
||||
|
||||
LoadFromDisk();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lädt den Upload-History aus der Datei
|
||||
/// </summary>
|
||||
private void LoadFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_trackerFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(_trackerFilePath);
|
||||
var data = JsonSerializer.Deserialize<Dictionary<string, UploadedImageInfo>>(json);
|
||||
_uploadedImages = data ?? new Dictionary<string, UploadedImageInfo>();
|
||||
_logger.Info($"Upload-History geladen: {_uploadedImages.Count} Einträge");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler beim Laden der Upload-History: {ex.Message}");
|
||||
_uploadedImages = new Dictionary<string, UploadedImageInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Speichert den Upload-History auf die Festplatte
|
||||
/// </summary>
|
||||
private void SaveToDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_uploadedImages, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(_trackerFilePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler beim Speichern der Upload-History: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Markiert ein Bild als erfolgreich hochgeladen
|
||||
/// </summary>
|
||||
public void MarkAsUploaded(string fileName, string? lycheeId = null)
|
||||
{
|
||||
var fileNameOnly = Path.GetFileName(fileName);
|
||||
|
||||
_uploadedImages[fileNameOnly] = new UploadedImageInfo
|
||||
{
|
||||
FileName = fileNameOnly,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
LycheeId = lycheeId,
|
||||
RetryCount = 0,
|
||||
LastRetryAt = null,
|
||||
ErrorMessage = null
|
||||
};
|
||||
|
||||
SaveToDisk();
|
||||
_logger.Info($"Bild als hochgeladen markiert: {fileNameOnly}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Markiert einen Upload-Fehler für ein Bild
|
||||
/// </summary>
|
||||
public void MarkAsFailedUpload(string fileName, string? errorMessage = null)
|
||||
{
|
||||
var fileNameOnly = Path.GetFileName(fileName);
|
||||
|
||||
if (_uploadedImages.ContainsKey(fileNameOnly))
|
||||
{
|
||||
var info = _uploadedImages[fileNameOnly];
|
||||
info.RetryCount++;
|
||||
info.LastRetryAt = DateTime.UtcNow;
|
||||
info.ErrorMessage = errorMessage;
|
||||
}
|
||||
else
|
||||
{
|
||||
_uploadedImages[fileNameOnly] = new UploadedImageInfo
|
||||
{
|
||||
FileName = fileNameOnly,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
RetryCount = 1,
|
||||
LastRetryAt = DateTime.UtcNow,
|
||||
ErrorMessage = errorMessage
|
||||
};
|
||||
}
|
||||
|
||||
SaveToDisk();
|
||||
_logger.Warning($"Upload-Fehler für {fileNameOnly}: {errorMessage}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prüft, ob ein Bild bereits hochgeladen wurde
|
||||
/// </summary>
|
||||
public bool IsUploaded(string fileName)
|
||||
{
|
||||
var fileNameOnly = Path.GetFileName(fileName);
|
||||
return _uploadedImages.ContainsKey(fileNameOnly) && _uploadedImages[fileNameOnly].UploadedAt != default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt alle Bilder zurück, die noch nicht hochgeladen wurden
|
||||
/// </summary>
|
||||
public List<string> GetFailedUploads(string pictureDirectory)
|
||||
{
|
||||
var failedFiles = new List<string>();
|
||||
|
||||
if (!Directory.Exists(pictureDirectory))
|
||||
{
|
||||
return failedFiles;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var allFiles = Directory.GetFiles(pictureDirectory, "img_*.jpg");
|
||||
|
||||
foreach (var filePath in allFiles)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
// Wenn nicht im Tracker oder mit Fehler → zu Retry-Liste hinzufügen
|
||||
if (!_uploadedImages.ContainsKey(fileName) ||
|
||||
_uploadedImages[fileName].UploadedAt == default)
|
||||
{
|
||||
failedFiles.Add(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Fehler beim Scannen von {pictureDirectory}: {ex.Message}");
|
||||
}
|
||||
|
||||
return failedFiles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt die Anzahl fehlgeschlagener Uploads zurück
|
||||
/// </summary>
|
||||
public int GetFailedUploadCount()
|
||||
{
|
||||
return _uploadedImages.Count(x => x.Value.UploadedAt == default);
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,19 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Close Button (top-right) -->
|
||||
<Button Grid.Row="0"
|
||||
Content="✕"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
Width="60"
|
||||
Height="60"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="20"
|
||||
FontSize="24"
|
||||
Click="CloseGallery_Click"
|
||||
Panel.ZIndex="10" />
|
||||
|
||||
<ScrollViewer Grid.Row="0" x:Name="GalleryScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Background="Black" CanContentScroll="False">
|
||||
<ItemsControl x:Name="PicturesPanel" Background="Black">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@ -36,15 +49,17 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- First Page Button -->
|
||||
<Button Grid.Column="0" x:Name="FirstPageButton" Content="⏮️"
|
||||
Width="60" Height="50" Margin="5,0"
|
||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||
<Button Grid.Column="0"
|
||||
x:Name="FirstPageButton"
|
||||
Content="⏮️"
|
||||
Style="{StaticResource PagingButtonStyle}"
|
||||
Click="FirstPageButton_Click" />
|
||||
|
||||
<!-- Previous Page Button -->
|
||||
<Button Grid.Column="1" x:Name="PreviousPageButton" Content="◀️"
|
||||
Width="60" Height="50" Margin="5,0"
|
||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||
<Button Grid.Column="1"
|
||||
x:Name="PreviousPageButton"
|
||||
Content="◀️"
|
||||
Style="{StaticResource PagingButtonStyle}"
|
||||
Click="PreviousPageButton_Click" />
|
||||
|
||||
<!-- Page Info -->
|
||||
@ -54,15 +69,17 @@
|
||||
Text="Seite 1 von 1" MinWidth="150" TextAlignment="Center" />
|
||||
|
||||
<!-- Next Page Button -->
|
||||
<Button Grid.Column="3" x:Name="NextPageButton" Content="▶️"
|
||||
Width="60" Height="50" Margin="5,0"
|
||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||
<Button Grid.Column="3"
|
||||
x:Name="NextPageButton"
|
||||
Content="▶️"
|
||||
Style="{StaticResource PagingButtonStyle}"
|
||||
Click="NextPageButton_Click" />
|
||||
|
||||
<!-- Last Page Button -->
|
||||
<Button Grid.Column="4" x:Name="LastPageButton" Content="⏭️"
|
||||
Width="60" Height="50" Margin="5,0"
|
||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||
<Button Grid.Column="4"
|
||||
x:Name="LastPageButton"
|
||||
Content="⏭️"
|
||||
Style="{StaticResource PagingButtonStyle}"
|
||||
Click="LastPageButton_Click" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
@ -7,6 +7,7 @@ using System.Windows.Media.Imaging;
|
||||
|
||||
using CamBooth.App.Core.AppSettings;
|
||||
using CamBooth.App.Core.Logging;
|
||||
using CamBooth.App.Features.LycheeUpload;
|
||||
|
||||
using Wpf.Ui.Controls;
|
||||
|
||||
@ -24,18 +25,24 @@ public partial class PictureGalleryPage : Page
|
||||
|
||||
private readonly PictureGalleryService _pictureGalleryService;
|
||||
|
||||
private readonly LycheeUploadService _lycheeUploadService;
|
||||
|
||||
private ContentDialog? _openContentDialog;
|
||||
|
||||
private int _currentPage = 1;
|
||||
private int _itemsPerPage = 12;
|
||||
private int _itemsPerPage = 11;
|
||||
private int _totalPages = 1;
|
||||
|
||||
// QR Code wird nur einmal erstellt und dann wiederverwendet
|
||||
private Border? _qrCodeBorder = null;
|
||||
|
||||
public PictureGalleryPage(AppSettingsService appSettingsService, Logger logger, PictureGalleryService pictureGalleryService)
|
||||
|
||||
public PictureGalleryPage(AppSettingsService appSettingsService, Logger logger, PictureGalleryService pictureGalleryService, LycheeUploadService lycheeUploadService)
|
||||
{
|
||||
this._appSettingsService = appSettingsService;
|
||||
this._logger = logger;
|
||||
this._pictureGalleryService = pictureGalleryService;
|
||||
this._lycheeUploadService = lycheeUploadService;
|
||||
this.InitializeComponent();
|
||||
this.Initialize();
|
||||
}
|
||||
@ -45,6 +52,9 @@ public partial class PictureGalleryPage : Page
|
||||
{
|
||||
try
|
||||
{
|
||||
// QR-Code nur einmal beim Initialisieren erstellen
|
||||
CreateQRCodeBorder();
|
||||
|
||||
_currentPage = 1;
|
||||
CalculateTotalPages();
|
||||
LoadCurrentPage();
|
||||
@ -56,6 +66,56 @@ public partial class PictureGalleryPage : Page
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateQRCodeBorder()
|
||||
{
|
||||
try
|
||||
{
|
||||
var qrCodeImage = this._lycheeUploadService.GenerateGalleryQRCode();
|
||||
if (qrCodeImage != null)
|
||||
{
|
||||
// QR-Code Container mit Label
|
||||
var qrContainer = new StackPanel { Orientation = Orientation.Vertical };
|
||||
|
||||
var qrImageControl = new Image
|
||||
{
|
||||
Source = qrCodeImage,
|
||||
Width = 220,
|
||||
Margin = new Thickness(4)
|
||||
};
|
||||
|
||||
var qrSubLabel = new TextBlock
|
||||
{
|
||||
Text = "zum Fotoalbum",
|
||||
Foreground = new SolidColorBrush(Colors.White),
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.Bold,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 4, 2)
|
||||
};
|
||||
|
||||
qrContainer.Children.Add(qrSubLabel);
|
||||
qrContainer.Children.Add(qrImageControl);
|
||||
|
||||
_qrCodeBorder = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Colors.Black),
|
||||
BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D4AF37")),
|
||||
BorderThickness = new Thickness(2),
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Padding = new Thickness(4),
|
||||
Child = qrContainer,
|
||||
Margin = new Thickness(4)
|
||||
};
|
||||
|
||||
this._logger.Debug("✅ QR-Code statisch erstellt");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Warning($"QR-Code konnte nicht generiert werden: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ContentDialog_OnButtonClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
@ -74,6 +134,13 @@ public partial class PictureGalleryPage : Page
|
||||
// Clear existing items
|
||||
this.PicturesPanel.Items.Clear();
|
||||
|
||||
// Füge den statischen QR-Code als erstes Element hinzu
|
||||
if (_qrCodeBorder != null)
|
||||
{
|
||||
this.PicturesPanel.Items.Add(_qrCodeBorder);
|
||||
}
|
||||
|
||||
// Jetzt die regulären Bilder hinzufügen
|
||||
int totalThumbnails = this._pictureGalleryService.ThumbnailsOrderedByNewestDescending.Count;
|
||||
int endIndex = Math.Min(startIndex + count, totalThumbnails);
|
||||
|
||||
@ -276,6 +343,19 @@ public partial class PictureGalleryPage : Page
|
||||
UpdatePagerControls();
|
||||
GalleryScrollViewer.ScrollToTop();
|
||||
}
|
||||
|
||||
private void CloseGallery_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Finde das MainWindow über die Window.GetWindow Methode
|
||||
var window = Window.GetWindow(this);
|
||||
if (window is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.ClosePicturePanel();
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("MainWindow konnte nicht gefunden werden, um die Galerie zu schließen");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -53,28 +53,245 @@
|
||||
Margin="24 24 24 12"/>
|
||||
<liveView:TimerControlRectangleAnimation x:Name="TimerControlRectangleAnimation" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Action Buttons Container (bottom-right) -->
|
||||
<StackPanel Grid.Row="0"
|
||||
x:Name="ActionButtonsContainer"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="20"
|
||||
Panel.ZIndex="3"
|
||||
Visibility="Hidden"
|
||||
Orientation="Vertical"
|
||||
Width="100">
|
||||
|
||||
<!-- Gallery/Sammlung Button -->
|
||||
<Border Background="#1A1A1A"
|
||||
BorderBrush="#D4AF37"
|
||||
BorderThickness="2"
|
||||
CornerRadius="10"
|
||||
Padding="8"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,0,12"
|
||||
MouseLeftButtonDown="SetVisibilityPicturePanel"
|
||||
Focusable="False">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="12" ShadowDepth="4" Opacity="0.5"/>
|
||||
</Border.Effect>
|
||||
<Border.Triggers>
|
||||
<EventTrigger RoutedEvent="MouseEnter">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
|
||||
To="1.05" Duration="0:0:0.2"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
|
||||
To="1.05" Duration="0:0:0.2"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
<EventTrigger RoutedEvent="MouseLeave">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
|
||||
To="1.0" Duration="0:0:0.2"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
|
||||
To="1.0" Duration="0:0:0.2"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</Border.Triggers>
|
||||
<Border.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" CenterX="0.5" CenterY="0.5"/>
|
||||
</Border.RenderTransform>
|
||||
<Border.RenderTransformOrigin>
|
||||
<Point X="0.5" Y="0.5"/>
|
||||
</Border.RenderTransformOrigin>
|
||||
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- Gallery Icon -->
|
||||
<Grid Width="55" Height="55" Margin="0,0,0,6">
|
||||
<Border BorderBrush="#D4AF37"
|
||||
BorderThickness="3"
|
||||
CornerRadius="3"
|
||||
Padding="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Grid.Column="0"
|
||||
Background="#D4AF37"
|
||||
Margin="1.5"
|
||||
CornerRadius="1.5"/>
|
||||
<Border Grid.Row="0" Grid.Column="1"
|
||||
Background="#D4AF37"
|
||||
Margin="1.5"
|
||||
CornerRadius="1.5"/>
|
||||
<Border Grid.Row="1" Grid.Column="0"
|
||||
Background="#D4AF37"
|
||||
Margin="1.5"
|
||||
CornerRadius="1.5"/>
|
||||
<Border Grid.Row="1" Grid.Column="1"
|
||||
Background="#D4AF37"
|
||||
Margin="1.5"
|
||||
CornerRadius="1.5"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Text -->
|
||||
<TextBlock Text="Foto"
|
||||
Foreground="White"
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
TextAlignment="Center"
|
||||
Margin="0,0,0,2"/>
|
||||
<TextBlock Text="Galerie"
|
||||
Foreground="#D4AF37"
|
||||
FontSize="8"
|
||||
FontWeight="SemiBold"
|
||||
TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Badge for new photos -->
|
||||
<Border x:Name="NewPhotosBadge"
|
||||
Background="#E61A1A1A"
|
||||
BorderBrush="#FF0000"
|
||||
BorderThickness="2"
|
||||
CornerRadius="12"
|
||||
Width="30"
|
||||
Height="30"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,-42,-10,0"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="10">
|
||||
<TextBlock x:Name="NewPhotoCountText"
|
||||
Text="1"
|
||||
Foreground="White"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<!-- QR/Sharing Button -->
|
||||
<Border Background="#1A1A1A"
|
||||
BorderBrush="#D4AF37"
|
||||
BorderThickness="2"
|
||||
CornerRadius="10"
|
||||
Padding="8"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonDown="ShowQRCode"
|
||||
Focusable="False">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="12" ShadowDepth="4" Opacity="0.5"/>
|
||||
</Border.Effect>
|
||||
<Border.Triggers>
|
||||
<EventTrigger RoutedEvent="MouseEnter">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
|
||||
To="1.05" Duration="0:0:0.2"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
|
||||
To="1.05" Duration="0:0:0.2"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
<EventTrigger RoutedEvent="MouseLeave">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
|
||||
To="1.0" Duration="0:0:0.2"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
|
||||
To="1.0" Duration="0:0:0.2"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</Border.Triggers>
|
||||
<Border.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" CenterX="0.5" CenterY="0.5"/>
|
||||
</Border.RenderTransform>
|
||||
<Border.RenderTransformOrigin>
|
||||
<Point X="0.5" Y="0.5"/>
|
||||
</Border.RenderTransformOrigin>
|
||||
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- QR Code Icon -->
|
||||
<Grid Width="55" Height="55" Margin="0,0,0,6">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Grid.Column="0" Background="#D4AF37" Margin="1.5" CornerRadius="1.5"/>
|
||||
<Border Grid.Row="0" Grid.Column="1" Background="Transparent" Margin="1.5"/>
|
||||
<Border Grid.Row="0" Grid.Column="2" Background="#D4AF37" Margin="1.5" CornerRadius="1.5"/>
|
||||
|
||||
<Border Grid.Row="1" Grid.Column="0" Background="Transparent" Margin="1.5"/>
|
||||
<Border Grid.Row="1" Grid.Column="1" Background="#D4AF37" Margin="1.5" CornerRadius="1.5"/>
|
||||
<Border Grid.Row="1" Grid.Column="2" Background="Transparent" Margin="1.5"/>
|
||||
|
||||
<Border Grid.Row="2" Grid.Column="0" Background="#D4AF37" Margin="1.5" CornerRadius="1.5"/>
|
||||
<Border Grid.Row="2" Grid.Column="1" Background="Transparent" Margin="1.5"/>
|
||||
<Border Grid.Row="2" Grid.Column="2" Background="#D4AF37" Margin="1.5" CornerRadius="1.5"/>
|
||||
|
||||
<Border Grid.RowSpan="3" Grid.ColumnSpan="3"
|
||||
BorderBrush="#D4AF37"
|
||||
BorderThickness="2"
|
||||
CornerRadius="3"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Text -->
|
||||
<TextBlock Text="Herunterladen"
|
||||
Foreground="White"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"
|
||||
TextAlignment="Center"
|
||||
Margin="0,0,0,2"/>
|
||||
<TextBlock Text="Fotos"
|
||||
Foreground="#D4AF37"
|
||||
FontSize="8"
|
||||
FontWeight="SemiBold"
|
||||
TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Hide Debug Button (top-right) -->
|
||||
<ui:Button Grid.Row="0"
|
||||
x:Name="HideDebugButton"
|
||||
Content="Hide Debug"
|
||||
Click="SetVisibilityDebugConsole"
|
||||
Width="160"
|
||||
Height="60"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="20"
|
||||
Panel.ZIndex="5"
|
||||
Visibility="Hidden"
|
||||
FontSize="16"
|
||||
Appearance="Secondary" />
|
||||
<Button Grid.Row="0"
|
||||
x:Name="HideDebugButton"
|
||||
Content="Hide Debug"
|
||||
Click="SetVisibilityDebugConsole"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
Width="160"
|
||||
Height="60"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="20"
|
||||
Panel.ZIndex="5"
|
||||
Visibility="Hidden"
|
||||
FontSize="16" />
|
||||
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom" Name="ButtonPanel" Background="Transparent" Panel.ZIndex="2"
|
||||
Visibility="Hidden"
|
||||
Margin="0 0 0 0">
|
||||
<!-- <ui:Button Content="Take Photo" Click="StartTakePhotoProcess" Width="200" Height="75" VerticalAlignment="Bottom" -->
|
||||
<!-- Margin="0 0 5 0" /> -->
|
||||
<Button Width="160" Height="160"
|
||||
Margin="0 0 0 30">
|
||||
<!-- Take Photo Button -->
|
||||
<Button Width="240" Height="120"
|
||||
Click="StartTakePhotoProcess"
|
||||
Style="{StaticResource ModernRounded3DButtonStyle}"
|
||||
FontSize="64"
|
||||
Foreground="White"
|
||||
Margin="0 0 5 0"
|
||||
@ -82,57 +299,25 @@
|
||||
BorderBrush="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ModernRounded3DButtonStyle}"/>
|
||||
<ui:Button x:Name="DebugCloseButton" Content="Close" Appearance="Danger" Click="CloseApp" Width="200" Height="75" VerticalAlignment="Bottom" Margin="0 0 5 0" />
|
||||
Content="🔴"/>
|
||||
<Button x:Name="DebugCloseButton"
|
||||
Content="Close"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
Click="CloseApp"
|
||||
Width="200"
|
||||
Height="75"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0 0 5 0"
|
||||
FontSize="16"
|
||||
FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Picture Gallery Dock (bottom-right) -->
|
||||
<Grid Grid.Row="0"
|
||||
x:Name="PictureGalleryDock"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="20"
|
||||
Panel.ZIndex="3"
|
||||
Visibility="Hidden">
|
||||
<ui:Button Content=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="30"
|
||||
Width="72"
|
||||
Height="72"
|
||||
Click="SetVisibilityPicturePanel"
|
||||
Background="#D4AF37"
|
||||
Foreground="#1F1A00"
|
||||
BorderBrush="#F6E7A1"
|
||||
BorderThickness="2" />
|
||||
<!-- Badge for new photos -->
|
||||
<Border x:Name="NewPhotosBadge"
|
||||
Background="#E61A1A1A"
|
||||
BorderBrush="#FF0000"
|
||||
BorderThickness="2"
|
||||
CornerRadius="12"
|
||||
Width="36"
|
||||
Height="36"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,-8,-8,0"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="1">
|
||||
<TextBlock x:Name="NewPhotoCountText"
|
||||
Text="1"
|
||||
Foreground="White"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Gallery Prompt -->
|
||||
<Border Grid.Row="0"
|
||||
x:Name="GalleryPrompt"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="20"
|
||||
VerticalAlignment="Top"
|
||||
Margin="20,80,20,20"
|
||||
Padding="16"
|
||||
Background="#E61A1A1A"
|
||||
BorderBrush="#66FFFFFF"
|
||||
@ -147,15 +332,12 @@
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 12 0" />
|
||||
<ui:Button Content="Jetzt in Galerie ansehen"
|
||||
Click="OpenGalleryFromPrompt"
|
||||
Width="240"
|
||||
Height="52"
|
||||
Background="#D4AF37"
|
||||
Foreground="#1F1A00"
|
||||
BorderBrush="#F6E7A1"
|
||||
BorderThickness="2"
|
||||
FontWeight="SemiBold" />
|
||||
<Button Content="Jetzt in Galerie ansehen"
|
||||
Click="OpenGalleryFromPrompt"
|
||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||
Width="240"
|
||||
Height="52"
|
||||
FontWeight="SemiBold" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@ -205,11 +387,86 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<Grid Grid.RowSpan="2"
|
||||
x:Name="LoadingOverlay"
|
||||
Background="#DD000000"
|
||||
Panel.ZIndex="11">
|
||||
<Border Background="#E61A1A1A"
|
||||
BorderBrush="#66FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Padding="48"
|
||||
MaxWidth="600"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock Text="CamBooth wird geladen..."
|
||||
Foreground="White"
|
||||
FontSize="42"
|
||||
FontWeight="Bold"
|
||||
TextAlignment="Center"
|
||||
Margin="0 0 0 30"/>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<Grid Width="100" Height="100" Margin="0 0 0 20">
|
||||
<Ellipse Width="80" Height="80"
|
||||
Stroke="#D4AF37"
|
||||
StrokeThickness="6"
|
||||
Opacity="0.3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
<Ellipse x:Name="LoadingSpinner"
|
||||
Width="80" Height="80"
|
||||
Stroke="#D4AF37"
|
||||
StrokeThickness="6"
|
||||
StrokeDashArray="60 200"
|
||||
StrokeDashCap="Round"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse.RenderTransform>
|
||||
<RotateTransform x:Name="LoadingSpinnerTransform" Angle="0"/>
|
||||
</Ellipse.RenderTransform>
|
||||
<Ellipse.Triggers>
|
||||
<EventTrigger RoutedEvent="Ellipse.Loaded">
|
||||
<BeginStoryboard>
|
||||
<Storyboard RepeatBehavior="Forever">
|
||||
<DoubleAnimation Storyboard.TargetName="LoadingSpinnerTransform"
|
||||
Storyboard.TargetProperty="Angle"
|
||||
From="0" To="360"
|
||||
Duration="0:0:1.5"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</Ellipse.Triggers>
|
||||
</Ellipse>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="LoadingStatusText"
|
||||
Text="Fotos werden geladen..."
|
||||
Foreground="#FFE8E8E8"
|
||||
FontSize="20"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0 0 0 10"/>
|
||||
|
||||
<TextBlock x:Name="LoadingCountText"
|
||||
Text="0 Fotos gefunden"
|
||||
Foreground="#FFCCCCCC"
|
||||
FontSize="16"
|
||||
TextAlignment="Center"
|
||||
Margin="0 0 0 0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Welcome Overlay -->
|
||||
<Grid Grid.RowSpan="2"
|
||||
x:Name="WelcomeOverlay"
|
||||
Background="#CC000000"
|
||||
Panel.ZIndex="10">
|
||||
Panel.ZIndex="10"
|
||||
Visibility="Collapsed">
|
||||
<Border Background="#E61A1A1A"
|
||||
BorderBrush="#66FFFFFF"
|
||||
BorderThickness="1"
|
||||
@ -232,6 +489,7 @@
|
||||
TextWrapping="Wrap"
|
||||
Margin="0 0 0 16"/>
|
||||
<ui:Button Click="StartExperience"
|
||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||
Width="480"
|
||||
Height="100"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
@ -74,7 +76,10 @@ public partial class MainWindow : Window
|
||||
InitializeComponent();
|
||||
this.SetVisibilityDebugConsole(_appSettings.IsDebugConsoleVisible);
|
||||
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
|
||||
_ = this._pictureGalleryService.LoadThumbnailsToCache();
|
||||
|
||||
// Lade Thumbnails asynchron und zeige dann den Welcome Screen
|
||||
_ = InitializeAsync();
|
||||
|
||||
this.Closing += OnClosing;
|
||||
TimerControlRectangleAnimation.OnTimerEllapsed += TimerControlRectangleAnimation_OnTimerEllapsed;
|
||||
this._focusStatusAnimationTimer.Tick += (_, _) =>
|
||||
@ -112,6 +117,114 @@ public partial class MainWindow : Window
|
||||
logger.Info("MainWindow initialized");
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Zeige Ladeanzeige
|
||||
this.LoadingOverlay.Visibility = Visibility.Visible;
|
||||
this.WelcomeOverlay.Visibility = Visibility.Collapsed;
|
||||
|
||||
// Lade Thumbnails mit Progress-Updates
|
||||
await LoadThumbnailsWithProgress();
|
||||
|
||||
// Warte kurz, damit der Benutzer die Fertigstellung sehen kann
|
||||
await Task.Delay(500);
|
||||
|
||||
// Verstecke Ladeanzeige und zeige Welcome Screen
|
||||
this.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.LoadingOverlay.Visibility = Visibility.Collapsed;
|
||||
this.WelcomeOverlay.Visibility = Visibility.Visible;
|
||||
});
|
||||
|
||||
this._logger.Info("Initialization completed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Error($"Initialization error: {ex.Message}");
|
||||
// Bei Fehler trotzdem zum Welcome Screen wechseln
|
||||
this.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.LoadingOverlay.Visibility = Visibility.Collapsed;
|
||||
this.WelcomeOverlay.Visibility = Visibility.Visible;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadThumbnailsWithProgress()
|
||||
{
|
||||
string pictureLocation = this._appSettings.PictureLocation;
|
||||
|
||||
// Sicherstellen, dass das Verzeichnis existiert
|
||||
if (!Directory.Exists(pictureLocation))
|
||||
{
|
||||
this._logger.Info($"Picture directory does not exist: '{pictureLocation}'. Creating it...");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(pictureLocation);
|
||||
this._logger.Info($"Picture directory created: '{pictureLocation}'");
|
||||
|
||||
this.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.LoadingStatusText.Text = "Keine Fotos gefunden";
|
||||
this.LoadingCountText.Text = "0 Fotos";
|
||||
});
|
||||
|
||||
await Task.Delay(1000);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Error($"Failed to create picture directory: {ex.Message}");
|
||||
this.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.LoadingStatusText.Text = "Fehler beim Erstellen des Foto-Ordners";
|
||||
this.LoadingCountText.Text = "";
|
||||
});
|
||||
await Task.Delay(2000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Zähle Bilddateien
|
||||
string[] imageExtensions = { ".jpg", ".jpeg", ".png", ".bmp", ".gif" };
|
||||
var picturePaths = Directory.EnumerateFiles(pictureLocation)
|
||||
.Where(f => imageExtensions.Contains(Path.GetExtension(f).ToLower()))
|
||||
.ToList();
|
||||
|
||||
int totalCount = picturePaths.Count;
|
||||
|
||||
if (totalCount == 0)
|
||||
{
|
||||
this._logger.Info($"No pictures found in directory: '{pictureLocation}'");
|
||||
this.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.LoadingStatusText.Text = "Keine Fotos gefunden";
|
||||
this.LoadingCountText.Text = "Bereit für neue Aufnahmen!";
|
||||
});
|
||||
await Task.Delay(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI mit Gesamtanzahl
|
||||
this.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.LoadingStatusText.Text = $"Lade {totalCount} Foto{(totalCount != 1 ? "s" : "")}...";
|
||||
this.LoadingCountText.Text = $"0 / {totalCount}";
|
||||
});
|
||||
|
||||
// Lade Thumbnails
|
||||
await this._pictureGalleryService.LoadThumbnailsToCache();
|
||||
|
||||
// Update UI nach dem Laden
|
||||
this.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.LoadingStatusText.Text = "Fotos erfolgreich geladen!";
|
||||
this.LoadingCountText.Text = $"{totalCount} Foto{(totalCount != 1 ? "s" : "")} bereit";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void TimerControlRectangleAnimation_OnTimerEllapsed()
|
||||
{
|
||||
@ -147,20 +260,28 @@ public partial class MainWindow : Window
|
||||
if (visibility)
|
||||
{
|
||||
this.HideGalleryPrompt();
|
||||
this.PicturePanel.Navigate(new PictureGalleryPage(this._appSettings, this._logger, this._pictureGalleryService));
|
||||
this.PicturePanel.Navigate(new PictureGalleryPage(this._appSettings, this._logger, this._pictureGalleryService, this._lycheeUploadService));
|
||||
// Reset new photo count when opening gallery
|
||||
this._pictureGalleryService.ResetNewPhotoCount();
|
||||
// Blende unnötige Buttons aus, wenn Galerie geöffnet wird
|
||||
this.ButtonPanel.Visibility = Visibility.Hidden;
|
||||
this.ActionButtonsContainer.Visibility = Visibility.Hidden;
|
||||
this.ShutdownDock.Visibility = Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.PicturePanel.ClearValue(MainWindow.ContentProperty);
|
||||
// Stelle Buttons wieder her, wenn Galerie geschlossen wird
|
||||
this.ButtonPanel.Visibility = Visibility.Visible;
|
||||
this.ActionButtonsContainer.Visibility = Visibility.Visible;
|
||||
this.ShutdownDock.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
this._isPicturePanelVisible = !visibility;
|
||||
}
|
||||
|
||||
|
||||
private void ClosePicturePanel()
|
||||
public void ClosePicturePanel()
|
||||
{
|
||||
if (this.PicturePanel.Content is PictureGalleryPage pictureGalleryPage)
|
||||
{
|
||||
@ -187,7 +308,7 @@ public partial class MainWindow : Window
|
||||
this.StartLiveViewIfNeeded();
|
||||
this.WelcomeOverlay.Visibility = Visibility.Collapsed;
|
||||
this.ButtonPanel.Visibility = Visibility.Visible;
|
||||
this.PictureGalleryDock.Visibility = Visibility.Visible;
|
||||
this.ActionButtonsContainer.Visibility = Visibility.Visible;
|
||||
this.ShutdownDock.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
@ -284,11 +405,11 @@ public partial class MainWindow : Window
|
||||
private void SwitchButtonAndTimerPanel()
|
||||
{
|
||||
this.ButtonPanel.Visibility = this.ButtonPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
||||
this.PictureGalleryDock.Visibility = this.PictureGalleryDock.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
||||
this.ActionButtonsContainer.Visibility = this.ActionButtonsContainer.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
||||
this.TimerPanel.Visibility = this.TimerPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
|
||||
private void SetVisibilityPicturePanel(object sender, RoutedEventArgs e)
|
||||
private void SetVisibilityPicturePanel(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
|
||||
}
|
||||
@ -339,8 +460,24 @@ public partial class MainWindow : Window
|
||||
System.Windows.MessageBox.Show("Windows konnte nicht heruntergefahren werden. Bitte erneut versuchen.");
|
||||
}
|
||||
}
|
||||
|
||||
this.Close();
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = "/l", // = logoff (abmelden)
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this._logger.Error(exception.Message);
|
||||
System.Windows.MessageBox.Show("Abmeldung fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleShutdownSlider(object sender, RoutedEventArgs e)
|
||||
@ -454,4 +591,33 @@ public partial class MainWindow : Window
|
||||
this.NewPhotosBadge.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowQRCode(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
this._logger.Info("Zeige QR-Code an...");
|
||||
|
||||
// Generiere QR-Code
|
||||
var qrCodeImage = this._lycheeUploadService.GenerateGalleryQRCode();
|
||||
|
||||
if (qrCodeImage == null)
|
||||
{
|
||||
System.Windows.MessageBox.Show("QR-Code konnte nicht generiert werden. Bitte überprüfe die Lychee-Konfiguration.",
|
||||
"Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Öffne QR-Code-Fenster
|
||||
var qrWindow = new QRCodeDisplayWindow();
|
||||
qrWindow.SetQRCode(qrCodeImage);
|
||||
qrWindow.ShowDialog();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Error($"Fehler beim Anzeigen des QR-Codes: {ex.Message}");
|
||||
System.Windows.MessageBox.Show($"Fehler beim Anzeigen des QR-Codes: {ex.Message}",
|
||||
"Fehler", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,115 +1,263 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- Runder Button Stil mit 3D-Effekt, Wellenbewegung und Schatten -->
|
||||
<Style x:Key="ModernRounded3DButtonStyle" TargetType="Button">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid>
|
||||
<!-- 3D Hintergrund -->
|
||||
<Ellipse>
|
||||
<Ellipse.Fill>
|
||||
<LinearGradientBrush StartPoint="0.3,0" EndPoint="0.7,1">
|
||||
<GradientStop Color="#FFB22222" Offset="0.0" /> <!-- Obere Kante -->
|
||||
<GradientStop Color="#FF8B0000" Offset="1.0" /> <!-- Untere Kante -->
|
||||
</LinearGradientBrush>
|
||||
</Ellipse.Fill>
|
||||
<Ellipse.Effect>
|
||||
<DropShadowEffect Color="Black"
|
||||
BlurRadius="20"
|
||||
ShadowDepth="7"
|
||||
Opacity="0.6" />
|
||||
</Ellipse.Effect>
|
||||
</Ellipse>
|
||||
|
||||
<!-- Rechteckiger Button mit runden Ecken und 3D-Effekt -->
|
||||
<Style x:Key="ModernRounded3DButtonStyle" TargetType="Button">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid>
|
||||
<!-- 3D Hintergrund mit abgerundeten Ecken -->
|
||||
<Border CornerRadius="20" Background="#FF8B0000"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black"
|
||||
BlurRadius="20"
|
||||
ShadowDepth="7"
|
||||
Opacity="0.6" />
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
|
||||
<!-- Glanzeffekt (Highlight) -->
|
||||
<Ellipse>
|
||||
<Ellipse.Fill>
|
||||
<RadialGradientBrush GradientOrigin="0.5,0.3" Center="0.5,0.3" RadiusX="1" RadiusY="1">
|
||||
<GradientStop Color="#90EEEEEE" Offset="0.0" />
|
||||
<GradientStop Color="#00EEEEEE" Offset="1.0" />
|
||||
</RadialGradientBrush>
|
||||
</Ellipse.Fill>
|
||||
<Ellipse.OpacityMask>
|
||||
<RadialGradientBrush GradientOrigin="0.5,0.5">
|
||||
<GradientStop Color="White" Offset="0.6" />
|
||||
<GradientStop Color="Transparent" Offset="0.8" />
|
||||
</RadialGradientBrush>
|
||||
</Ellipse.OpacityMask>
|
||||
<!-- Dynamische Größe -->
|
||||
<Ellipse.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="0.6" />
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
<!-- Gradient Überlagerung für 3D-Effekt -->
|
||||
<Border CornerRadius="20" Background="#FFB22222" Opacity="0.3" />
|
||||
|
||||
<!-- Content (Button-Text) -->
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextElement.FontSize="32"
|
||||
TextElement.FontWeight="Bold"
|
||||
/>
|
||||
<!-- Glanzeffekt -->
|
||||
<Border CornerRadius="20">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,0.3">
|
||||
<GradientStop Color="#80FFFFFF" Offset="0.0" />
|
||||
<GradientStop Color="#00FFFFFF" Offset="0.8" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<!-- Wellenkreis -->
|
||||
<Ellipse Name="RippleEffect"
|
||||
Fill="White"
|
||||
Opacity="0"
|
||||
Width="0"
|
||||
Height="0"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Ellipse.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
</Grid>
|
||||
<!-- Content (Button-Text) -->
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextElement.FontSize="32"
|
||||
TextElement.FontWeight="Bold" />
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- Hover-Effekt -->
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Background" Value="#FF4500" />
|
||||
<Setter TargetName="RippleEffect" Property="Opacity" Value="0.2" />
|
||||
</Trigger>
|
||||
<!-- Ripple-Effekt -->
|
||||
<Ellipse Name="RippleEffect"
|
||||
Fill="White"
|
||||
Opacity="0"
|
||||
Width="0"
|
||||
Height="0"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Ellipse.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
</Grid>
|
||||
|
||||
<!-- Klick-Effekt mit Wellen -->
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<!-- Wellenbewegung -->
|
||||
<DoubleAnimation Storyboard.TargetName="RippleEffect"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0.3" To="0" Duration="0:0:0.5" />
|
||||
<DoubleAnimation Storyboard.TargetName="RippleEffect"
|
||||
Storyboard.TargetProperty="Width"
|
||||
From="0" To="200" Duration="0:0:0.5" />
|
||||
<DoubleAnimation Storyboard.TargetName="RippleEffect"
|
||||
Storyboard.TargetProperty="Height"
|
||||
From="0" To="200" Duration="0:0:0.5" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Setter Property="Background" Value="#D32F2F" />
|
||||
</Trigger>
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- Hover-Effekt -->
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Background" Value="#FF4500" />
|
||||
<Setter TargetName="RippleEffect" Property="Opacity" Value="0.2" />
|
||||
</Trigger>
|
||||
|
||||
<!-- 3D Effekt beim Loslassen -->
|
||||
<Trigger Property="IsPressed" Value="False">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<!-- Bewegung nach oben simulieren -->
|
||||
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
||||
To="-2"
|
||||
Duration="0:0:0.2"
|
||||
AutoReverse="True" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<!-- Klick-Effekt mit Wellen -->
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<!-- Wellenbewegung -->
|
||||
<DoubleAnimation Storyboard.TargetName="RippleEffect"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0.3" To="0" Duration="0:0:0.5" />
|
||||
<DoubleAnimation Storyboard.TargetName="RippleEffect"
|
||||
Storyboard.TargetProperty="Width"
|
||||
From="0" To="200" Duration="0:0:0.5" />
|
||||
<DoubleAnimation Storyboard.TargetName="RippleEffect"
|
||||
Storyboard.TargetProperty="Height"
|
||||
From="0" To="200" Duration="0:0:0.5" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Setter Property="Background" Value="#D32F2F" />
|
||||
</Trigger>
|
||||
|
||||
<!-- 3D Effekt beim Loslassen -->
|
||||
<Trigger Property="IsPressed" Value="False">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<!-- Bewegung nach oben simulieren -->
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
||||
To="-2"
|
||||
Duration="0:0:0.2"
|
||||
AutoReverse="True" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
<!-- Icon Button Style Basis -->
|
||||
<Style x:Key="IconButtonStyle" TargetType="Button" BasedOn="{StaticResource ModernRounded3DButtonStyle}">
|
||||
<Setter Property="Padding" Value="16,12" />
|
||||
<Setter Property="ContentTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ContentPresenter Content="{TemplateBinding Content}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Nur Icon Buttons (ohne Text) -->
|
||||
<Style x:Key="IconOnlyButtonStyle" TargetType="Button" BasedOn="{StaticResource ModernRounded3DButtonStyle}">
|
||||
<Setter Property="Width" Value="60" />
|
||||
<Setter Property="Height" Value="60" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
|
||||
<!-- Material Design Icon Button Style -->
|
||||
<Style x:Key="MaterialIconButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="8" />
|
||||
<Setter Property="Width" Value="50" />
|
||||
<Setter Property="Height" Value="50" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontSize" Value="24" />
|
||||
<Setter Property="FontFamily" Value="Segoe MDL2 Assets" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid Background="Transparent">
|
||||
<Ellipse Name="HoverEllipse" Fill="White" Opacity="0" />
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="HoverEllipse" Property="Opacity" Value="0.15" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="HoverEllipse" Property="Opacity" Value="0.25" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Colored Material Icon Button Style -->
|
||||
<Style x:Key="MaterialIconButtonColoredStyle" TargetType="Button" BasedOn="{StaticResource MaterialIconButtonStyle}">
|
||||
<Setter Property="Width" Value="60" />
|
||||
<Setter Property="Height" Value="60" />
|
||||
<Setter Property="FontSize" Value="28" />
|
||||
<Setter Property="Foreground" Value="#1F1A00" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid>
|
||||
<Ellipse Name="BackgroundEllipse" Fill="#FFD700" />
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}" />
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="BackgroundEllipse" Property="Opacity" Value="0.8" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="BackgroundEllipse" Property="Opacity" Value="0.6" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Primary Action Button Style (Gold mit Rounded) -->
|
||||
<Style x:Key="PrimaryActionButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="#D4AF37" />
|
||||
<Setter Property="Foreground" Value="#1F1A00" />
|
||||
<Setter Property="BorderBrush" Value="#F6E7A1" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
<Setter Property="Padding" Value="16,8" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#F0C847" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#C49B2E" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Secondary Button Style (Dark mit Border) -->
|
||||
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="#3C3C3C" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="BorderBrush" Value="#555555" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="16,8" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#4C4C4C" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#2C2C2C" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Paging Button Style (für Bildergalerie) -->
|
||||
<Style x:Key="PagingButtonStyle" TargetType="Button" BasedOn="{StaticResource SecondaryButtonStyle}">
|
||||
<Setter Property="Width" Value="60" />
|
||||
<Setter Property="Height" Value="50" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="5,0" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
172
src/CamBooth/SESSION_NOT_OPEN_FIX.md
Normal file
172
src/CamBooth/SESSION_NOT_OPEN_FIX.md
Normal file
@ -0,0 +1,172 @@
|
||||
# SESSION_NOT_OPEN Error - Root Cause and Fix
|
||||
|
||||
## Problem Summary
|
||||
The application was failing with `SESSION_NOT_OPEN` error when trying to start the live view camera feed:
|
||||
```
|
||||
Failed to start live view: SESSION_NOT_OPEN at EOSDigital.API.STAThread.Invoke(Action action)
|
||||
at EOSDigital.API.Camera.OpenSession()
|
||||
at CamBooth.App.Features.Camera.CameraService.OpenSession()
|
||||
```
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Issue 1: Uninitialized Camera Instance at Startup
|
||||
In `App.xaml.cs`, the `ICamera` dependency is registered as a **Singleton at application startup**:
|
||||
```csharp
|
||||
services.AddSingleton<ICamera, Camera>();
|
||||
```
|
||||
|
||||
This creates a single Camera instance **before any actual physical camera is connected**, and this uninitialized instance is never properly updated when cameras are detected later.
|
||||
|
||||
### Issue 2: Stale Camera Reference
|
||||
The `CameraService` constructor receives this uninitialized `_mainCamera` instance:
|
||||
```csharp
|
||||
public CameraService(..., ICamera mainCamera, ICanonAPI APIHandler)
|
||||
{
|
||||
this._mainCamera = mainCamera; // This is the uninitialized singleton
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Issue 3: Incorrect Flow in ConnectCamera()
|
||||
The original `ConnectCamera()` method called `OpenSession()` **before reassigning** `_mainCamera`:
|
||||
```csharp
|
||||
public void ConnectCamera()
|
||||
{
|
||||
this.RefreshCamera(); // Gets camera list
|
||||
List<ICamera> cameraList = this._APIHandler.GetCameraList();
|
||||
if (cameraList.Any())
|
||||
{
|
||||
this.OpenSession(); // ← Uses OLD _mainCamera, not the newly detected one!
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `OpenSession()` method did try to reassign `_mainCamera`, but the error was thrown **before reaching that point** in some error conditions.
|
||||
|
||||
## The Fix
|
||||
|
||||
### Change 1: Update ConnectCamera() Method
|
||||
Moved the `_mainCamera` assignment **before** calling `OpenSession()`:
|
||||
|
||||
```csharp
|
||||
public void ConnectCamera()
|
||||
{
|
||||
ErrorHandler.SevereErrorHappened += this.ErrorHandler_SevereErrorHappened;
|
||||
ErrorHandler.NonSevereErrorHappened += this.ErrorHandler_NonSevereErrorHappened;
|
||||
|
||||
try
|
||||
{
|
||||
this.RefreshCamera();
|
||||
|
||||
// Retry logic for camera detection (some systems need time to initialize)
|
||||
int maxRetries = 3;
|
||||
int retryDelay = 500; // milliseconds
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
if (this.CamList != null && this.CamList.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
this._logger.Info($"No cameras found on attempt {attempt + 1}/{maxRetries}, retrying in {retryDelay}ms...");
|
||||
System.Threading.Thread.Sleep(retryDelay);
|
||||
this.RefreshCamera();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.CamList == null || !this.CamList.Any())
|
||||
{
|
||||
this.ReportError("No cameras / devices found");
|
||||
throw new InvalidOperationException("No cameras / devices found after multiple attempts");
|
||||
}
|
||||
|
||||
this._logger.Info($"Found {this.CamList.Count} camera(s)");
|
||||
string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName));
|
||||
this._logger.Info(cameraDeviceNames);
|
||||
|
||||
// ← UPDATE _mainCamera BEFORE calling OpenSession()
|
||||
this._mainCamera = this.CamList[0];
|
||||
this._logger.Info($"Selected camera: {this._mainCamera.DeviceName}");
|
||||
|
||||
this.OpenSession();
|
||||
this.SetSettingSaveToComputer();
|
||||
this.StarLiveView();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Error($"Error connecting camera: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key improvements:**
|
||||
1. **Explicit camera assignment before OpenSession()** - Ensures the correct physical camera is used
|
||||
2. **Retry logic** - Handles SDK initialization delays on some systems
|
||||
3. **Better error messages** - Clearer logging of the connection process
|
||||
|
||||
### Change 2: Simplify OpenSession() Method
|
||||
Removed redundant camera reassignment and simplified validation:
|
||||
|
||||
```csharp
|
||||
private void OpenSession()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this._mainCamera == null)
|
||||
{
|
||||
throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first.");
|
||||
}
|
||||
|
||||
// Check if session is already open
|
||||
if (this._mainCamera.SessionOpen)
|
||||
{
|
||||
this._logger.Info($"Camera session already open for {this._mainCamera.DeviceName}");
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.Info($"Opening session for camera: {this._mainCamera.DeviceName}");
|
||||
this._mainCamera.OpenSession();
|
||||
this._logger.Info("Camera session opened successfully");
|
||||
|
||||
this._mainCamera.StateChanged += this.MainCamera_StateChanged;
|
||||
this._mainCamera.DownloadReady += this.MainCamera_DownloadReady;
|
||||
|
||||
this.AvList = this._mainCamera.GetSettingsList(PropertyID.Av);
|
||||
this.TvList = this._mainCamera.GetSettingsList(PropertyID.Tv);
|
||||
this.ISOList = this._mainCamera.GetSettingsList(PropertyID.ISO);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Error($"Failed to open camera session: {ex.Message}");
|
||||
this.ReportError($"Failed to open camera session: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Improvements:**
|
||||
1. **No redundant camera assignment** - Camera is already set by ConnectCamera()
|
||||
2. **Clearer null check** - Validates that ConnectCamera() was called first
|
||||
3. **Maintained duplex open check** - Still checks if session is already open
|
||||
|
||||
## Files Modified
|
||||
- `CamBooth.App\Features\Camera\CameraService.cs`
|
||||
- `ConnectCamera()` method
|
||||
- `OpenSession()` method
|
||||
|
||||
## Testing Recommendations
|
||||
1. **Test with camera connected** - Verify live view starts successfully
|
||||
2. **Test with camera disconnected** - Verify graceful error handling with retries
|
||||
3. **Test with delayed camera detection** - Ensure retry logic gives SDK time to initialize
|
||||
4. **Test camera reconnection** - Disconnect and reconnect camera to verify session management
|
||||
|
||||
## Additional Notes
|
||||
- The singleton pattern for `ICamera` in dependency injection is not ideal, but this fix ensures the correct instance is used once a physical camera is detected
|
||||
- The retry logic (3 attempts, 500ms delay) can be adjusted based on system requirements
|
||||
- Consider future refactoring to use factory pattern for camera initialization instead of singleton
|
||||
Loading…
Reference in New Issue
Block a user