Files
fotospiel-app/clients/photobooth-uploader/PhotoboothUploader/MainWindow.axaml.cs
Codex Agent 53094b8d36
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Add filters, throttling, and connection test
2026-01-13 11:15:57 +01:00

1026 lines
29 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 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;
private DateTimeOffset? _lastSuccessAt;
private bool _advancedVisible;
private readonly DispatcherTimer _liveTimer = new();
public ObservableCollection<UploadItem> RecentUploads { get; } = new();
public MainWindow()
{
InitializeComponent();
_settings = _settingsStore.Load();
EnsureSettingsCollections();
_settings.BaseUrl = NormalizeBaseUrl(_settings.BaseUrl) ?? DefaultBaseUrl;
if (_settings.MaxConcurrentUploads <= 0)
{
_settings.MaxConcurrentUploads = 2;
}
_userAgent = $"FotospielPhotoboothUploader/{GetAppVersion()}";
_client = new PhotoboothConnectClient(_settings.BaseUrl, _userAgent);
_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 ConnectButton_Click(object? sender, RoutedEventArgs e)
{
var code = (CodeBox.Text ?? string.Empty).Trim();
if (!IsValidCode(code))
{
StatusText.Text = "Bitte einen gültigen 6-stelligen Code eingeben.";
return;
}
ConnectButton.IsEnabled = false;
ReconnectButton.IsEnabled = false;
StatusText.Text = "Verbinde...";
await RedeemCodeAsync(code);
ConnectButton.IsEnabled = true;
ReconnectButton.IsEnabled = true;
}
private async void ReconnectButton_Click(object? sender, RoutedEventArgs e)
{
var code = (CodeBox.Text ?? string.Empty).Trim();
if (!IsValidCode(code))
{
StatusText.Text = "Bitte einen gültigen 6-stelligen Code eingeben.";
return;
}
ReconnectButton.IsEnabled = false;
StatusText.Text = "Verbindung erneuern...";
await RedeemCodeAsync(code);
ReconnectButton.IsEnabled = true;
}
private async Task RedeemCodeAsync(string code)
{
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 response = await _client.RedeemAsync(code);
if (response.Data is null)
{
StatusText.Text = response.Message ?? "Verbindung fehlgeschlagen.";
SetLastError(response.Message ?? "Verbindung fehlgeschlagen.");
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;
ReconnectButton.IsEnabled = true;
UpdateDiagnostics();
StartUploadPipelineIfReady();
}
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 ApplySettings()
{
if (!string.IsNullOrWhiteSpace(_settings.WatchFolder))
{
FolderText.Text = _settings.WatchFolder;
}
BaseUrlBox.Text = _settings.BaseUrl ?? DefaultBaseUrl;
MaxUploadsBox.Text = _settings.MaxConcurrentUploads.ToString();
UploadTempoBox.SelectedIndex = ResolveUploadTempoIndex(_settings.UploadDelayMs);
IncludePatternsBox.Text = _settings.IncludePatterns ?? string.Empty;
ExcludePatternsBox.Text = _settings.ExcludePatterns ?? string.Empty;
ManualUploadUrlBox.Text = _settings.UploadUrl ?? string.Empty;
ManualUsernameBox.Text = _settings.Username ?? string.Empty;
ManualPasswordBox.Text = string.Empty;
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
{
StatusText.Text = "Verbunden. Upload bereit.";
PickFolderButton.IsEnabled = true;
TestUploadButton.IsEnabled = true;
ReconnectButton.IsEnabled = true;
StartUploadPipelineIfReady();
}
UpdateCountersText();
UpdateFolderHealth();
UpdateDiagnostics();
UpdateSteps();
}
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))
{
UpdateSteps();
return;
}
ResetCounters();
_uploadService.Start(_settings, OnQueued, OnUploading, OnSuccess, OnFailure);
StartWatcher(_settings.WatchFolder);
RestorePendingUploads();
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;
}
if (ShouldSkipUpload(e.FullPath))
{
return;
}
_uploadService.Enqueue(e.FullPath, OnQueued);
}
private void OnFileRenamed(object sender, RenamedEventArgs e)
{
if (!IsSupportedImage(e.FullPath))
{
return;
}
if (ShouldSkipUpload(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)
{
Interlocked.Increment(ref _queuedCount);
UpdateUpload(path, UploadStatus.Queued);
AddPendingUpload(path);
UpdateStatusIfAllowed($"Wartet: {Path.GetFileName(path)}", false);
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);
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}");
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();
Interlocked.Add(ref _failedCount, -retried);
Interlocked.Add(ref _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 void CodeBox_TextChanged(object? sender, TextChangedEventArgs e)
{
var code = (CodeBox.Text ?? string.Empty).Trim();
var enabled = IsValidCode(code);
ReconnectButton.IsEnabled = enabled;
}
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 TitleText_PointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.ClickCount < 2)
{
return;
}
_advancedVisible = !_advancedVisible;
AdvancedPanel.IsVisible = _advancedVisible;
}
private void SaveAdvancedButton_Click(object? sender, RoutedEventArgs e)
{
var normalizedBaseUrl = NormalizeBaseUrl(BaseUrlBox.Text);
if (normalizedBaseUrl is null)
{
UpdateStatus("Ungültige Basis-URL.");
SetLastError("Ungültige Basis-URL.");
return;
}
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 = (ManualUploadUrlBox.Text ?? string.Empty).Trim();
var manualUsername = (ManualUsernameBox.Text ?? string.Empty).Trim();
var manualPassword = (ManualPasswordBox.Text ?? string.Empty).Trim();
_settings.BaseUrl = normalizedBaseUrl;
_settings.MaxConcurrentUploads = maxUploads;
_settings.UploadDelayMs = ResolveUploadDelay(UploadTempoBox.SelectedIndex);
_settings.IncludePatterns = NormalizePatternInput(IncludePatternsBox.Text);
_settings.ExcludePatterns = NormalizePatternInput(ExcludePatternsBox.Text);
if (!string.IsNullOrWhiteSpace(manualUploadUrl))
{
_settings.UploadUrl = ResolveUploadUrl(manualUploadUrl);
}
if (!string.IsNullOrWhiteSpace(manualUsername))
{
_settings.Username = manualUsername;
}
if (!string.IsNullOrWhiteSpace(manualPassword))
{
_settings.Password = manualPassword;
}
_settingsStore.Save(_settings);
_client = new PhotoboothConnectClient(_settings.BaseUrl, _userAgent);
_uploadService.Configure(_userAgent);
UpdateDiagnostics();
UpdateFolderHealth();
RestartUploadPipeline();
UpdateSteps();
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
{
StatusText.Text = "Verbunden. Upload bereit.";
PickFolderButton.IsEnabled = true;
TestUploadButton.IsEnabled = true;
ReconnectButton.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 oder Basis-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.Head, 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 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 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: —";
return;
}
if (!Directory.Exists(folder))
{
FolderHealthText.Text = "Ordner: fehlt";
DiskFreeText.Text = "Freier Speicher: —";
return;
}
try
{
_ = Directory.EnumerateFileSystemEntries(folder).FirstOrDefault();
FolderHealthText.Text = "Ordner: OK";
}
catch (UnauthorizedAccessException)
{
FolderHealthText.Text = "Ordner: Keine Berechtigung";
}
catch
{
FolderHealthText.Text = "Ordner: Fehler";
}
DiskFreeText.Text = $"Freier Speicher: {FormatDiskFree(folder)}";
}
private void UpdateLiveStatus()
{
Dispatcher.UIThread.Post(() =>
{
if (_lastSuccessAt is null)
{
LiveStatusText.Text = "Live: —";
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)})";
});
}
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 "—";
}
var drive = new DriveInfo(root);
if (!drive.IsReady)
{
return "—";
}
var freeGb = drive.AvailableFreeSpace / (1024d * 1024d * 1024d);
return $"{freeGb:0.0} GB";
}
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(() =>
{
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 = $"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();
}
private static bool IsValidCode(string code)
{
return code.Length == 6 && code.All(ch => ch is >= '0' and <= '9');
}
private void EnsureSettingsCollections()
{
_settings.PendingUploads ??= new List<string>();
_settings.UploadedFiles ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
private string? ResolveTestUrl()
{
var manual = NormalizeUrl(ManualUploadUrlBox.Text);
if (!string.IsNullOrWhiteSpace(manual))
{
return manual;
}
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
{
return _settings.UploadUrl;
}
var normalizedBase = NormalizeBaseUrl(BaseUrlBox.Text);
return normalizedBase;
}
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 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 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;
}
}