Compare commits
No commits in common. "097de823c09c43d386afbb21f0c06c822aaabc54" and "9b39de7b76858a691d4b0ca8074e00e2d7e91410" have entirely different histories.
097de823c0
...
9b39de7b76
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8" />
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
</project>
|
</project>
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<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;
|
||||||
@ -11,7 +11,6 @@ using EDSDKLib.API.Base;
|
|||||||
|
|
||||||
using EOSDigital.API;
|
using EOSDigital.API;
|
||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace CamBooth.App;
|
namespace CamBooth.App;
|
||||||
@ -27,31 +26,13 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
// Konfiguration laden
|
|
||||||
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
|
|
||||||
|
|
||||||
var configBuilder = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(AppContext.BaseDirectory)
|
|
||||||
.AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true);
|
|
||||||
|
|
||||||
if (environment == "Development")
|
|
||||||
{
|
|
||||||
configBuilder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var configuration = configBuilder.Build();
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
// Register Configuration
|
|
||||||
services.AddSingleton<IConfiguration>(configuration);
|
|
||||||
|
|
||||||
// Register base services
|
// Register base services
|
||||||
services.AddSingleton<Logger>();
|
services.AddSingleton<Logger>();
|
||||||
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
|
||||||
@ -105,42 +86,7 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -25,19 +25,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" />
|
||||||
<PackageReference Include="Serilog" Version="4.3.1"/>
|
|
||||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0"/>
|
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/>
|
|
||||||
<PackageReference Include="Serilog.Sinks.Http" Version="9.0.0"/>
|
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4"/>
|
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0"/>
|
|
||||||
<PackageReference Include="WPF-UI" Version="4.0.0-rc.3" />
|
<PackageReference Include="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>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using CamBooth.App.Core.Logging;
|
using CamBooth.App.Core.Logging;
|
||||||
|
|
||||||
namespace CamBooth.App.Core.AppSettings;
|
namespace CamBooth.App.Core.AppSettings;
|
||||||
|
|
||||||
@ -62,15 +62,6 @@ public class AppSettingsService
|
|||||||
|
|
||||||
public string ConfigFileName => loadedConfigFile;
|
public string ConfigFileName => loadedConfigFile;
|
||||||
|
|
||||||
// Logging Settings
|
|
||||||
public string LogLevel => configuration["LoggingSettings:LogLevel"] ?? "Information";
|
|
||||||
|
|
||||||
public string LogDirectory => configuration["LoggingSettings:LogDirectory"] ?? "Logs";
|
|
||||||
|
|
||||||
public string? RemoteServerUrl => configuration["LoggingSettings:RemoteServerUrl"];
|
|
||||||
|
|
||||||
public string? RemoteServerApiKey => configuration["LoggingSettings:RemoteServerApiKey"];
|
|
||||||
|
|
||||||
// Lychee Upload Settings
|
// Lychee Upload Settings
|
||||||
public string? LycheeApiUrl => configuration["LycheeSettings:ApiUrl"];
|
public string? LycheeApiUrl => configuration["LycheeSettings:ApiUrl"];
|
||||||
|
|
||||||
|
|||||||
@ -11,40 +11,11 @@
|
|||||||
"IsShutdownEnabled": false,
|
"IsShutdownEnabled": false,
|
||||||
"UseMockCamera": true
|
"UseMockCamera": true
|
||||||
},
|
},
|
||||||
"Serilog": {
|
|
||||||
"MinimumLevel": {
|
|
||||||
"Default": "Debug",
|
|
||||||
"Override": {
|
|
||||||
"Microsoft": "Warning",
|
|
||||||
"System": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"WriteTo": [
|
|
||||||
{
|
|
||||||
"Name": "Console"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "File",
|
|
||||||
"Args": {
|
|
||||||
"path": "Logs/cambooth-dev-.log",
|
|
||||||
"rollingInterval": "Day",
|
|
||||||
"retainedFileCountLimit": 7,
|
|
||||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"LoggingSettings": {
|
|
||||||
"LogLevel": "Debug",
|
|
||||||
"LogDirectory": "Logs",
|
|
||||||
"RemoteServerUrl": "https://log.grimma-fotobox.de",
|
|
||||||
"RemoteServerApiKey": "nhnVql3QNgoAxvDWmNyU"
|
|
||||||
},
|
|
||||||
"LycheeSettings": {
|
"LycheeSettings": {
|
||||||
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
"ApiUrl": "https://cambooth-pics.rblnews.de",
|
||||||
"Username": "itob",
|
"Username": "itob",
|
||||||
"Password": "VfVyqal&Nv8U&P",
|
"Password": "VfVyqal&Nv8U&P",
|
||||||
"DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
|
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
|
||||||
"AutoUploadEnabled": true
|
"AutoUploadEnabled": true
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
|
|||||||
@ -8,44 +8,15 @@
|
|||||||
"PhotoCountdownSeconds": 5,
|
"PhotoCountdownSeconds": 5,
|
||||||
"FocusDelaySeconds": 2,
|
"FocusDelaySeconds": 2,
|
||||||
"FocusTimeoutMs": 3000,
|
"FocusTimeoutMs": 3000,
|
||||||
"IsShutdownEnabled": false,
|
"IsShutdownEnabled": true,
|
||||||
"UseMockCamera": false
|
"UseMockCamera": true
|
||||||
},
|
|
||||||
"Serilog": {
|
|
||||||
"MinimumLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Override": {
|
|
||||||
"Microsoft": "Warning",
|
|
||||||
"System": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"WriteTo": [
|
|
||||||
{
|
|
||||||
"Name": "Console"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "File",
|
|
||||||
"Args": {
|
|
||||||
"path": "Logs/cambooth-.log",
|
|
||||||
"rollingInterval": "Day",
|
|
||||||
"retainedFileCountLimit": 120,
|
|
||||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"LoggingSettings": {
|
|
||||||
"LogLevel": "Information",
|
|
||||||
"LogDirectory": "Logs",
|
|
||||||
"RemoteServerUrl": "https://log.grimma-fotobox.de",
|
|
||||||
"RemoteServerApiKey": "8rjvr0zZmceuFZMYydKU"
|
|
||||||
},
|
},
|
||||||
"LycheeSettings": {
|
"LycheeSettings": {
|
||||||
"ApiUrl": "https://gallery.grimma-fotobox.de",
|
"ApiUrl": "https://cambooth-pics.rblnews.de",
|
||||||
"Username": "itob",
|
"Username": "itob",
|
||||||
"Password": "VfVyqal&Nv8U&P",
|
"Password": "VfVyqal&Nv8U&P",
|
||||||
"DefaultAlbumId": "ukPbEHxdV_q1BOpIVKah7TUR",
|
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",
|
||||||
"AutoUploadEnabled": true
|
"AutoUploadEnabled": false
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;"
|
"DefaultConnection": "Server=myServer;Database=myDB;User Id=myUser;Password=myPassword;"
|
||||||
|
|||||||
@ -6,19 +6,9 @@
|
|||||||
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"
|
<Button Click="ButtonBase_OnClick" Width="300" Height="100" FontSize="25" HorizontalAlignment="Center" VerticalAlignment="Bottom" Content="Schließen"></Button>
|
||||||
Width="300"
|
|
||||||
Height="100"
|
|
||||||
FontSize="25"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
Content="Schließen"
|
|
||||||
Style="{StaticResource PrimaryActionButtonStyle}"></Button>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@ -1,63 +1,13 @@
|
|||||||
using System;
|
using System.Globalization;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using Serilog;
|
|
||||||
using Serilog.Core;
|
|
||||||
using Serilog.Events;
|
|
||||||
using Serilog.Formatting.Json;
|
|
||||||
using Serilog.Sinks.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace CamBooth.App.Core.Logging;
|
namespace CamBooth.App.Core.Logging;
|
||||||
|
|
||||||
/// <summary>
|
public class Logger
|
||||||
/// Custom HTTP Client für Serilog HTTP Sink mit API-Key Support
|
|
||||||
/// </summary>
|
|
||||||
public class SeqHttpClient : IHttpClient
|
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly string _logsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||||
private readonly string _apiKey;
|
private readonly string _errorLogPath;
|
||||||
|
|
||||||
public SeqHttpClient(string apiKey = "")
|
|
||||||
{
|
|
||||||
_httpClient = new HttpClient();
|
|
||||||
_apiKey = apiKey;
|
|
||||||
|
|
||||||
// Setze API-Key Header, falls vorhanden
|
|
||||||
if (!string.IsNullOrWhiteSpace(_apiKey))
|
|
||||||
{
|
|
||||||
_httpClient.DefaultRequestHeaders.Add("X-Seq-Api-Key", _apiKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Configure(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
// Konfiguration vom HTTP Sink - nicht nötig für unseren Use-Case
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (var content = new StreamContent(contentStream))
|
|
||||||
{
|
|
||||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
|
||||||
return await _httpClient.PostAsync(requestUri, content, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_httpClient?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Logger : IDisposable
|
|
||||||
{
|
|
||||||
private readonly Serilog.Core.Logger _serilogLogger;
|
|
||||||
|
|
||||||
public event LoggingEventHandler? InfoLog;
|
public event LoggingEventHandler? InfoLog;
|
||||||
public event LoggingEventHandler? ErrorLog;
|
public event LoggingEventHandler? ErrorLog;
|
||||||
@ -65,191 +15,68 @@ public class Logger : IDisposable
|
|||||||
public event LoggingEventHandler? DebugLog;
|
public event LoggingEventHandler? DebugLog;
|
||||||
public delegate void LoggingEventHandler(string text);
|
public delegate void LoggingEventHandler(string text);
|
||||||
|
|
||||||
public Logger(IConfiguration configuration)
|
public Logger()
|
||||||
{
|
{
|
||||||
var logLevel = configuration["LoggingSettings:LogLevel"] ?? "Information";
|
// Logs-Ordner erstellen, falls nicht vorhanden
|
||||||
var logDirectoryInput = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
|
if (!Directory.Exists(_logsDirectory))
|
||||||
var remoteServerUrl = configuration["LoggingSettings:RemoteServerUrl"];
|
|
||||||
var remoteServerApiKey = configuration["LoggingSettings:RemoteServerApiKey"];
|
|
||||||
|
|
||||||
// Konvertiere zu absolutem Pfad
|
|
||||||
var logDirectory = Path.IsPathRooted(logDirectoryInput)
|
|
||||||
? logDirectoryInput
|
|
||||||
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logDirectoryInput);
|
|
||||||
|
|
||||||
// Erstelle Verzeichnis, falls nicht vorhanden
|
|
||||||
if (!Directory.Exists(logDirectory))
|
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(logDirectory);
|
Directory.CreateDirectory(_logsDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
var minimumLevel = ParseLogLevel(logLevel);
|
_errorLogPath = Path.Combine(_logsDirectory, "error.txt");
|
||||||
|
}
|
||||||
|
|
||||||
var loggerConfig = new LoggerConfiguration()
|
private void WriteToErrorLog(string message)
|
||||||
.MinimumLevel.Is(minimumLevel)
|
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
|
||||||
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
|
||||||
.Enrich.FromLogContext()
|
|
||||||
.Enrich.WithProperty("Application", "CamBooth")
|
|
||||||
.WriteTo.Console(
|
|
||||||
outputTemplate: "{Timestamp:dd.MM.yyyy HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
|
||||||
.WriteTo.File(
|
|
||||||
Path.Combine(logDirectory, "cambooth-.log"),
|
|
||||||
rollingInterval: RollingInterval.Day,
|
|
||||||
retainedFileCountLimit: 30,
|
|
||||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(remoteServerUrl))
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
loggerConfig.WriteTo.Seq(
|
File.AppendAllText(_errorLogPath, message + Environment.NewLine);
|
||||||
serverUrl: remoteServerUrl,
|
|
||||||
apiKey: remoteServerApiKey,
|
|
||||||
restrictedToMinimumLevel: minimumLevel
|
|
||||||
);
|
|
||||||
|
|
||||||
Console.WriteLine($"Seq Sink konfiguriert: {remoteServerUrl}");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Fehler beim Konfigurieren des Seq-Sinks: {ex.Message}");
|
Console.WriteLine($"Fehler beim Schreiben in Fehlerlog: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_serilogLogger = loggerConfig.CreateLogger();
|
|
||||||
|
|
||||||
Log.Logger = _serilogLogger;
|
|
||||||
|
|
||||||
Info($"Serilog Logger initialisiert - Level: {logLevel}, Directory: {logDirectory}");
|
|
||||||
if (!string.IsNullOrWhiteSpace(remoteServerUrl))
|
|
||||||
{
|
|
||||||
Info($"Remote Server konfiguriert: {remoteServerUrl}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogEventLevel ParseLogLevel(string level)
|
|
||||||
{
|
|
||||||
return level.ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"verbose" => LogEventLevel.Verbose,
|
|
||||||
"debug" => LogEventLevel.Debug,
|
|
||||||
"information" => LogEventLevel.Information,
|
|
||||||
"warning" => LogEventLevel.Warning,
|
|
||||||
"error" => LogEventLevel.Error,
|
|
||||||
"fatal" => LogEventLevel.Fatal,
|
|
||||||
_ => LogEventLevel.Information
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Info(string message)
|
public void Info(string message)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Application.Current?.Dispatcher != null)
|
|
||||||
{
|
{
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [INFO] " + message;
|
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [INFO] " + message;
|
||||||
InfoLog?.Invoke(formattedMessage);
|
InfoLog?.Invoke(message);
|
||||||
|
Console.WriteLine(message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_serilogLogger.Information(message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Fehler beim Info-Logging: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Warning(string message)
|
public void Warning(string message)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Application.Current?.Dispatcher != null)
|
|
||||||
{
|
{
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [WARNING] " + message;
|
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [WARNING] " + message;
|
||||||
WarningLog?.Invoke(formattedMessage);
|
WarningLog?.Invoke(message);
|
||||||
|
Console.WriteLine(message);
|
||||||
|
WriteToErrorLog(message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_serilogLogger.Warning(message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Fehler beim Warning-Logging: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Error(string message)
|
public void Error(string message)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Application.Current?.Dispatcher != null)
|
|
||||||
{
|
{
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [ERROR] " + message;
|
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [ERROR] " + message;
|
||||||
ErrorLog?.Invoke(formattedMessage);
|
ErrorLog?.Invoke(message);
|
||||||
|
Console.WriteLine(message);
|
||||||
|
WriteToErrorLog(message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_serilogLogger.Error(message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Fehler beim Error-Logging: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Error(string message, Exception exception)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Application.Current?.Dispatcher != null)
|
|
||||||
{
|
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [ERROR] " + message;
|
|
||||||
ErrorLog?.Invoke(formattedMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_serilogLogger.Error(exception, message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Fehler beim Error-Logging: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Debug(string message)
|
public void Debug(string message)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Application.Current?.Dispatcher != null)
|
|
||||||
{
|
{
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [DEBUG] " + message;
|
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [DEBUG] " + message;
|
||||||
DebugLog?.Invoke(formattedMessage);
|
DebugLog?.Invoke(message);
|
||||||
|
Console.WriteLine(message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_serilogLogger.Debug(message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Fehler beim Debug-Logging: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_serilogLogger?.Dispose();
|
|
||||||
Log.CloseAndFlush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -22,8 +22,6 @@ 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;
|
||||||
@ -49,7 +47,6 @@ 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)
|
||||||
{
|
{
|
||||||
@ -57,7 +54,6 @@ 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
|
||||||
@ -93,44 +89,22 @@ public class CameraService : IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.RefreshCamera();
|
this.RefreshCamera();
|
||||||
|
List<ICamera> cameraList = this._APIHandler.GetCameraList();
|
||||||
// Retry logic for camera detection (some systems need time to initialize)
|
if (cameraList.Any())
|
||||||
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}");
|
||||||
@ -187,11 +161,13 @@ public class CameraService : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (this._mainCamera == null)
|
if (this.CamList == null || this.CamList.Count == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first.");
|
throw new InvalidOperationException("No cameras available in camera list");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
@ -401,9 +377,34 @@ public class CameraService : IDisposable
|
|||||||
this._pictureGalleryService.LoadThumbnailsToCache();
|
this._pictureGalleryService.LoadThumbnailsToCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Füge neues Foto zur Upload-Queue hinzu (wenn Auto-Upload aktiviert)
|
// Auto-Upload zu Lychee, falls aktiviert
|
||||||
this._uploadQueueService.QueueNewPhoto(savedPhotoPath);
|
if (this._appSettings.LycheeAutoUploadEnabled)
|
||||||
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,9 +75,11 @@
|
|||||||
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>
|
||||||
|
|
||||||
@ -163,16 +165,18 @@
|
|||||||
<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,11 +1,10 @@
|
|||||||
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;
|
||||||
|
|
||||||
@ -19,7 +18,6 @@ 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;
|
||||||
@ -29,7 +27,6 @@ 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();
|
||||||
@ -50,11 +47,6 @@ 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>
|
||||||
@ -540,52 +532,6 @@ 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();
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,288 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,195 +0,0 @@
|
|||||||
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,19 +14,6 @@
|
|||||||
<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>
|
||||||
@ -49,17 +36,15 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- First Page Button -->
|
<!-- First Page Button -->
|
||||||
<Button Grid.Column="0"
|
<Button Grid.Column="0" x:Name="FirstPageButton" Content="⏮️"
|
||||||
x:Name="FirstPageButton"
|
Width="60" Height="50" Margin="5,0"
|
||||||
Content="⏮️"
|
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||||
Style="{StaticResource PagingButtonStyle}"
|
|
||||||
Click="FirstPageButton_Click" />
|
Click="FirstPageButton_Click" />
|
||||||
|
|
||||||
<!-- Previous Page Button -->
|
<!-- Previous Page Button -->
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1" x:Name="PreviousPageButton" Content="◀️"
|
||||||
x:Name="PreviousPageButton"
|
Width="60" Height="50" Margin="5,0"
|
||||||
Content="◀️"
|
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||||
Style="{StaticResource PagingButtonStyle}"
|
|
||||||
Click="PreviousPageButton_Click" />
|
Click="PreviousPageButton_Click" />
|
||||||
|
|
||||||
<!-- Page Info -->
|
<!-- Page Info -->
|
||||||
@ -69,17 +54,15 @@
|
|||||||
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"
|
<Button Grid.Column="3" x:Name="NextPageButton" Content="▶️"
|
||||||
x:Name="NextPageButton"
|
Width="60" Height="50" Margin="5,0"
|
||||||
Content="▶️"
|
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||||
Style="{StaticResource PagingButtonStyle}"
|
|
||||||
Click="NextPageButton_Click" />
|
Click="NextPageButton_Click" />
|
||||||
|
|
||||||
<!-- Last Page Button -->
|
<!-- Last Page Button -->
|
||||||
<Button Grid.Column="4"
|
<Button Grid.Column="4" x:Name="LastPageButton" Content="⏭️"
|
||||||
x:Name="LastPageButton"
|
Width="60" Height="50" Margin="5,0"
|
||||||
Content="⏭️"
|
FontSize="20" Background="#3C3C3C" Foreground="White"
|
||||||
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,7 +7,6 @@ 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;
|
||||||
|
|
||||||
@ -25,24 +24,18 @@ 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 = 11;
|
private int _itemsPerPage = 12;
|
||||||
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();
|
||||||
}
|
}
|
||||||
@ -52,9 +45,6 @@ public partial class PictureGalleryPage : Page
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// QR-Code nur einmal beim Initialisieren erstellen
|
|
||||||
CreateQRCodeBorder();
|
|
||||||
|
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
CalculateTotalPages();
|
CalculateTotalPages();
|
||||||
LoadCurrentPage();
|
LoadCurrentPage();
|
||||||
@ -66,56 +56,6 @@ 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)
|
||||||
{
|
{
|
||||||
@ -134,13 +74,6 @@ 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);
|
||||||
|
|
||||||
@ -343,19 +276,6 @@ 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,229 +53,11 @@
|
|||||||
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) -->
|
||||||
<Button Grid.Row="0"
|
<ui: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"
|
||||||
@ -283,15 +65,16 @@
|
|||||||
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 30">
|
Margin="0 0 0 0">
|
||||||
<!-- Take Photo Button -->
|
<!-- <ui:Button Content="Take Photo" Click="StartTakePhotoProcess" Width="200" Height="75" VerticalAlignment="Bottom" -->
|
||||||
<Button Width="240" Height="120"
|
<!-- Margin="0 0 5 0" /> -->
|
||||||
|
<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"
|
||||||
@ -299,25 +82,57 @@
|
|||||||
BorderBrush="Transparent"
|
BorderBrush="Transparent"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Content="🔴"/>
|
Style="{StaticResource ModernRounded3DButtonStyle}"/>
|
||||||
<Button x:Name="DebugCloseButton"
|
<ui:Button x:Name="DebugCloseButton" Content="Close" Appearance="Danger" Click="CloseApp" Width="200" Height="75" VerticalAlignment="Bottom" Margin="0 0 5 0" />
|
||||||
Content="Close"
|
|
||||||
Style="{StaticResource SecondaryButtonStyle}"
|
|
||||||
Click="CloseApp"
|
|
||||||
Width="200"
|
|
||||||
Height="75"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
Margin="0 0 5 0"
|
|
||||||
FontSize="16"
|
|
||||||
FontWeight="Bold" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Picture Gallery Dock (bottom-right) -->
|
||||||
|
<Grid Grid.Row="0"
|
||||||
|
x:Name="PictureGalleryDock"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Margin="20"
|
||||||
|
Panel.ZIndex="3"
|
||||||
|
Visibility="Hidden">
|
||||||
|
<ui:Button Content=""
|
||||||
|
FontFamily="Segoe MDL2 Assets"
|
||||||
|
FontSize="30"
|
||||||
|
Width="72"
|
||||||
|
Height="72"
|
||||||
|
Click="SetVisibilityPicturePanel"
|
||||||
|
Background="#D4AF37"
|
||||||
|
Foreground="#1F1A00"
|
||||||
|
BorderBrush="#F6E7A1"
|
||||||
|
BorderThickness="2" />
|
||||||
|
<!-- Badge for new photos -->
|
||||||
|
<Border x:Name="NewPhotosBadge"
|
||||||
|
Background="#E61A1A1A"
|
||||||
|
BorderBrush="#FF0000"
|
||||||
|
BorderThickness="2"
|
||||||
|
CornerRadius="12"
|
||||||
|
Width="36"
|
||||||
|
Height="36"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,-8,-8,0"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Panel.ZIndex="1">
|
||||||
|
<TextBlock x:Name="NewPhotoCountText"
|
||||||
|
Text="1"
|
||||||
|
Foreground="White"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="Bold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- Gallery Prompt -->
|
<!-- Gallery Prompt -->
|
||||||
<Border Grid.Row="0"
|
<Border Grid.Row="0"
|
||||||
x:Name="GalleryPrompt"
|
x:Name="GalleryPrompt"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Bottom"
|
||||||
Margin="20,80,20,20"
|
Margin="20"
|
||||||
Padding="16"
|
Padding="16"
|
||||||
Background="#E61A1A1A"
|
Background="#E61A1A1A"
|
||||||
BorderBrush="#66FFFFFF"
|
BorderBrush="#66FFFFFF"
|
||||||
@ -332,11 +147,14 @@
|
|||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="0 0 12 0" />
|
Margin="0 0 12 0" />
|
||||||
<Button Content="Jetzt in Galerie ansehen"
|
<ui: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>
|
||||||
@ -387,86 +205,11 @@
|
|||||||
</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"
|
||||||
@ -489,7 +232,6 @@
|
|||||||
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,10 +1,8 @@
|
|||||||
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;
|
||||||
@ -76,10 +74,7 @@ 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 += (_, _) =>
|
||||||
@ -117,114 +112,6 @@ 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()
|
||||||
{
|
{
|
||||||
@ -260,28 +147,20 @@ 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._lycheeUploadService));
|
this.PicturePanel.Navigate(new PictureGalleryPage(this._appSettings, this._logger, this._pictureGalleryService));
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void ClosePicturePanel()
|
private void ClosePicturePanel()
|
||||||
{
|
{
|
||||||
if (this.PicturePanel.Content is PictureGalleryPage pictureGalleryPage)
|
if (this.PicturePanel.Content is PictureGalleryPage pictureGalleryPage)
|
||||||
{
|
{
|
||||||
@ -308,7 +187,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.ActionButtonsContainer.Visibility = Visibility.Visible;
|
this.PictureGalleryDock.Visibility = Visibility.Visible;
|
||||||
this.ShutdownDock.Visibility = Visibility.Visible;
|
this.ShutdownDock.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,11 +284,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.ActionButtonsContainer.Visibility = this.ActionButtonsContainer.Visibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden;
|
this.PictureGalleryDock.Visibility = this.PictureGalleryDock.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, MouseButtonEventArgs e)
|
private void SetVisibilityPicturePanel(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
|
this.SetVisibilityPicturePanel(this._isPicturePanelVisible);
|
||||||
}
|
}
|
||||||
@ -460,24 +339,8 @@ 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)
|
||||||
@ -591,33 +454,4 @@ 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,44 +1,55 @@
|
|||||||
<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 mit abgerundeten Ecken -->
|
<!-- 3D Hintergrund -->
|
||||||
<Border CornerRadius="20" Background="#FF8B0000"
|
<Ellipse>
|
||||||
BorderBrush="Transparent"
|
<Ellipse.Fill>
|
||||||
BorderThickness="0">
|
<LinearGradientBrush StartPoint="0.3,0" EndPoint="0.7,1">
|
||||||
<Border.Effect>
|
<GradientStop Color="#FFB22222" Offset="0.0" /> <!-- Obere Kante -->
|
||||||
|
<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" />
|
||||||
</Border.Effect>
|
</Ellipse.Effect>
|
||||||
</Border>
|
</Ellipse>
|
||||||
|
|
||||||
<!-- Gradient Überlagerung für 3D-Effekt -->
|
<!-- Glanzeffekt (Highlight) -->
|
||||||
<Border CornerRadius="20" Background="#FFB22222" Opacity="0.3" />
|
<Ellipse>
|
||||||
|
<Ellipse.Fill>
|
||||||
<!-- Glanzeffekt -->
|
<RadialGradientBrush GradientOrigin="0.5,0.3" Center="0.5,0.3" RadiusX="1" RadiusY="1">
|
||||||
<Border CornerRadius="20">
|
<GradientStop Color="#90EEEEEE" Offset="0.0" />
|
||||||
<Border.Background>
|
<GradientStop Color="#00EEEEEE" Offset="1.0" />
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,0.3">
|
</RadialGradientBrush>
|
||||||
<GradientStop Color="#80FFFFFF" Offset="0.0" />
|
</Ellipse.Fill>
|
||||||
<GradientStop Color="#00FFFFFF" Offset="0.8" />
|
<Ellipse.OpacityMask>
|
||||||
</LinearGradientBrush>
|
<RadialGradientBrush GradientOrigin="0.5,0.5">
|
||||||
</Border.Background>
|
<GradientStop Color="White" Offset="0.6" />
|
||||||
</Border>
|
<GradientStop Color="Transparent" Offset="0.8" />
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Ripple-Effekt -->
|
<!-- Wellenkreis -->
|
||||||
<Ellipse Name="RippleEffect"
|
<Ellipse Name="RippleEffect"
|
||||||
Fill="White"
|
Fill="White"
|
||||||
Opacity="0"
|
Opacity="0"
|
||||||
@ -86,8 +97,7 @@
|
|||||||
<BeginStoryboard>
|
<BeginStoryboard>
|
||||||
<Storyboard>
|
<Storyboard>
|
||||||
<!-- Bewegung nach oben simulieren -->
|
<!-- Bewegung nach oben simulieren -->
|
||||||
<DoubleAnimation
|
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
||||||
Storyboard.TargetProperty="RenderTransform.(TranslateTransform.Y)"
|
|
||||||
To="-2"
|
To="-2"
|
||||||
Duration="0:0:0.2"
|
Duration="0:0:0.2"
|
||||||
AutoReverse="True" />
|
AutoReverse="True" />
|
||||||
@ -101,163 +111,5 @@
|
|||||||
</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>
|
||||||
@ -6,8 +6,6 @@
|
|||||||
- Energiesparmodus abschalten
|
- Energiesparmodus abschalten
|
||||||
- Starbildschirm mit freundlicher Begrüßung, kurzer Erklärung, und viel Spaß wünschen mit der FotoCam
|
- Starbildschirm mit freundlicher Begrüßung, kurzer Erklärung, und viel Spaß wünschen mit der FotoCam
|
||||||
- Verschiedene Hinweise anzeigen beim Fotografieren (lächeln, Hasensohren, Zunge raus, Grimasse, usw.)
|
- Verschiedene Hinweise anzeigen beim Fotografieren (lächeln, Hasensohren, Zunge raus, Grimasse, usw.)
|
||||||
- Bild über QR Code runterladen (QR Code anzeigen, sowie ausdrucken und anklebene)
|
- Bild über QR Code runterladen
|
||||||
- Windows updates deaktivieren
|
- Windows updates deaktivieren
|
||||||
- logging einbinden (Elastic order ähnliches)
|
- logging einbinden (Elastic order ähnliches)
|
||||||
- Router anschließen für Upload
|
|
||||||
- Configs kontrollieren auf Fotobox
|
|
||||||
|
|||||||
@ -1,172 +0,0 @@
|
|||||||
# 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