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 System.Windows;
|
||||||
|
|
||||||
using CamBooth.App.Core.AppSettings;
|
using CamBooth.App.Core.AppSettings;
|
||||||
@ -51,6 +51,7 @@ public partial class App : Application
|
|||||||
services.AddSingleton<AppSettingsService>();
|
services.AddSingleton<AppSettingsService>();
|
||||||
services.AddSingleton<PictureGalleryService>();
|
services.AddSingleton<PictureGalleryService>();
|
||||||
services.AddSingleton<LycheeUploadService>();
|
services.AddSingleton<LycheeUploadService>();
|
||||||
|
services.AddSingleton<UploadQueueService>();
|
||||||
services.AddSingleton<CameraService>();
|
services.AddSingleton<CameraService>();
|
||||||
|
|
||||||
// Zuerst den Provider bauen, um AppSettings zu laden
|
// Zuerst den Provider bauen, um AppSettings zu laden
|
||||||
@ -104,7 +105,42 @@ public partial class App : Application
|
|||||||
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
_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>();
|
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||||
mainWindow.Show();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -33,6 +33,11 @@
|
|||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4"/>
|
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0"/>
|
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0"/>
|
||||||
<PackageReference Include="WPF-UI" Version="4.0.0-rc.3"/>
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
||||||
"Username": "itob",
|
"Username": "itob",
|
||||||
"Password": "VfVyqal&Nv8U&P",
|
"Password": "VfVyqal&Nv8U&P",
|
||||||
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
|
"DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
|
||||||
"AutoUploadEnabled": true
|
"AutoUploadEnabled": true
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
|
|||||||
@ -44,8 +44,8 @@
|
|||||||
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
||||||
"Username": "itob",
|
"Username": "itob",
|
||||||
"Password": "VfVyqal&Nv8U&P",
|
"Password": "VfVyqal&Nv8U&P",
|
||||||
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
|
"DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
|
||||||
"AutoUploadEnabled": false
|
"AutoUploadEnabled": true
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;"
|
"DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;"
|
||||||
|
|||||||
@ -6,9 +6,19 @@
|
|||||||
xmlns:local="clr-namespace:CamBooth.App.Core"
|
xmlns:local="clr-namespace:CamBooth.App.Core"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="GenericOverlayWindow" Height="350" Width="600">
|
Title="GenericOverlayWindow" Height="350" Width="600">
|
||||||
|
<Window.Resources>
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/CamBooth.App;component/Resources/ButtonStyles.xaml" />
|
||||||
|
</Window.Resources>
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextBlock Name="TbHeader" FontSize="40" HorizontalAlignment="Center">Hoppla</TextBlock>
|
<TextBlock Name="TbHeader" FontSize="40" HorizontalAlignment="Center">Hoppla</TextBlock>
|
||||||
<TextBlock Name="TbContent" FontSize="25" HorizontalAlignment="Center" VerticalAlignment="Center">Sorry, da ging was schief!</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>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -68,10 +68,16 @@ public class Logger : IDisposable
|
|||||||
public Logger(IConfiguration configuration)
|
public Logger(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var logLevel = configuration["LoggingSettings:LogLevel"] ?? "Information";
|
var logLevel = configuration["LoggingSettings:LogLevel"] ?? "Information";
|
||||||
var logDirectory = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
|
var logDirectoryInput = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
|
||||||
var remoteServerUrl = configuration["LoggingSettings:RemoteServerUrl"];
|
var remoteServerUrl = configuration["LoggingSettings:RemoteServerUrl"];
|
||||||
var remoteServerApiKey = configuration["LoggingSettings:RemoteServerApiKey"];
|
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))
|
if (!Directory.Exists(logDirectory))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(logDirectory);
|
Directory.CreateDirectory(logDirectory);
|
||||||
|
|||||||
@ -22,6 +22,8 @@ public class CameraService : IDisposable
|
|||||||
|
|
||||||
private readonly LycheeUploadService _lycheeUploadService;
|
private readonly LycheeUploadService _lycheeUploadService;
|
||||||
|
|
||||||
|
private readonly UploadQueueService _uploadQueueService;
|
||||||
|
|
||||||
private readonly ICanonAPI _APIHandler;
|
private readonly ICanonAPI _APIHandler;
|
||||||
|
|
||||||
private CameraValue[] AvList;
|
private CameraValue[] AvList;
|
||||||
@ -47,6 +49,7 @@ public class CameraService : IDisposable
|
|||||||
AppSettingsService appSettings,
|
AppSettingsService appSettings,
|
||||||
PictureGalleryService pictureGalleryService,
|
PictureGalleryService pictureGalleryService,
|
||||||
LycheeUploadService lycheeUploadService,
|
LycheeUploadService lycheeUploadService,
|
||||||
|
UploadQueueService uploadQueueService,
|
||||||
ICamera mainCamera,
|
ICamera mainCamera,
|
||||||
ICanonAPI APIHandler)
|
ICanonAPI APIHandler)
|
||||||
{
|
{
|
||||||
@ -54,6 +57,7 @@ public class CameraService : IDisposable
|
|||||||
this._appSettings = appSettings;
|
this._appSettings = appSettings;
|
||||||
this._pictureGalleryService = pictureGalleryService;
|
this._pictureGalleryService = pictureGalleryService;
|
||||||
this._lycheeUploadService = lycheeUploadService;
|
this._lycheeUploadService = lycheeUploadService;
|
||||||
|
this._uploadQueueService = uploadQueueService;
|
||||||
this._mainCamera = mainCamera;
|
this._mainCamera = mainCamera;
|
||||||
this._APIHandler = APIHandler;
|
this._APIHandler = APIHandler;
|
||||||
try
|
try
|
||||||
@ -89,22 +93,44 @@ public class CameraService : IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.RefreshCamera();
|
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++)
|
||||||
{
|
{
|
||||||
|
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 reference to the freshly detected camera
|
||||||
|
this._mainCamera = this.CamList[0];
|
||||||
|
this._logger.Info($"Selected camera: {this._mainCamera.DeviceName}");
|
||||||
|
|
||||||
this.OpenSession();
|
this.OpenSession();
|
||||||
this.SetSettingSaveToComputer();
|
this.SetSettingSaveToComputer();
|
||||||
this.StarLiveView();
|
this.StarLiveView();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
this.ReportError("No cameras / devices found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string cameraDeviceNames = string.Join(", ", cameraList.Select(cam => cam.DeviceName));
|
|
||||||
this._logger.Info(cameraDeviceNames);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
this._logger.Error($"Error connecting camera: {ex.Message}");
|
this._logger.Error($"Error connecting camera: {ex.Message}");
|
||||||
@ -161,13 +187,11 @@ public class CameraService : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
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
|
// Check if session is already open
|
||||||
if (this._mainCamera.SessionOpen)
|
if (this._mainCamera.SessionOpen)
|
||||||
{
|
{
|
||||||
@ -377,34 +401,9 @@ public class CameraService : IDisposable
|
|||||||
this._pictureGalleryService.LoadThumbnailsToCache();
|
this._pictureGalleryService.LoadThumbnailsToCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-Upload zu Lychee, falls aktiviert
|
// Füge neues Foto zur Upload-Queue hinzu (wenn Auto-Upload aktiviert)
|
||||||
if (this._appSettings.LycheeAutoUploadEnabled)
|
this._uploadQueueService.QueueNewPhoto(savedPhotoPath);
|
||||||
{
|
this._logger.Info($"Foto zur Upload-Queue hinzugefügt: {Info.FileName}");
|
||||||
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}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -75,11 +75,9 @@
|
|||||||
x:Name="ConnectButton"
|
x:Name="ConnectButton"
|
||||||
Content="Verbinden"
|
Content="Verbinden"
|
||||||
Click="ConnectButton_Click"
|
Click="ConnectButton_Click"
|
||||||
|
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||||
Width="140"
|
Width="140"
|
||||||
Height="45"
|
Height="45" />
|
||||||
Background="#D4AF37"
|
|
||||||
Foreground="#1F1A00"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@ -165,18 +163,16 @@
|
|||||||
<ui:Button x:Name="UploadLastPhotoButton"
|
<ui:Button x:Name="UploadLastPhotoButton"
|
||||||
Content="Letztes Foto hochladen"
|
Content="Letztes Foto hochladen"
|
||||||
Click="UploadLastPhotoButton_Click"
|
Click="UploadLastPhotoButton_Click"
|
||||||
|
Style="{StaticResource SecondaryButtonStyle}"
|
||||||
Width="200"
|
Width="200"
|
||||||
Height="45"
|
Height="45"
|
||||||
Margin="0,0,10,0"
|
Margin="0,0,10,0" />
|
||||||
Appearance="Secondary" />
|
|
||||||
<ui:Button x:Name="UploadAllPhotosButton"
|
<ui:Button x:Name="UploadAllPhotosButton"
|
||||||
Content="Alle Fotos hochladen"
|
Content="Alle Fotos hochladen"
|
||||||
Click="UploadAllPhotosButton_Click"
|
Click="UploadAllPhotosButton_Click"
|
||||||
|
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||||
Width="200"
|
Width="200"
|
||||||
Height="45"
|
Height="45" />
|
||||||
Background="#D4AF37"
|
|
||||||
Foreground="#1F1A00"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
using CamBooth.App.Core.AppSettings;
|
using CamBooth.App.Core.AppSettings;
|
||||||
using CamBooth.App.Core.Logging;
|
using CamBooth.App.Core.Logging;
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ public class LycheeUploadService : IDisposable
|
|||||||
private readonly AppSettingsService _appSettings;
|
private readonly AppSettingsService _appSettings;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly QRCodeGenerationService _qrCodeGenerationService;
|
||||||
private readonly CookieContainer _cookieContainer;
|
private readonly CookieContainer _cookieContainer;
|
||||||
private readonly HttpClientHandler _httpClientHandler;
|
private readonly HttpClientHandler _httpClientHandler;
|
||||||
private string? _csrfToken;
|
private string? _csrfToken;
|
||||||
@ -27,6 +29,7 @@ public class LycheeUploadService : IDisposable
|
|||||||
{
|
{
|
||||||
_appSettings = appSettings;
|
_appSettings = appSettings;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_qrCodeGenerationService = new QRCodeGenerationService(logger);
|
||||||
|
|
||||||
// CookieContainer für Session-Management
|
// CookieContainer für Session-Management
|
||||||
_cookieContainer = new CookieContainer();
|
_cookieContainer = new CookieContainer();
|
||||||
@ -47,6 +50,11 @@ public class LycheeUploadService : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAuthenticated => _isAuthenticated;
|
public bool IsAuthenticated => _isAuthenticated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Der zuletzt generierte QR-Code
|
||||||
|
/// </summary>
|
||||||
|
public BitmapImage? LastGeneratedQRCode { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authentifiziert sich bei Lychee
|
/// Authentifiziert sich bei Lychee
|
||||||
/// </summary>
|
/// </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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_httpClient?.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" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</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">
|
<ScrollViewer Grid.Row="0" x:Name="GalleryScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Background="Black" CanContentScroll="False">
|
||||||
<ItemsControl x:Name="PicturesPanel" Background="Black">
|
<ItemsControl x:Name="PicturesPanel" Background="Black">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
@ -36,15 +49,17 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- First Page Button -->
|
<!-- First Page Button -->
|
||||||
<Button Grid.Column="0" x:Name="FirstPageButton" Content="⏮️"
|
<Button Grid.Column="0"
|
||||||
Width="60" Height="50" Margin="5,0"
|
x:Name="FirstPageButton"
|
||||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
Content="⏮️"
|
||||||
|
Style="{StaticResource PagingButtonStyle}"
|
||||||
Click="FirstPageButton_Click" />
|
Click="FirstPageButton_Click" />
|
||||||
|
|
||||||
<!-- Previous Page Button -->
|
<!-- Previous Page Button -->
|
||||||
<Button Grid.Column="1" x:Name="PreviousPageButton" Content="◀️"
|
<Button Grid.Column="1"
|
||||||
Width="60" Height="50" Margin="5,0"
|
x:Name="PreviousPageButton"
|
||||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
Content="◀️"
|
||||||
|
Style="{StaticResource PagingButtonStyle}"
|
||||||
Click="PreviousPageButton_Click" />
|
Click="PreviousPageButton_Click" />
|
||||||
|
|
||||||
<!-- Page Info -->
|
<!-- Page Info -->
|
||||||
@ -54,15 +69,17 @@
|
|||||||
Text="Seite 1 von 1" MinWidth="150" TextAlignment="Center" />
|
Text="Seite 1 von 1" MinWidth="150" TextAlignment="Center" />
|
||||||
|
|
||||||
<!-- Next Page Button -->
|
<!-- Next Page Button -->
|
||||||
<Button Grid.Column="3" x:Name="NextPageButton" Content="▶️"
|
<Button Grid.Column="3"
|
||||||
Width="60" Height="50" Margin="5,0"
|
x:Name="NextPageButton"
|
||||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
Content="▶️"
|
||||||
|
Style="{StaticResource PagingButtonStyle}"
|
||||||
Click="NextPageButton_Click" />
|
Click="NextPageButton_Click" />
|
||||||
|
|
||||||
<!-- Last Page Button -->
|
<!-- Last Page Button -->
|
||||||
<Button Grid.Column="4" x:Name="LastPageButton" Content="⏭️"
|
<Button Grid.Column="4"
|
||||||
Width="60" Height="50" Margin="5,0"
|
x:Name="LastPageButton"
|
||||||
FontSize="20" Background="#3C3C3C" Foreground="White"
|
Content="⏭️"
|
||||||
|
Style="{StaticResource PagingButtonStyle}"
|
||||||
Click="LastPageButton_Click" />
|
Click="LastPageButton_Click" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Documents;
|
using System.Windows.Documents;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
@ -7,6 +7,7 @@ using System.Windows.Media.Imaging;
|
|||||||
|
|
||||||
using CamBooth.App.Core.AppSettings;
|
using CamBooth.App.Core.AppSettings;
|
||||||
using CamBooth.App.Core.Logging;
|
using CamBooth.App.Core.Logging;
|
||||||
|
using CamBooth.App.Features.LycheeUpload;
|
||||||
|
|
||||||
using Wpf.Ui.Controls;
|
using Wpf.Ui.Controls;
|
||||||
|
|
||||||
@ -24,18 +25,24 @@ public partial class PictureGalleryPage : Page
|
|||||||
|
|
||||||
private readonly PictureGalleryService _pictureGalleryService;
|
private readonly PictureGalleryService _pictureGalleryService;
|
||||||
|
|
||||||
|
private readonly LycheeUploadService _lycheeUploadService;
|
||||||
|
|
||||||
private ContentDialog? _openContentDialog;
|
private ContentDialog? _openContentDialog;
|
||||||
|
|
||||||
private int _currentPage = 1;
|
private int _currentPage = 1;
|
||||||
private int _itemsPerPage = 12;
|
private int _itemsPerPage = 11;
|
||||||
private int _totalPages = 1;
|
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._appSettingsService = appSettingsService;
|
||||||
this._logger = logger;
|
this._logger = logger;
|
||||||
this._pictureGalleryService = pictureGalleryService;
|
this._pictureGalleryService = pictureGalleryService;
|
||||||
|
this._lycheeUploadService = lycheeUploadService;
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
this.Initialize();
|
this.Initialize();
|
||||||
}
|
}
|
||||||
@ -45,6 +52,9 @@ public partial class PictureGalleryPage : Page
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// QR-Code nur einmal beim Initialisieren erstellen
|
||||||
|
CreateQRCodeBorder();
|
||||||
|
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
CalculateTotalPages();
|
CalculateTotalPages();
|
||||||
LoadCurrentPage();
|
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)
|
private void ContentDialog_OnButtonClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
{
|
{
|
||||||
@ -74,6 +134,13 @@ public partial class PictureGalleryPage : Page
|
|||||||
// Clear existing items
|
// Clear existing items
|
||||||
this.PicturesPanel.Items.Clear();
|
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 totalThumbnails = this._pictureGalleryService.ThumbnailsOrderedByNewestDescending.Count;
|
||||||
int endIndex = Math.Min(startIndex + count, totalThumbnails);
|
int endIndex = Math.Min(startIndex + count, totalThumbnails);
|
||||||
|
|
||||||
@ -276,6 +343,19 @@ public partial class PictureGalleryPage : Page
|
|||||||
UpdatePagerControls();
|
UpdatePagerControls();
|
||||||
GalleryScrollViewer.ScrollToTop();
|
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,11 +53,229 @@
|
|||||||
Margin="24 24 24 12"/>
|
Margin="24 24 24 12"/>
|
||||||
<liveView:TimerControlRectangleAnimation x:Name="TimerControlRectangleAnimation" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
<liveView:TimerControlRectangleAnimation x:Name="TimerControlRectangleAnimation" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</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) -->
|
<!-- Hide Debug Button (top-right) -->
|
||||||
<ui:Button Grid.Row="0"
|
<Button Grid.Row="0"
|
||||||
x:Name="HideDebugButton"
|
x:Name="HideDebugButton"
|
||||||
Content="Hide Debug"
|
Content="Hide Debug"
|
||||||
Click="SetVisibilityDebugConsole"
|
Click="SetVisibilityDebugConsole"
|
||||||
|
Style="{StaticResource SecondaryButtonStyle}"
|
||||||
Width="160"
|
Width="160"
|
||||||
Height="60"
|
Height="60"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
@ -65,16 +283,15 @@
|
|||||||
Margin="20"
|
Margin="20"
|
||||||
Panel.ZIndex="5"
|
Panel.ZIndex="5"
|
||||||
Visibility="Hidden"
|
Visibility="Hidden"
|
||||||
FontSize="16"
|
FontSize="16" />
|
||||||
Appearance="Secondary" />
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom" Name="ButtonPanel" Background="Transparent" Panel.ZIndex="2"
|
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom" Name="ButtonPanel" Background="Transparent" Panel.ZIndex="2"
|
||||||
Visibility="Hidden"
|
Visibility="Hidden"
|
||||||
Margin="0 0 0 0">
|
Margin="0 0 0 30">
|
||||||
<!-- <ui:Button Content="Take Photo" Click="StartTakePhotoProcess" Width="200" Height="75" VerticalAlignment="Bottom" -->
|
<!-- Take Photo Button -->
|
||||||
<!-- Margin="0 0 5 0" /> -->
|
<Button Width="240" Height="120"
|
||||||
<Button Width="160" Height="160"
|
|
||||||
Click="StartTakePhotoProcess"
|
Click="StartTakePhotoProcess"
|
||||||
|
Style="{StaticResource ModernRounded3DButtonStyle}"
|
||||||
FontSize="64"
|
FontSize="64"
|
||||||
Foreground="White"
|
Foreground="White"
|
||||||
Margin="0 0 5 0"
|
Margin="0 0 5 0"
|
||||||
@ -82,57 +299,25 @@
|
|||||||
BorderBrush="Transparent"
|
BorderBrush="Transparent"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Style="{StaticResource ModernRounded3DButtonStyle}"/>
|
Content="🔴"/>
|
||||||
<ui:Button x:Name="DebugCloseButton" Content="Close" Appearance="Danger" Click="CloseApp" Width="200" Height="75" VerticalAlignment="Bottom" Margin="0 0 5 0" />
|
<Button x:Name="DebugCloseButton"
|
||||||
</StackPanel>
|
Content="Close"
|
||||||
|
Style="{StaticResource SecondaryButtonStyle}"
|
||||||
<!-- Picture Gallery Dock (bottom-right) -->
|
Click="CloseApp"
|
||||||
<Grid Grid.Row="0"
|
Width="200"
|
||||||
x:Name="PictureGalleryDock"
|
Height="75"
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Margin="20"
|
Margin="0 0 5 0"
|
||||||
Panel.ZIndex="3"
|
FontSize="16"
|
||||||
Visibility="Hidden">
|
FontWeight="Bold" />
|
||||||
<ui:Button Content=""
|
</StackPanel>
|
||||||
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 -->
|
<!-- Gallery Prompt -->
|
||||||
<Border Grid.Row="0"
|
<Border Grid.Row="0"
|
||||||
x:Name="GalleryPrompt"
|
x:Name="GalleryPrompt"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Top"
|
||||||
Margin="20"
|
Margin="20,80,20,20"
|
||||||
Padding="16"
|
Padding="16"
|
||||||
Background="#E61A1A1A"
|
Background="#E61A1A1A"
|
||||||
BorderBrush="#66FFFFFF"
|
BorderBrush="#66FFFFFF"
|
||||||
@ -147,14 +332,11 @@
|
|||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="0 0 12 0" />
|
Margin="0 0 12 0" />
|
||||||
<ui:Button Content="Jetzt in Galerie ansehen"
|
<Button Content="Jetzt in Galerie ansehen"
|
||||||
Click="OpenGalleryFromPrompt"
|
Click="OpenGalleryFromPrompt"
|
||||||
|
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||||
Width="240"
|
Width="240"
|
||||||
Height="52"
|
Height="52"
|
||||||
Background="#D4AF37"
|
|
||||||
Foreground="#1F1A00"
|
|
||||||
BorderBrush="#F6E7A1"
|
|
||||||
BorderThickness="2"
|
|
||||||
FontWeight="SemiBold" />
|
FontWeight="SemiBold" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@ -205,11 +387,86 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</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 -->
|
<!-- Welcome Overlay -->
|
||||||
<Grid Grid.RowSpan="2"
|
<Grid Grid.RowSpan="2"
|
||||||
x:Name="WelcomeOverlay"
|
x:Name="WelcomeOverlay"
|
||||||
Background="#CC000000"
|
Background="#CC000000"
|
||||||
Panel.ZIndex="10">
|
Panel.ZIndex="10"
|
||||||
|
Visibility="Collapsed">
|
||||||
<Border Background="#E61A1A1A"
|
<Border Background="#E61A1A1A"
|
||||||
BorderBrush="#66FFFFFF"
|
BorderBrush="#66FFFFFF"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
@ -232,6 +489,7 @@
|
|||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Margin="0 0 0 16"/>
|
Margin="0 0 0 16"/>
|
||||||
<ui:Button Click="StartExperience"
|
<ui:Button Click="StartExperience"
|
||||||
|
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||||
Width="480"
|
Width="480"
|
||||||
Height="100"
|
Height="100"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
@ -74,7 +76,10 @@ public partial class MainWindow : Window
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.SetVisibilityDebugConsole(_appSettings.IsDebugConsoleVisible);
|
this.SetVisibilityDebugConsole(_appSettings.IsDebugConsoleVisible);
|
||||||
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
|
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
|
||||||
_ = this._pictureGalleryService.LoadThumbnailsToCache();
|
|
||||||
|
// Lade Thumbnails asynchron und zeige dann den Welcome Screen
|
||||||
|
_ = InitializeAsync();
|
||||||
|
|
||||||
this.Closing += OnClosing;
|
this.Closing += OnClosing;
|
||||||
TimerControlRectangleAnimation.OnTimerEllapsed += TimerControlRectangleAnimation_OnTimerEllapsed;
|
TimerControlRectangleAnimation.OnTimerEllapsed += TimerControlRectangleAnimation_OnTimerEllapsed;
|
||||||
this._focusStatusAnimationTimer.Tick += (_, _) =>
|
this._focusStatusAnimationTimer.Tick += (_, _) =>
|
||||||
@ -112,6 +117,114 @@ public partial class MainWindow : Window
|
|||||||
logger.Info("MainWindow initialized");
|
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()
|
private void TimerControlRectangleAnimation_OnTimerEllapsed()
|
||||||
{
|
{
|
||||||
@ -147,20 +260,28 @@ public partial class MainWindow : Window
|
|||||||
if (visibility)
|
if (visibility)
|
||||||
{
|
{
|
||||||
this.HideGalleryPrompt();
|
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
|
// Reset new photo count when opening gallery
|
||||||
this._pictureGalleryService.ResetNewPhotoCount();
|
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
|
else
|
||||||
{
|
{
|
||||||
this.PicturePanel.ClearValue(MainWindow.ContentProperty);
|
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;
|
this._isPicturePanelVisible = !visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void ClosePicturePanel()
|
public void ClosePicturePanel()
|
||||||
{
|
{
|
||||||
if (this.PicturePanel.Content is PictureGalleryPage pictureGalleryPage)
|
if (this.PicturePanel.Content is PictureGalleryPage pictureGalleryPage)
|
||||||
{
|
{
|
||||||
@ -187,7 +308,7 @@ public partial class MainWindow : Window
|
|||||||
this.StartLiveViewIfNeeded();
|
this.StartLiveViewIfNeeded();
|
||||||
this.WelcomeOverlay.Visibility = Visibility.Collapsed;
|
this.WelcomeOverlay.Visibility = Visibility.Collapsed;
|
||||||
this.ButtonPanel.Visibility = Visibility.Visible;
|
this.ButtonPanel.Visibility = Visibility.Visible;
|
||||||
this.PictureGalleryDock.Visibility = Visibility.Visible;
|
this.ActionButtonsContainer.Visibility = Visibility.Visible;
|
||||||
this.ShutdownDock.Visibility = Visibility.Visible;
|
this.ShutdownDock.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,11 +405,11 @@ public partial class MainWindow : Window
|
|||||||
private void SwitchButtonAndTimerPanel()
|
private void SwitchButtonAndTimerPanel()
|
||||||
{
|
{
|
||||||
this.ButtonPanel.Visibility = this.ButtonPanel.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
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;
|
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);
|
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.");
|
System.Windows.MessageBox.Show("Windows konnte nicht heruntergefahren werden. Bitte erneut versuchen.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
this.Close();
|
{
|
||||||
|
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)
|
private void ToggleShutdownSlider(object sender, RoutedEventArgs e)
|
||||||
@ -454,4 +591,33 @@ public partial class MainWindow : Window
|
|||||||
this.NewPhotosBadge.Visibility = Visibility.Collapsed;
|
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,55 +1,44 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<!-- Runder Button Stil mit 3D-Effekt, Wellenbewegung und Schatten -->
|
|
||||||
|
<!-- Rechteckiger Button mit runden Ecken und 3D-Effekt -->
|
||||||
<Style x:Key="ModernRounded3DButtonStyle" TargetType="Button">
|
<Style x:Key="ModernRounded3DButtonStyle" TargetType="Button">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="Button">
|
<ControlTemplate TargetType="Button">
|
||||||
<Grid>
|
<Grid>
|
||||||
<!-- 3D Hintergrund -->
|
<!-- 3D Hintergrund mit abgerundeten Ecken -->
|
||||||
<Ellipse>
|
<Border CornerRadius="20" Background="#FF8B0000"
|
||||||
<Ellipse.Fill>
|
BorderBrush="Transparent"
|
||||||
<LinearGradientBrush StartPoint="0.3,0" EndPoint="0.7,1">
|
BorderThickness="0">
|
||||||
<GradientStop Color="#FFB22222" Offset="0.0" /> <!-- Obere Kante -->
|
<Border.Effect>
|
||||||
<GradientStop Color="#FF8B0000" Offset="1.0" /> <!-- Untere Kante -->
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Ellipse.Fill>
|
|
||||||
<Ellipse.Effect>
|
|
||||||
<DropShadowEffect Color="Black"
|
<DropShadowEffect Color="Black"
|
||||||
BlurRadius="20"
|
BlurRadius="20"
|
||||||
ShadowDepth="7"
|
ShadowDepth="7"
|
||||||
Opacity="0.6" />
|
Opacity="0.6" />
|
||||||
</Ellipse.Effect>
|
</Border.Effect>
|
||||||
</Ellipse>
|
</Border>
|
||||||
|
|
||||||
<!-- Glanzeffekt (Highlight) -->
|
<!-- Gradient Überlagerung für 3D-Effekt -->
|
||||||
<Ellipse>
|
<Border CornerRadius="20" Background="#FFB22222" Opacity="0.3" />
|
||||||
<Ellipse.Fill>
|
|
||||||
<RadialGradientBrush GradientOrigin="0.5,0.3" Center="0.5,0.3" RadiusX="1" RadiusY="1">
|
<!-- Glanzeffekt -->
|
||||||
<GradientStop Color="#90EEEEEE" Offset="0.0" />
|
<Border CornerRadius="20">
|
||||||
<GradientStop Color="#00EEEEEE" Offset="1.0" />
|
<Border.Background>
|
||||||
</RadialGradientBrush>
|
<LinearGradientBrush StartPoint="0,0" EndPoint="0,0.3">
|
||||||
</Ellipse.Fill>
|
<GradientStop Color="#80FFFFFF" Offset="0.0" />
|
||||||
<Ellipse.OpacityMask>
|
<GradientStop Color="#00FFFFFF" Offset="0.8" />
|
||||||
<RadialGradientBrush GradientOrigin="0.5,0.5">
|
</LinearGradientBrush>
|
||||||
<GradientStop Color="White" Offset="0.6" />
|
</Border.Background>
|
||||||
<GradientStop Color="Transparent" Offset="0.8" />
|
</Border>
|
||||||
</RadialGradientBrush>
|
|
||||||
</Ellipse.OpacityMask>
|
|
||||||
<!-- Dynamische Größe -->
|
|
||||||
<Ellipse.RenderTransform>
|
|
||||||
<ScaleTransform ScaleX="1" ScaleY="0.6" />
|
|
||||||
</Ellipse.RenderTransform>
|
|
||||||
</Ellipse>
|
|
||||||
|
|
||||||
<!-- Content (Button-Text) -->
|
<!-- Content (Button-Text) -->
|
||||||
<ContentPresenter HorizontalAlignment="Center"
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
TextElement.FontSize="32"
|
TextElement.FontSize="32"
|
||||||
TextElement.FontWeight="Bold"
|
TextElement.FontWeight="Bold" />
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Wellenkreis -->
|
<!-- Ripple-Effekt -->
|
||||||
<Ellipse Name="RippleEffect"
|
<Ellipse Name="RippleEffect"
|
||||||
Fill="White"
|
Fill="White"
|
||||||
Opacity="0"
|
Opacity="0"
|
||||||
@ -97,7 +86,8 @@
|
|||||||
<BeginStoryboard>
|
<BeginStoryboard>
|
||||||
<Storyboard>
|
<Storyboard>
|
||||||
<!-- Bewegung nach oben simulieren -->
|
<!-- Bewegung nach oben simulieren -->
|
||||||
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
<DoubleAnimation
|
||||||
|
Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
||||||
To="-2"
|
To="-2"
|
||||||
Duration="0:0:0.2"
|
Duration="0:0:0.2"
|
||||||
AutoReverse="True" />
|
AutoReverse="True" />
|
||||||
@ -111,5 +101,163 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- 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>
|
</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