443 lines
13 KiB
C#
443 lines
13 KiB
C#
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();
|
||
}
|
||
}
|