Files
ai-stylegallery/PhotoboothUploader/MainWindow.axaml.cs

1159 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, UploadItem> _uploadsByPath = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _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<string> _logBuffer = new();
public ObservableCollection<UploadItem> 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<string> 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<string>();
_settings.UploadedFiles ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
_settings.Profiles ??= new List<PhotoboothProfile>();
}
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<string> SplitPatterns(string? input)
{
if (string.IsNullOrWhiteSpace(input))
{
return new List<string>();
}
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<char> text, ReadOnlySpan<char> 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");
}
}