Add filters, throttling, and connection test
This commit is contained in:
@@ -65,6 +65,16 @@
|
|||||||
<TextBox x:Name="BaseUrlBox" Watermark="https://fotospiel.app" />
|
<TextBox x:Name="BaseUrlBox" Watermark="https://fotospiel.app" />
|
||||||
<TextBlock Text="Max. parallele Uploads" />
|
<TextBlock Text="Max. parallele Uploads" />
|
||||||
<TextBox x:Name="MaxUploadsBox" Watermark="2" />
|
<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="Manuelle Zugangsdaten (optional)" FontWeight="SemiBold" Margin="0,8,0,0" />
|
||||||
<TextBlock Text="Diese Felder ueberschreiben den Verbindungscode." Classes="subtitle" TextWrapping="Wrap" />
|
<TextBlock Text="Diese Felder ueberschreiben den Verbindungscode." Classes="subtitle" TextWrapping="Wrap" />
|
||||||
<TextBlock Text="Upload-URL" />
|
<TextBlock Text="Upload-URL" />
|
||||||
@@ -73,6 +83,7 @@
|
|||||||
<TextBox x:Name="ManualUsernameBox" />
|
<TextBox x:Name="ManualUsernameBox" />
|
||||||
<TextBlock Text="Passwort" />
|
<TextBlock Text="Passwort" />
|
||||||
<TextBox x:Name="ManualPasswordBox" PasswordChar="•" />
|
<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" />
|
<Button x:Name="SaveAdvancedButton" Content="Speichern" Click="SaveAdvancedButton_Click" Classes="primary" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
@@ -172,6 +174,9 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
BaseUrlBox.Text = _settings.BaseUrl ?? DefaultBaseUrl;
|
BaseUrlBox.Text = _settings.BaseUrl ?? DefaultBaseUrl;
|
||||||
MaxUploadsBox.Text = _settings.MaxConcurrentUploads.ToString();
|
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;
|
ManualUploadUrlBox.Text = _settings.UploadUrl ?? string.Empty;
|
||||||
ManualUsernameBox.Text = _settings.Username ?? string.Empty;
|
ManualUsernameBox.Text = _settings.Username ?? string.Empty;
|
||||||
ManualPasswordBox.Text = string.Empty;
|
ManualPasswordBox.Text = string.Empty;
|
||||||
@@ -446,6 +451,17 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private bool ShouldSkipUpload(string path)
|
private bool ShouldSkipUpload(string path)
|
||||||
{
|
{
|
||||||
|
var fileName = Path.GetFileName(path);
|
||||||
|
if (!MatchesIncludePatterns(fileName))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MatchesExcludePatterns(fileName))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var signature = GetUploadSignature(path);
|
var signature = GetUploadSignature(path);
|
||||||
if (signature is null)
|
if (signature is null)
|
||||||
{
|
{
|
||||||
@@ -534,6 +550,9 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
_settings.BaseUrl = normalizedBaseUrl;
|
_settings.BaseUrl = normalizedBaseUrl;
|
||||||
_settings.MaxConcurrentUploads = maxUploads;
|
_settings.MaxConcurrentUploads = maxUploads;
|
||||||
|
_settings.UploadDelayMs = ResolveUploadDelay(UploadTempoBox.SelectedIndex);
|
||||||
|
_settings.IncludePatterns = NormalizePatternInput(IncludePatternsBox.Text);
|
||||||
|
_settings.ExcludePatterns = NormalizePatternInput(ExcludePatternsBox.Text);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(manualUploadUrl))
|
if (!string.IsNullOrWhiteSpace(manualUploadUrl))
|
||||||
{
|
{
|
||||||
@@ -569,6 +588,40 @@ public partial class MainWindow : Window
|
|||||||
UpdateStatus("Einstellungen gespeichert.");
|
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)
|
private async void TestUploadButton_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_settings.UploadUrl))
|
if (string.IsNullOrWhiteSpace(_settings.UploadUrl))
|
||||||
@@ -814,4 +867,159 @@ public partial class MainWindow : Window
|
|||||||
_settings.PendingUploads ??= new List<string>();
|
_settings.PendingUploads ??= new List<string>();
|
||||||
_settings.UploadedFiles ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ public sealed class PhotoboothSettings
|
|||||||
public string? Password { get; set; }
|
public string? Password { get; set; }
|
||||||
public string? ResponseFormat { get; set; }
|
public string? ResponseFormat { get; set; }
|
||||||
public string? WatchFolder { 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 List<string> PendingUploads { get; set; } = new();
|
||||||
public Dictionary<string, string> UploadedFiles { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, string> UploadedFiles { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public string? LastError { get; set; }
|
public string? LastError { get; set; }
|
||||||
public string? LastErrorAt { get; set; }
|
public string? LastErrorAt { get; set; }
|
||||||
public int MaxConcurrentUploads { get; set; } = 2;
|
public int MaxConcurrentUploads { get; set; } = 2;
|
||||||
|
public int UploadDelayMs { get; set; } = 500;
|
||||||
public double WindowWidth { get; set; }
|
public double WindowWidth { get; set; }
|
||||||
public double WindowHeight { get; set; }
|
public double WindowHeight { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ public sealed class UploadService
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_pending.TryRemove(path, out _);
|
_pending.TryRemove(path, out _);
|
||||||
|
if (settings.UploadDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(settings.UploadDelayMs, token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user