using System; using System.IO; using System.Linq; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Net.Http; using System.Net.Http.Headers; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Platform.Storage; using Avalonia.Platform; using Avalonia.Threading; using PhotoboothUploader.Models; using PhotoboothUploader.Services; namespace PhotoboothUploader; public partial class MainWindow : Window { private const string DefaultUploadUrl = "https://stylegallery.fotospiel.app/api/v1/photobooth/upload"; private readonly SettingsStore _settingsStore = new(); private readonly UploadService _uploadService = new(); private PhotoboothSettings _settings; private FileSystemWatcher? _watcher; private readonly Dictionary _uploadsByPath = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _failedPaths = new(StringComparer.OrdinalIgnoreCase); private readonly string _userAgent; private int _queuedCount; private int _uploadingCount; private int _failedCount; private DateTimeOffset? _lastSuccessAt; private readonly DispatcherTimer _liveTimer = new(); private readonly List _logBuffer = new(); public ObservableCollection RecentUploads { get; } = new(); public MainWindow() { InitializeComponent(); _settings = _settingsStore.Load(); EnsureSettingsCollections(); if (string.IsNullOrWhiteSpace(_settings.UploadUrl)) { _settings.UploadUrl = DefaultUploadUrl; _settings.BaseUrl = ExtractBaseUrl(_settings.UploadUrl); } if (_settings.MaxConcurrentUploads <= 0) { _settings.MaxConcurrentUploads = 2; } _userAgent = $"AIStylegalleryPhotoboothUploader/{GetAppVersion()}"; _uploadService.Configure(_userAgent); _settingsStore.Save(_settings); DataContext = this; _liveTimer.Interval = TimeSpan.FromSeconds(30); _liveTimer.Tick += (_, _) => UpdateLiveStatus(); _liveTimer.Start(); Opened += OnWindowOpened; Closing += OnWindowClosing; ApplySettings(); } private async void PickFolderButton_Click(object? sender, RoutedEventArgs e) { var options = new FolderPickerOpenOptions { Title = "Upload-Ordner auswählen", AllowMultiple = false, }; var folders = await StorageProvider.OpenFolderPickerAsync(options); var folder = folders.FirstOrDefault(); var localPath = folder?.TryGetLocalPath(); if (string.IsNullOrWhiteSpace(localPath)) { return; } _settings.WatchFolder = localPath; _settingsStore.Save(_settings); FolderText.Text = localPath; UpdateFolderHealth(); StartUploadPipelineIfReady(); } private void DslrBoothPresetButton_Click(object? sender, RoutedEventArgs e) { ApplyPresetFolder(GetDslrBoothFolder()); } private void SparkboothPresetButton_Click(object? sender, RoutedEventArgs e) { ApplyPresetFolder(GetSparkboothFolder()); } private void ApplySettings() { if (!string.IsNullOrWhiteSpace(_settings.WatchFolder)) { FolderText.Text = _settings.WatchFolder; } MaxUploadsBox.Text = _settings.MaxConcurrentUploads.ToString(); UploadTempoBox.SelectedIndex = ResolveUploadTempoIndex(_settings.UploadDelayMs); IncludePatternsBox.Text = _settings.IncludePatterns ?? string.Empty; ExcludePatternsBox.Text = _settings.ExcludePatterns ?? string.Empty; ResponseFormatBox.SelectedIndex = ResolveResponseFormatIndex(_settings.ResponseFormat); ManualUploadUrlBox.Text = _settings.UploadUrl ?? string.Empty; ManualUsernameBox.Text = _settings.Username ?? string.Empty; ManualPasswordBox.Text = string.Empty; if (!string.IsNullOrWhiteSpace(_settings.UploadUrl)) { StatusText.Text = "Upload bereit."; PickFolderButton.IsEnabled = true; TestUploadButton.IsEnabled = true; StartUploadPipelineIfReady(); } else { PickFolderButton.IsEnabled = false; TestUploadButton.IsEnabled = false; } UpdateCountersText(); UpdateFolderHealth(); UpdateDiagnostics(); RefreshProfiles(); UpdatePresetButtons(); } private void OnWindowOpened(object? sender, EventArgs e) { ApplyWindowSize(); } private void OnWindowClosing(object? sender, WindowClosingEventArgs e) { _settings.WindowWidth = Width; _settings.WindowHeight = Height; _settingsStore.Save(_settings); } private void ApplyWindowSize() { if (_settings.WindowWidth > 0) { Width = Math.Max(MinWidth, _settings.WindowWidth); } if (_settings.WindowHeight > 0) { Height = Math.Max(MinHeight, _settings.WindowHeight); } } private void StartUploadPipelineIfReady() { if (string.IsNullOrWhiteSpace(_settings.UploadUrl) || string.IsNullOrWhiteSpace(_settings.WatchFolder)) { return; } ResetCounters(); _uploadService.Start(_settings, OnQueued, OnUploading, OnSuccess, OnFailure); StartWatcher(_settings.WatchFolder); RestorePendingUploads(); } private void StartWatcher(string folder) { _watcher?.Dispose(); _watcher = new FileSystemWatcher(folder) { IncludeSubdirectories = false, EnableRaisingEvents = true, }; _watcher.Created += OnFileChanged; _watcher.Changed += OnFileChanged; _watcher.Renamed += OnFileRenamed; } private void OnFileChanged(object sender, FileSystemEventArgs e) { if (!IsSupportedImage(e.FullPath)) { return; } if (ShouldSkipUpload(e.FullPath)) { return; } RecordLastSeen(e.FullPath); _uploadService.Enqueue(e.FullPath, OnQueued); } private void OnFileRenamed(object sender, RenamedEventArgs e) { if (!IsSupportedImage(e.FullPath)) { return; } if (ShouldSkipUpload(e.FullPath)) { return; } RecordLastSeen(e.FullPath); _uploadService.Enqueue(e.FullPath, OnQueued); } private bool IsSupportedImage(string path) { var extension = Path.GetExtension(path)?.ToLowerInvariant(); return extension is ".jpg" or ".jpeg" or ".png" or ".webp"; } private void UpdateStatus(string message) { Dispatcher.UIThread.Post(() => StatusText.Text = message); } private void OnQueued(string path) { Interlocked.Increment(ref _queuedCount); UpdateUpload(path, UploadStatus.Queued); AddPendingUpload(path); UpdateStatusIfAllowed($"Wartet: {Path.GetFileName(path)}", false); AppendLog($"Wartet: {Path.GetFileName(path)}"); UpdateCountersText(); } private void OnUploading(string path) { Interlocked.Decrement(ref _queuedCount); Interlocked.Increment(ref _uploadingCount); UpdateUpload(path, UploadStatus.Uploading); UpdateStatusIfAllowed($"Upload läuft: {Path.GetFileName(path)}", false); UpdateCountersText(); } private void OnSuccess(string path) { _failedPaths.Remove(path); Interlocked.Decrement(ref _uploadingCount); _lastSuccessAt = DateTimeOffset.Now; UpdateUpload(path, UploadStatus.Success); RemovePendingUpload(path); MarkUploaded(path); UpdateStatusIfAllowed($"Hochgeladen: {Path.GetFileName(path)}", false); AppendLog($"Hochgeladen: {Path.GetFileName(path)}"); UpdateCountersText(); UpdateLiveStatus(); } private void OnFailure(string path, string message) { _failedPaths.Add(path); Interlocked.Decrement(ref _uploadingCount); Interlocked.Increment(ref _failedCount); UpdateUpload(path, UploadStatus.Failed); RemovePendingUpload(path); UpdateStatusIfAllowed($"Upload fehlgeschlagen: {Path.GetFileName(path)}", true); SetLastError($"{Path.GetFileName(path)} – {message}"); AppendLog($"Fehlgeschlagen: {Path.GetFileName(path)} – {message}"); UpdateRetryButton(); UpdateCountersText(); } private void UpdateUpload(string path, UploadStatus status) { Dispatcher.UIThread.Post(() => { if (!_uploadsByPath.TryGetValue(path, out var item)) { item = new UploadItem(path); _uploadsByPath[path] = item; RecentUploads.Insert(0, item); } item.Status = status; LastUploadText.Text = status == UploadStatus.Success ? $"Letzter Upload: {item.UpdatedLabel}" : LastUploadText.Text; while (RecentUploads.Count > 3) { var last = RecentUploads[^1]; _uploadsByPath.Remove(last.Path); RecentUploads.RemoveAt(RecentUploads.Count - 1); } UpdateRetryButton(); }); } private void UpdateStatusIfAllowed(string message, bool important) { var quiet = QuietToggle?.IsChecked ?? false; if (quiet && !important) { return; } UpdateStatus(message); } private void UpdateRetryButton() { RetryFailedButton.IsEnabled = _failedPaths.Count > 0; ClearFailedButton.IsEnabled = _failedPaths.Count > 0; } private void RetryFailedButton_Click(object? sender, RoutedEventArgs e) { if (_failedPaths.Count == 0) { return; } var retried = _failedPaths.Count; foreach (var path in _failedPaths.ToList()) { _uploadService.Enqueue(path, OnQueued); } _failedPaths.Clear(); Interlocked.Add(ref _failedCount, -retried); Interlocked.Add(ref _queuedCount, retried); UpdateRetryButton(); UpdateCountersText(); } private void ClearFailedButton_Click(object? sender, RoutedEventArgs e) { if (_failedPaths.Count == 0) { return; } var cleared = _failedPaths.Count; _failedPaths.Clear(); Interlocked.Add(ref _failedCount, -cleared); UpdateRetryButton(); UpdateCountersText(); UpdateStatusIfAllowed("Fehlerliste geleert.", false); } private void RestorePendingUploads() { EnsureSettingsCollections(); if (_settings.PendingUploads.Count == 0) { return; } var pending = _settings.PendingUploads.ToList(); var changed = false; foreach (var path in pending) { if (string.IsNullOrWhiteSpace(path) || !File.Exists(path) || ShouldSkipUpload(path)) { _settings.PendingUploads.Remove(path); changed = true; continue; } _uploadService.Enqueue(path, OnQueued); } if (changed) { _settingsStore.Save(_settings); } } private bool ShouldSkipUpload(string path) { var fileName = Path.GetFileName(path); if (!MatchesIncludePatterns(fileName)) { return true; } if (MatchesExcludePatterns(fileName)) { return true; } var signature = GetUploadSignature(path); if (signature is null) { return true; } return _settings.UploadedFiles.TryGetValue(path, out var recorded) && recorded == signature; } private string? GetUploadSignature(string path) { if (!File.Exists(path)) { return null; } var info = new FileInfo(path); return $"{info.Length}:{info.LastWriteTimeUtc.Ticks}"; } private void AddPendingUpload(string path) { EnsureSettingsCollections(); if (!_settings.PendingUploads.Contains(path)) { _settings.PendingUploads.Add(path); _settingsStore.Save(_settings); } } private void RemovePendingUpload(string path) { EnsureSettingsCollections(); if (_settings.PendingUploads.Remove(path)) { _settingsStore.Save(_settings); } } private void MarkUploaded(string path) { var signature = GetUploadSignature(path); if (signature is null) { return; } EnsureSettingsCollections(); _settings.UploadedFiles[path] = signature; _settingsStore.Save(_settings); } private void SaveAdvancedButton_Click(object? sender, RoutedEventArgs e) { if (!int.TryParse(MaxUploadsBox.Text, out var maxUploads) || maxUploads < 1 || maxUploads > 5) { UpdateStatus("Max. parallele Uploads muss zwischen 1 und 5 liegen."); SetLastError("Ungültige Parallel-Uploads."); return; } var manualUploadUrl = NormalizeUrl(ManualUploadUrlBox.Text); var manualUsername = (ManualUsernameBox.Text ?? string.Empty).Trim(); var manualPassword = (ManualPasswordBox.Text ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(manualUploadUrl)) { UpdateStatus("Bitte eine gültige Upload-URL eingeben."); SetLastError("Ungültige Upload-URL."); return; } _settings.BaseUrl = ExtractBaseUrl(manualUploadUrl); _settings.MaxConcurrentUploads = maxUploads; _settings.UploadDelayMs = ResolveUploadDelay(UploadTempoBox.SelectedIndex); _settings.IncludePatterns = NormalizePatternInput(IncludePatternsBox.Text); _settings.ExcludePatterns = NormalizePatternInput(ExcludePatternsBox.Text); _settings.ResponseFormat = ResolveResponseFormat(ResponseFormatBox.SelectedIndex); _settings.UploadUrl = manualUploadUrl; _settings.Username = string.IsNullOrWhiteSpace(manualUsername) ? null : manualUsername; _settings.Password = string.IsNullOrWhiteSpace(manualPassword) ? null : manualPassword; _settingsStore.Save(_settings); _uploadService.Configure(_userAgent); UpdateDiagnostics(); UpdateFolderHealth(); RestartUploadPipeline(); if (!string.IsNullOrWhiteSpace(_settings.UploadUrl)) { StatusText.Text = "Upload bereit."; PickFolderButton.IsEnabled = true; TestUploadButton.IsEnabled = true; } UpdateStatus("Einstellungen gespeichert."); } private async void TestConnectionButton_Click(object? sender, RoutedEventArgs e) { var targetUrl = ResolveTestUrl(); if (targetUrl is null) { UpdateStatus("Bitte eine gültige Upload-URL speichern."); return; } UpdateStatus("Verbindung wird getestet..."); try { using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(8), }; client.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var request = new HttpRequestMessage(HttpMethod.Post, targetUrl); using var response = await client.SendAsync(request); var status = $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim(); UpdateStatus($"Server erreichbar ({status})."); } catch (TaskCanceledException) { UpdateStatus("Verbindungstest: Zeitüberschreitung."); } catch (HttpRequestException) { UpdateStatus("Verbindungstest: Netzwerkfehler."); } } private async void TestUploadButton_Click(object? sender, RoutedEventArgs e) { if (string.IsNullOrWhiteSpace(_settings.UploadUrl)) { UpdateStatus("Bitte zuerst eine Upload-URL speichern."); return; } try { var tempPath = await CreateSampleUploadAsync(); _uploadService.Enqueue(tempPath, OnQueued); UpdateStatusIfAllowed("Test-Upload hinzugefügt.", false); } catch { UpdateStatus("Test-Upload konnte nicht erstellt werden."); SetLastError("Test-Upload konnte nicht erstellt werden."); } } private async Task CreateSampleUploadAsync() { var uri = new Uri("avares://PhotoboothUploader/Assets/sample-upload.png"); await using var source = AssetLoader.Open(uri); var tempPath = Path.Combine(Path.GetTempPath(), $"stylegallery-test-{DateTimeOffset.Now:yyyyMMddHHmmss}.png"); await using var target = File.Create(tempPath); await source.CopyToAsync(target); return tempPath; } private void ResetCounters() { Interlocked.Exchange(ref _queuedCount, 0); Interlocked.Exchange(ref _uploadingCount, 0); Interlocked.Exchange(ref _failedCount, 0); _failedPaths.Clear(); _lastSuccessAt = null; UpdateCountersText(); UpdateRetryButton(); UpdateLiveStatus(); } private void RestartUploadPipeline() { _uploadService.Stop(); StartUploadPipelineIfReady(); } private void UpdateFolderHealth() { var folder = _settings.WatchFolder; if (string.IsNullOrWhiteSpace(folder)) { FolderHealthText.Text = "Ordner: —"; DiskFreeText.Text = "Freier Speicher: —"; LastSeenText.Text = "Letzte Datei: —"; return; } if (!Directory.Exists(folder)) { FolderHealthText.Text = "Ordner: fehlt"; DiskFreeText.Text = "Freier Speicher: —"; LastSeenText.Text = "Letzte Datei: —"; return; } try { _ = Directory.EnumerateFileSystemEntries(folder).FirstOrDefault(); FolderHealthText.Text = CanWriteToFolder(folder) ? "Ordner: OK (schreibbar)" : "Ordner: OK (nur lesen)"; } catch (UnauthorizedAccessException) { FolderHealthText.Text = "Ordner: Keine Berechtigung"; } catch { FolderHealthText.Text = "Ordner: Fehler"; } DiskFreeText.Text = FormatDiskFree(folder); LastSeenText.Text = FormatLastSeen(); } private void UpdateLiveStatus() { Dispatcher.UIThread.Post(() => { if (_lastSuccessAt is null) { LiveStatusText.Text = "Live: —"; UpdateDiagnostics(); return; } var age = DateTimeOffset.Now - _lastSuccessAt.Value; var isLive = age <= TimeSpan.FromMinutes(5); var label = isLive ? "Live: Ja" : "Live: Nein"; LiveStatusText.Text = $"{label} (letzter Upload {FormatRelativeAge(age)})"; UpdateDiagnostics(); }); } private static string FormatRelativeAge(TimeSpan age) { if (age.TotalMinutes < 1) { return "gerade eben"; } if (age.TotalHours < 1) { return $"vor {Math.Round(age.TotalMinutes)} min"; } if (age.TotalDays < 1) { return $"vor {Math.Round(age.TotalHours)} h"; } return $"vor {Math.Round(age.TotalDays)} d"; } private static string FormatDiskFree(string folder) { var root = Path.GetPathRoot(folder); if (string.IsNullOrWhiteSpace(root)) { return "Freier Speicher: —"; } var drive = new DriveInfo(root); if (!drive.IsReady) { return "Freier Speicher: —"; } var freeGb = drive.AvailableFreeSpace / (1024d * 1024d * 1024d); var label = freeGb < 5 ? "niedrig" : "ok"; return $"Freier Speicher: {freeGb:0.0} GB ({label})"; } private void UpdateCountersText() { Dispatcher.UIThread.Post(() => { var queued = Volatile.Read(ref _queuedCount); var uploading = Volatile.Read(ref _uploadingCount); var failed = Volatile.Read(ref _failedCount); QueueStatusText.Text = $"Warteschlange: {Math.Max(0, queued)} · Läuft: {Math.Max(0, uploading)} · Fehlgeschlagen: {Math.Max(0, failed)}"; }); } private void UpdateDiagnostics() { Dispatcher.UIThread.Post(() => { EventNameText.Text = $"Benutzername: {_settings.Username ?? "—"}"; var baseUrl = _settings.BaseUrl ?? ExtractBaseUrl(_settings.UploadUrl); BaseUrlText.Text = $"Basis-URL: {baseUrl ?? "—"}"; UploadUrlText.Text = $"Upload-URL: {_settings.UploadUrl ?? "—"}"; VersionText.Text = $"App-Version: {GetAppVersion()}"; ConnectExpiryText.Text = $"Antwort-Format: {FormatResponseFormat()}"; LastErrorText.Text = $"Letzter Fehler: {FormatLastError()}"; }); } private void SetLastError(string message) { _settings.LastError = message; _settings.LastErrorAt = DateTimeOffset.Now.ToString("O"); _settingsStore.Save(_settings); UpdateDiagnostics(); } private void ClearLastError() { _settings.LastError = null; _settings.LastErrorAt = null; _settingsStore.Save(_settings); UpdateDiagnostics(); } private string FormatLastError() { if (string.IsNullOrWhiteSpace(_settings.LastError)) { return "—"; } if (DateTimeOffset.TryParse(_settings.LastErrorAt, out var timestamp)) { return $"{timestamp:dd.MM.yyyy HH:mm} – {_settings.LastError}"; } return _settings.LastError; } private static string GetAppVersion() { var version = Assembly.GetExecutingAssembly().GetName().Version; return version is null ? "0.0.0" : version.ToString(); } private void EnsureSettingsCollections() { _settings.PendingUploads ??= new List(); _settings.UploadedFiles ??= new Dictionary(StringComparer.OrdinalIgnoreCase); _settings.Profiles ??= new List(); } private void RecordLastSeen(string path) { _settings.LastSeenFile = Path.GetFileName(path); _settings.LastSeenAt = DateTimeOffset.Now.ToString("O"); _settingsStore.Save(_settings); UpdateFolderHealth(); } private bool CanWriteToFolder(string folder) { try { var testFile = Path.Combine(folder, $".stylegallery-write-test-{Guid.NewGuid():N}.tmp"); File.WriteAllText(testFile, "ok"); File.Delete(testFile); return true; } catch { return false; } } private string FormatLastSeen() { if (string.IsNullOrWhiteSpace(_settings.LastSeenAt) || string.IsNullOrWhiteSpace(_settings.LastSeenFile)) { return "Letzte Datei: —"; } if (DateTimeOffset.TryParse(_settings.LastSeenAt, out var timestamp)) { return $"Letzte Datei: {_settings.LastSeenFile} ({timestamp:HH:mm})"; } return $"Letzte Datei: {_settings.LastSeenFile}"; } private string? ResolveTestUrl() { var manual = NormalizeUrl(ManualUploadUrlBox.Text); if (!string.IsNullOrWhiteSpace(manual)) { return manual; } return _settings.UploadUrl; } private static string? NormalizeUrl(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } var trimmed = value.Trim(); if (Uri.TryCreate(trimmed, UriKind.Absolute, out _)) { return trimmed; } return null; } private static string? ExtractBaseUrl(string? url) { if (string.IsNullOrWhiteSpace(url)) { return null; } if (!Uri.TryCreate(url.Trim(), UriKind.Absolute, out var absolute)) { return null; } return absolute.GetLeftPart(UriPartial.Authority); } private string FormatResponseFormat() { if (string.Equals(_settings.ResponseFormat, "json", StringComparison.OrdinalIgnoreCase)) { return "JSON"; } if (string.Equals(_settings.ResponseFormat, "xml", StringComparison.OrdinalIgnoreCase)) { return "XML"; } return "Auto"; } private static int ResolveUploadDelay(int selectedIndex) { return selectedIndex switch { 0 => 0, 2 => 1500, _ => 500, }; } private static int ResolveUploadTempoIndex(int delay) { if (delay <= 0) { return 0; } if (delay >= 1500) { return 2; } return 1; } private static string? ResolveResponseFormat(int selectedIndex) { return selectedIndex switch { 1 => "json", 2 => "xml", _ => null, }; } private static int ResolveResponseFormatIndex(string? format) { if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { return 1; } if (string.Equals(format, "xml", StringComparison.OrdinalIgnoreCase)) { return 2; } return 0; } private static string NormalizePatternInput(string? input) { if (string.IsNullOrWhiteSpace(input)) { return string.Empty; } var parts = SplitPatterns(input); return string.Join(';', parts); } private bool MatchesIncludePatterns(string fileName) { var patterns = SplitPatterns(_settings.IncludePatterns); if (patterns.Count == 0) { return true; } return patterns.Any(pattern => IsPatternMatch(fileName, pattern)); } private bool MatchesExcludePatterns(string fileName) { var patterns = SplitPatterns(_settings.ExcludePatterns); if (patterns.Count == 0) { return false; } return patterns.Any(pattern => IsPatternMatch(fileName, pattern)); } private static List SplitPatterns(string? input) { if (string.IsNullOrWhiteSpace(input)) { return new List(); } return input .Split(new[] { ';', ',', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(pattern => pattern.Length > 0) .ToList(); } private static bool IsPatternMatch(string fileName, string pattern) { var span = fileName.AsSpan(); var token = pattern.AsSpan(); return IsPatternMatch(span, token); } private static bool IsPatternMatch(ReadOnlySpan text, ReadOnlySpan pattern) { var textIndex = 0; var patternIndex = 0; var starIndex = -1; var matchIndex = 0; while (textIndex < text.Length) { if (patternIndex < pattern.Length && (pattern[patternIndex] == '?' || char.ToLowerInvariant(pattern[patternIndex]) == char.ToLowerInvariant(text[textIndex]))) { textIndex++; patternIndex++; continue; } if (patternIndex < pattern.Length && pattern[patternIndex] == '*') { starIndex = patternIndex; matchIndex = textIndex; patternIndex++; continue; } if (starIndex != -1) { patternIndex = starIndex + 1; matchIndex++; textIndex = matchIndex; continue; } return false; } while (patternIndex < pattern.Length && pattern[patternIndex] == '*') { patternIndex++; } return patternIndex == pattern.Length; } private void RefreshProfiles() { ProfilesBox.ItemsSource = _settings.Profiles.Select(profile => profile.DisplayName).ToList(); } private void LoadProfileButton_Click(object? sender, RoutedEventArgs e) { var index = ProfilesBox.SelectedIndex; if (index < 0 || index >= _settings.Profiles.Count) { UpdateStatus("Bitte zuerst ein Profil auswaehlen."); return; } var profile = _settings.Profiles[index]; _settings.BaseUrl = ExtractBaseUrl(profile.UploadUrl) ?? ExtractBaseUrl(profile.BaseUrl) ?? _settings.BaseUrl; _settings.UploadUrl = profile.UploadUrl; _settings.Username = profile.Username; _settings.Password = profile.Password; _settings.ResponseFormat = profile.ResponseFormat; _settings.WatchFolder = profile.WatchFolder; _settings.IncludePatterns = profile.IncludePatterns; _settings.ExcludePatterns = profile.ExcludePatterns; _settings.MaxConcurrentUploads = profile.MaxConcurrentUploads > 0 ? profile.MaxConcurrentUploads : _settings.MaxConcurrentUploads; _settings.UploadDelayMs = profile.UploadDelayMs; _settingsStore.Save(_settings); ApplySettings(); UpdateStatus("Profil geladen."); } private void SaveProfileButton_Click(object? sender, RoutedEventArgs e) { var label = _settings.Username ?? ExtractBaseUrl(_settings.UploadUrl) ?? "Neues Profil"; var profile = new PhotoboothProfile { Label = label, EventName = _settings.EventName, BaseUrl = _settings.BaseUrl, UploadUrl = _settings.UploadUrl, Username = _settings.Username, Password = _settings.Password, ResponseFormat = _settings.ResponseFormat, WatchFolder = _settings.WatchFolder, IncludePatterns = _settings.IncludePatterns, ExcludePatterns = _settings.ExcludePatterns, MaxConcurrentUploads = _settings.MaxConcurrentUploads, UploadDelayMs = _settings.UploadDelayMs, }; _settings.Profiles.RemoveAll(existing => string.Equals(existing.DisplayName, profile.DisplayName, StringComparison.OrdinalIgnoreCase)); _settings.Profiles.Insert(0, profile); _settingsStore.Save(_settings); RefreshProfiles(); ProfilesBox.SelectedIndex = 0; UpdateStatus("Profil gespeichert."); } private async void LogCopyButton_Click(object? sender, RoutedEventArgs e) { var content = ReadLogForCopy(); if (string.IsNullOrWhiteSpace(content)) { UpdateStatus("Log ist leer."); return; } var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; if (clipboard is null) { UpdateStatus("Zwischenablage nicht verfuegbar."); return; } await clipboard.SetTextAsync(content); UpdateStatus("Log kopiert."); } private void AppendLog(string message) { var line = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss} {message}"; _logBuffer.Add(line); while (_logBuffer.Count > 200) { _logBuffer.RemoveAt(0); } try { File.AppendAllLines(_settingsStore.LogPath, new[] { line }); } catch { // ignore file errors } } private string ReadLogForCopy() { try { if (File.Exists(_settingsStore.LogPath)) { var lines = File.ReadAllLines(_settingsStore.LogPath); return string.Join(Environment.NewLine, lines.TakeLast(200)); } } catch { // ignore } return _logBuffer.Count > 0 ? string.Join(Environment.NewLine, _logBuffer) : string.Empty; } private void UpdatePresetButtons() { DslrBoothPresetButton.IsVisible = OperatingSystem.IsWindows(); SparkboothPresetButton.IsVisible = OperatingSystem.IsWindows() || OperatingSystem.IsMacOS(); } private void ApplyPresetFolder(string? folder) { if (string.IsNullOrWhiteSpace(folder)) { UpdateStatus("Preset-Ordner nicht verfügbar."); return; } try { Directory.CreateDirectory(folder); } catch { UpdateStatus("Preset-Ordner konnte nicht erstellt werden."); return; } _settings.WatchFolder = folder; _settingsStore.Save(_settings); FolderText.Text = folder; UpdateFolderHealth(); StartUploadPipelineIfReady(); } private static string? GetDslrBoothFolder() { return OperatingSystem.IsWindows() ? @"C:\dslrBooth" : null; } private static string? GetSparkboothFolder() { var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); if (string.IsNullOrWhiteSpace(documents)) { return null; } return Path.Combine(documents, "sparkbooth"); } }