Add filters, throttling, and connection test
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-13 11:15:57 +01:00
parent 0c33c1ddc1
commit 53094b8d36
4 changed files with 226 additions and 0 deletions

View File

@@ -65,6 +65,16 @@
<TextBox x:Name="BaseUrlBox" Watermark="https://fotospiel.app" />
<TextBlock Text="Max. parallele Uploads" />
<TextBox x:Name="MaxUploadsBox" Watermark="2" />
<TextBlock Text="Upload-Tempo" />
<ComboBox x:Name="UploadTempoBox" SelectedIndex="1">
<ComboBoxItem Content="Schnell (ohne Pause)" />
<ComboBoxItem Content="Normal" />
<ComboBoxItem Content="Sanft (schont Netzwerk)" />
</ComboBox>
<TextBlock Text="Nur diese Dateien (optional)" />
<TextBox x:Name="IncludePatternsBox" Watermark="*.jpg;*.jpeg;*.png" />
<TextBlock Text="Dateien ausschliessen (optional)" />
<TextBox x:Name="ExcludePatternsBox" Watermark="*_preview*;*.tmp" />
<TextBlock Text="Manuelle Zugangsdaten (optional)" FontWeight="SemiBold" Margin="0,8,0,0" />
<TextBlock Text="Diese Felder ueberschreiben den Verbindungscode." Classes="subtitle" TextWrapping="Wrap" />
<TextBlock Text="Upload-URL" />
@@ -73,6 +83,7 @@
<TextBox x:Name="ManualUsernameBox" />
<TextBlock Text="Passwort" />
<TextBox x:Name="ManualPasswordBox" PasswordChar="•" />
<Button x:Name="TestConnectionButton" Content="Verbindung testen" Click="TestConnectionButton_Click" Classes="secondary" />
<Button x:Name="SaveAdvancedButton" Content="Speichern" Click="SaveAdvancedButton_Click" Classes="primary" />
</StackPanel>
</Border>

View File

@@ -6,6 +6,8 @@ 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;
@@ -172,6 +174,9 @@ public partial class MainWindow : Window
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;
@@ -446,6 +451,17 @@ public partial class MainWindow : Window
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)
{
@@ -534,6 +550,9 @@ public partial class MainWindow : Window
_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))
{
@@ -569,6 +588,40 @@ public partial class MainWindow : Window
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))
@@ -814,4 +867,159 @@ public partial class MainWindow : Window
_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;
}
}

View File

@@ -12,11 +12,14 @@ public sealed class PhotoboothSettings
public string? Password { get; set; }
public string? ResponseFormat { get; set; }
public string? WatchFolder { get; set; }
public string? IncludePatterns { get; set; }
public string? ExcludePatterns { get; set; }
public List<string> PendingUploads { get; set; } = new();
public Dictionary<string, string> UploadedFiles { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public string? LastError { get; set; }
public string? LastErrorAt { get; set; }
public int MaxConcurrentUploads { get; set; } = 2;
public int UploadDelayMs { get; set; } = 500;
public double WindowWidth { get; set; }
public double WindowHeight { get; set; }
}

View File

@@ -108,6 +108,10 @@ public sealed class UploadService
finally
{
_pending.TryRemove(path, out _);
if (settings.UploadDelayMs > 0)
{
await Task.Delay(settings.UploadDelayMs, token);
}
}
}
}