Files
fotospiel-app/clients/photobooth-uploader/PhotoboothUploader/MainWindow.axaml.cs
Codex Agent c8d1ac7971
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Improve uploader client connection and diagnostics
2026-01-12 20:40:40 +01:00

443 lines
13 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.Tasks;
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 DefaultBaseUrl = "https://fotospiel.app";
private PhotoboothConnectClient _client;
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;
public ObservableCollection<UploadItem> RecentUploads { get; } = new();
public MainWindow()
{
InitializeComponent();
_settings = _settingsStore.Load();
_settings.BaseUrl = NormalizeBaseUrl(_settings.BaseUrl) ?? DefaultBaseUrl;
_userAgent = $"FotospielPhotoboothUploader/{GetAppVersion()}";
_client = new PhotoboothConnectClient(_settings.BaseUrl, _userAgent);
_uploadService.Configure(_userAgent);
_settingsStore.Save(_settings);
DataContext = this;
ApplySettings();
}
private async void ConnectButton_Click(object? sender, RoutedEventArgs e)
{
var normalizedBaseUrl = NormalizeBaseUrl(_settings.BaseUrl);
if (normalizedBaseUrl is null)
{
UpdateStatus("Ungültige Basis-URL. Bitte Support kontaktieren.");
SetLastError("Ungültige Basis-URL.");
return;
}
if (!string.Equals(normalizedBaseUrl, _settings.BaseUrl, StringComparison.OrdinalIgnoreCase))
{
_settings.BaseUrl = normalizedBaseUrl;
_client = new PhotoboothConnectClient(_settings.BaseUrl, _userAgent);
}
var code = (CodeBox.Text ?? string.Empty).Trim();
if (code.Length != 6 || code.Any(ch => ch is < '0' or > '9'))
{
StatusText.Text = "Bitte einen gültigen 6-stelligen Code eingeben.";
return;
}
ConnectButton.IsEnabled = false;
StatusText.Text = "Verbinde...";
var response = await _client.RedeemAsync(code);
if (response.Data is null)
{
StatusText.Text = response.Message ?? "Verbindung fehlgeschlagen.";
SetLastError(response.Message ?? "Verbindung fehlgeschlagen.");
ConnectButton.IsEnabled = true;
return;
}
ClearLastError();
_settings.UploadUrl = ResolveUploadUrl(response.Data.UploadUrl);
_settings.Username = response.Data.Username;
_settings.Password = response.Data.Password;
_settings.ResponseFormat = response.Data.ResponseFormat;
_settings.EventName = response.Data.EventName;
_settingsStore.Save(_settings);
StatusText.Text = "Verbunden. Upload bereit.";
PickFolderButton.IsEnabled = true;
TestUploadButton.IsEnabled = true;
UpdateDiagnostics();
StartUploadPipelineIfReady();
ConnectButton.IsEnabled = true;
}
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;
StartUploadPipelineIfReady();
}
private void ApplySettings()
{
if (!string.IsNullOrWhiteSpace(_settings.WatchFolder))
{
FolderText.Text = _settings.WatchFolder;
}
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
{
StatusText.Text = "Verbunden. Upload bereit.";
PickFolderButton.IsEnabled = true;
TestUploadButton.IsEnabled = true;
StartUploadPipelineIfReady();
}
UpdateCountersText();
UpdateDiagnostics();
UpdateSteps();
}
private void StartUploadPipelineIfReady()
{
if (string.IsNullOrWhiteSpace(_settings.UploadUrl) || string.IsNullOrWhiteSpace(_settings.WatchFolder))
{
UpdateSteps();
return;
}
_uploadService.Start(_settings, OnQueued, OnUploading, OnSuccess, OnFailure);
StartWatcher(_settings.WatchFolder);
UpdateSteps();
}
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;
}
_uploadService.Enqueue(e.FullPath, OnQueued);
}
private void OnFileRenamed(object sender, RenamedEventArgs e)
{
if (!IsSupportedImage(e.FullPath))
{
return;
}
_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)
{
_queuedCount = Math.Max(0, _queuedCount + 1);
UpdateUpload(path, UploadStatus.Queued);
UpdateStatusIfAllowed($"Wartet: {Path.GetFileName(path)}", false);
UpdateCountersText();
}
private void OnUploading(string path)
{
_queuedCount = Math.Max(0, _queuedCount - 1);
_uploadingCount = Math.Max(0, _uploadingCount + 1);
UpdateUpload(path, UploadStatus.Uploading);
UpdateStatusIfAllowed($"Upload läuft: {Path.GetFileName(path)}", false);
UpdateCountersText();
}
private void OnSuccess(string path)
{
_failedPaths.Remove(path);
_uploadingCount = Math.Max(0, _uploadingCount - 1);
UpdateUpload(path, UploadStatus.Success);
UpdateStatusIfAllowed($"Hochgeladen: {Path.GetFileName(path)}", false);
UpdateCountersText();
}
private void OnFailure(string path)
{
_failedPaths.Add(path);
_uploadingCount = Math.Max(0, _uploadingCount - 1);
_failedCount = Math.Max(0, _failedCount + 1);
UpdateUpload(path, UploadStatus.Failed);
UpdateStatusIfAllowed($"Upload fehlgeschlagen: {Path.GetFileName(path)}", true);
SetLastError($"Upload fehlgeschlagen: {Path.GetFileName(path)}");
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;
}
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();
_failedCount = Math.Max(0, _failedCount - retried);
_queuedCount = Math.Max(0, _queuedCount + retried);
UpdateRetryButton();
UpdateCountersText();
}
private void UpdateSteps()
{
var hasCode = !string.IsNullOrWhiteSpace(_settings.UploadUrl);
var hasFolder = !string.IsNullOrWhiteSpace(_settings.WatchFolder);
var ready = hasCode && hasFolder;
StepCodeText.Text = hasCode ? "1. Code eingeben ✓" : "1. Code eingeben";
StepFolderText.Text = hasFolder ? "2. Upload-Ordner wählen ✓" : "2. Upload-Ordner wählen";
StepReadyText.Text = ready ? "3. Upload läuft ✓" : "3. Upload läuft";
}
private async void TestUploadButton_Click(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(_settings.UploadUrl))
{
UpdateStatus("Bitte zuerst verbinden.");
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(), $"fotospiel-test-{DateTimeOffset.Now:yyyyMMddHHmmss}.png");
await using var target = File.Create(tempPath);
await source.CopyToAsync(target);
return tempPath;
}
private string? ResolveUploadUrl(string? uploadUrl)
{
if (string.IsNullOrWhiteSpace(uploadUrl))
{
return uploadUrl;
}
if (Uri.TryCreate(uploadUrl, UriKind.Absolute, out _))
{
return uploadUrl;
}
var baseUri = new Uri(_settings.BaseUrl ?? DefaultBaseUrl, UriKind.Absolute);
return new Uri(baseUri, uploadUrl).ToString();
}
private static string? NormalizeBaseUrl(string? baseUrl)
{
if (string.IsNullOrWhiteSpace(baseUrl))
{
return null;
}
var trimmed = baseUrl.Trim();
if (Uri.TryCreate(trimmed, UriKind.Absolute, out var absolute))
{
return absolute.GetLeftPart(UriPartial.Authority);
}
if (Uri.TryCreate($"https://{trimmed}", UriKind.Absolute, out absolute))
{
return absolute.GetLeftPart(UriPartial.Authority);
}
return null;
}
private void UpdateCountersText()
{
Dispatcher.UIThread.Post(() =>
{
QueueStatusText.Text = $"Warteschlange: {_queuedCount} · Läuft: {_uploadingCount} · Fehlgeschlagen: {_failedCount}";
});
}
private void UpdateDiagnostics()
{
Dispatcher.UIThread.Post(() =>
{
EventNameText.Text = $"Event: {_settings.EventName ?? ""}";
BaseUrlText.Text = $"Basis-URL: {_settings.BaseUrl ?? ""}";
VersionText.Text = $"App-Version: {GetAppVersion()}";
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();
}
}