Extend uploader profiles, filters, and diagnostics
This commit is contained in:
@@ -35,6 +35,7 @@ public partial class MainWindow : Window
|
||||
private DateTimeOffset? _lastSuccessAt;
|
||||
private bool _advancedVisible;
|
||||
private readonly DispatcherTimer _liveTimer = new();
|
||||
private readonly List<string> _logBuffer = new();
|
||||
|
||||
public ObservableCollection<UploadItem> RecentUploads { get; } = new();
|
||||
|
||||
@@ -130,12 +131,15 @@ public partial class MainWindow : Window
|
||||
_settings.Password = response.Data.Password;
|
||||
_settings.ResponseFormat = response.Data.ResponseFormat;
|
||||
_settings.EventName = response.Data.EventName;
|
||||
_settings.ConnectExpiresAt = response.Data.ExpiresAt;
|
||||
_settingsStore.Save(_settings);
|
||||
|
||||
StatusText.Text = "Verbunden. Upload bereit.";
|
||||
AppendLog("Verbunden mit Event.");
|
||||
PickFolderButton.IsEnabled = true;
|
||||
TestUploadButton.IsEnabled = true;
|
||||
ReconnectButton.IsEnabled = true;
|
||||
UpdateAdvancedLockState();
|
||||
UpdateDiagnostics();
|
||||
StartUploadPipelineIfReady();
|
||||
}
|
||||
@@ -177,9 +181,11 @@ public partial class MainWindow : Window
|
||||
UploadTempoBox.SelectedIndex = ResolveUploadTempoIndex(_settings.UploadDelayMs);
|
||||
IncludePatternsBox.Text = _settings.IncludePatterns ?? string.Empty;
|
||||
ExcludePatternsBox.Text = _settings.ExcludePatterns ?? string.Empty;
|
||||
ResponseFormatBox.SelectedIndex = ResolveResponseFormatIndex(_settings.ResponseFormat);
|
||||
ManualUploadUrlBox.Text = _settings.UploadUrl ?? string.Empty;
|
||||
ManualUsernameBox.Text = _settings.Username ?? string.Empty;
|
||||
ManualPasswordBox.Text = string.Empty;
|
||||
SettingsUnlockToggle.IsChecked = false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
|
||||
{
|
||||
@@ -194,6 +200,8 @@ public partial class MainWindow : Window
|
||||
UpdateFolderHealth();
|
||||
UpdateDiagnostics();
|
||||
UpdateSteps();
|
||||
RefreshProfiles();
|
||||
UpdateAdvancedLockState();
|
||||
}
|
||||
|
||||
private void OnWindowOpened(object? sender, EventArgs e)
|
||||
@@ -263,6 +271,7 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
RecordLastSeen(e.FullPath);
|
||||
_uploadService.Enqueue(e.FullPath, OnQueued);
|
||||
}
|
||||
|
||||
@@ -278,6 +287,7 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
RecordLastSeen(e.FullPath);
|
||||
_uploadService.Enqueue(e.FullPath, OnQueued);
|
||||
}
|
||||
|
||||
@@ -299,6 +309,7 @@ public partial class MainWindow : Window
|
||||
UpdateUpload(path, UploadStatus.Queued);
|
||||
AddPendingUpload(path);
|
||||
UpdateStatusIfAllowed($"Wartet: {Path.GetFileName(path)}", false);
|
||||
AppendLog($"Wartet: {Path.GetFileName(path)}");
|
||||
UpdateCountersText();
|
||||
}
|
||||
|
||||
@@ -320,6 +331,7 @@ public partial class MainWindow : Window
|
||||
RemovePendingUpload(path);
|
||||
MarkUploaded(path);
|
||||
UpdateStatusIfAllowed($"Hochgeladen: {Path.GetFileName(path)}", false);
|
||||
AppendLog($"Hochgeladen: {Path.GetFileName(path)}");
|
||||
UpdateCountersText();
|
||||
UpdateLiveStatus();
|
||||
}
|
||||
@@ -333,6 +345,7 @@ public partial class MainWindow : Window
|
||||
RemovePendingUpload(path);
|
||||
UpdateStatusIfAllowed($"Upload fehlgeschlagen: {Path.GetFileName(path)}", true);
|
||||
SetLastError($"{Path.GetFileName(path)} – {message}");
|
||||
AppendLog($"Fehlgeschlagen: {Path.GetFileName(path)} – {message}");
|
||||
UpdateRetryButton();
|
||||
UpdateCountersText();
|
||||
}
|
||||
@@ -379,6 +392,7 @@ public partial class MainWindow : Window
|
||||
private void UpdateRetryButton()
|
||||
{
|
||||
RetryFailedButton.IsEnabled = _failedPaths.Count > 0;
|
||||
ClearFailedButton.IsEnabled = _failedPaths.Count > 0;
|
||||
}
|
||||
|
||||
private void RetryFailedButton_Click(object? sender, RoutedEventArgs e)
|
||||
@@ -401,6 +415,21 @@ public partial class MainWindow : Window
|
||||
UpdateCountersText();
|
||||
}
|
||||
|
||||
private void ClearFailedButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_failedPaths.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cleared = _failedPaths.Count;
|
||||
_failedPaths.Clear();
|
||||
Interlocked.Add(ref _failedCount, -cleared);
|
||||
UpdateRetryButton();
|
||||
UpdateCountersText();
|
||||
UpdateStatusIfAllowed("Fehlerliste geleert.", false);
|
||||
}
|
||||
|
||||
private void UpdateSteps()
|
||||
{
|
||||
var hasCode = !string.IsNullOrWhiteSpace(_settings.UploadUrl);
|
||||
@@ -419,6 +448,11 @@ public partial class MainWindow : Window
|
||||
ReconnectButton.IsEnabled = enabled;
|
||||
}
|
||||
|
||||
private void SettingsUnlockToggle_Changed(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateAdvancedLockState();
|
||||
}
|
||||
|
||||
private void RestorePendingUploads()
|
||||
{
|
||||
EnsureSettingsCollections();
|
||||
@@ -553,6 +587,7 @@ public partial class MainWindow : Window
|
||||
_settings.UploadDelayMs = ResolveUploadDelay(UploadTempoBox.SelectedIndex);
|
||||
_settings.IncludePatterns = NormalizePatternInput(IncludePatternsBox.Text);
|
||||
_settings.ExcludePatterns = NormalizePatternInput(ExcludePatternsBox.Text);
|
||||
_settings.ResponseFormat = ResolveResponseFormat(ResponseFormatBox.SelectedIndex);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(manualUploadUrl))
|
||||
{
|
||||
@@ -577,6 +612,7 @@ public partial class MainWindow : Window
|
||||
UpdateFolderHealth();
|
||||
RestartUploadPipeline();
|
||||
UpdateSteps();
|
||||
UpdateAdvancedLockState();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
|
||||
{
|
||||
@@ -678,6 +714,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
FolderHealthText.Text = "Ordner: —";
|
||||
DiskFreeText.Text = "Freier Speicher: —";
|
||||
LastSeenText.Text = "Letzte Datei: —";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -685,13 +722,14 @@ public partial class MainWindow : Window
|
||||
{
|
||||
FolderHealthText.Text = "Ordner: fehlt";
|
||||
DiskFreeText.Text = "Freier Speicher: —";
|
||||
LastSeenText.Text = "Letzte Datei: —";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = Directory.EnumerateFileSystemEntries(folder).FirstOrDefault();
|
||||
FolderHealthText.Text = "Ordner: OK";
|
||||
FolderHealthText.Text = CanWriteToFolder(folder) ? "Ordner: OK (schreibbar)" : "Ordner: OK (nur lesen)";
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
@@ -702,7 +740,8 @@ public partial class MainWindow : Window
|
||||
FolderHealthText.Text = "Ordner: Fehler";
|
||||
}
|
||||
|
||||
DiskFreeText.Text = $"Freier Speicher: {FormatDiskFree(folder)}";
|
||||
DiskFreeText.Text = FormatDiskFree(folder);
|
||||
LastSeenText.Text = FormatLastSeen();
|
||||
}
|
||||
|
||||
private void UpdateLiveStatus()
|
||||
@@ -712,6 +751,7 @@ public partial class MainWindow : Window
|
||||
if (_lastSuccessAt is null)
|
||||
{
|
||||
LiveStatusText.Text = "Live: —";
|
||||
UpdateDiagnostics();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -719,6 +759,7 @@ public partial class MainWindow : Window
|
||||
var isLive = age <= TimeSpan.FromMinutes(5);
|
||||
var label = isLive ? "Live: Ja" : "Live: Nein";
|
||||
LiveStatusText.Text = $"{label} (letzter Upload {FormatRelativeAge(age)})";
|
||||
UpdateDiagnostics();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -747,17 +788,18 @@ public partial class MainWindow : Window
|
||||
var root = Path.GetPathRoot(folder);
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
return "—";
|
||||
return "Freier Speicher: —";
|
||||
}
|
||||
|
||||
var drive = new DriveInfo(root);
|
||||
if (!drive.IsReady)
|
||||
{
|
||||
return "—";
|
||||
return "Freier Speicher: —";
|
||||
}
|
||||
|
||||
var freeGb = drive.AvailableFreeSpace / (1024d * 1024d * 1024d);
|
||||
return $"{freeGb:0.0} GB";
|
||||
var label = freeGb < 5 ? "niedrig" : "ok";
|
||||
return $"Freier Speicher: {freeGb:0.0} GB ({label})";
|
||||
}
|
||||
|
||||
private string? ResolveUploadUrl(string? uploadUrl)
|
||||
@@ -816,6 +858,7 @@ public partial class MainWindow : Window
|
||||
EventNameText.Text = $"Event: {_settings.EventName ?? "—"}";
|
||||
BaseUrlText.Text = $"Basis-URL: {_settings.BaseUrl ?? "—"}";
|
||||
VersionText.Text = $"App-Version: {GetAppVersion()}";
|
||||
ConnectExpiryText.Text = FormatConnectExpiry();
|
||||
LastErrorText.Text = $"Letzter Fehler: {FormatLastError()}";
|
||||
});
|
||||
}
|
||||
@@ -866,6 +909,71 @@ public partial class MainWindow : Window
|
||||
{
|
||||
_settings.PendingUploads ??= new List<string>();
|
||||
_settings.UploadedFiles ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
_settings.Profiles ??= new List<PhotoboothProfile>();
|
||||
}
|
||||
|
||||
private void RecordLastSeen(string path)
|
||||
{
|
||||
_settings.LastSeenFile = Path.GetFileName(path);
|
||||
_settings.LastSeenAt = DateTimeOffset.Now.ToString("O");
|
||||
_settingsStore.Save(_settings);
|
||||
UpdateFolderHealth();
|
||||
}
|
||||
|
||||
private bool CanWriteToFolder(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
var testFile = Path.Combine(folder, $".fotospiel-write-test-{Guid.NewGuid():N}.tmp");
|
||||
File.WriteAllText(testFile, "ok");
|
||||
File.Delete(testFile);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatLastSeen()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_settings.LastSeenAt) || string.IsNullOrWhiteSpace(_settings.LastSeenFile))
|
||||
{
|
||||
return "Letzte Datei: —";
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(_settings.LastSeenAt, out var timestamp))
|
||||
{
|
||||
return $"Letzte Datei: {_settings.LastSeenFile} ({timestamp:HH:mm})";
|
||||
}
|
||||
|
||||
return $"Letzte Datei: {_settings.LastSeenFile}";
|
||||
}
|
||||
|
||||
private string FormatConnectExpiry()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_settings.ConnectExpiresAt))
|
||||
{
|
||||
return "Verbindungscode: —";
|
||||
}
|
||||
|
||||
if (!DateTimeOffset.TryParse(_settings.ConnectExpiresAt, out var expiry))
|
||||
{
|
||||
return "Verbindungscode: —";
|
||||
}
|
||||
|
||||
var remaining = expiry - DateTimeOffset.Now;
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
return "Verbindungscode: abgelaufen";
|
||||
}
|
||||
|
||||
if (remaining.TotalHours >= 1)
|
||||
{
|
||||
return $"Verbindungscode: {Math.Ceiling(remaining.TotalHours)} h gueltig";
|
||||
}
|
||||
|
||||
return $"Verbindungscode: {Math.Ceiling(remaining.TotalMinutes)} min gueltig";
|
||||
}
|
||||
|
||||
private string? ResolveTestUrl()
|
||||
@@ -926,6 +1034,31 @@ public partial class MainWindow : Window
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static string? ResolveResponseFormat(int selectedIndex)
|
||||
{
|
||||
return selectedIndex switch
|
||||
{
|
||||
1 => "json",
|
||||
2 => "xml",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static int ResolveResponseFormatIndex(string? format)
|
||||
{
|
||||
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (string.Equals(format, "xml", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string NormalizePatternInput(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
@@ -1022,4 +1155,138 @@ public partial class MainWindow : Window
|
||||
|
||||
return patternIndex == pattern.Length;
|
||||
}
|
||||
|
||||
private void UpdateAdvancedLockState()
|
||||
{
|
||||
var connected = !string.IsNullOrWhiteSpace(_settings.UploadUrl);
|
||||
var unlocked = SettingsUnlockToggle?.IsChecked ?? false;
|
||||
var enabled = !connected || unlocked;
|
||||
|
||||
BaseUrlBox.IsEnabled = enabled;
|
||||
MaxUploadsBox.IsEnabled = enabled;
|
||||
UploadTempoBox.IsEnabled = enabled;
|
||||
IncludePatternsBox.IsEnabled = enabled;
|
||||
ExcludePatternsBox.IsEnabled = enabled;
|
||||
ResponseFormatBox.IsEnabled = enabled;
|
||||
ManualUploadUrlBox.IsEnabled = enabled;
|
||||
ManualUsernameBox.IsEnabled = enabled;
|
||||
ManualPasswordBox.IsEnabled = enabled;
|
||||
TestConnectionButton.IsEnabled = enabled;
|
||||
SaveAdvancedButton.IsEnabled = enabled;
|
||||
}
|
||||
|
||||
private void RefreshProfiles()
|
||||
{
|
||||
ProfilesBox.ItemsSource = _settings.Profiles.Select(profile => profile.DisplayName).ToList();
|
||||
}
|
||||
|
||||
private void LoadProfileButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var index = ProfilesBox.SelectedIndex;
|
||||
if (index < 0 || index >= _settings.Profiles.Count)
|
||||
{
|
||||
UpdateStatus("Bitte zuerst ein Profil auswaehlen.");
|
||||
return;
|
||||
}
|
||||
|
||||
var profile = _settings.Profiles[index];
|
||||
_settings.BaseUrl = NormalizeBaseUrl(profile.BaseUrl) ?? _settings.BaseUrl;
|
||||
_settings.UploadUrl = profile.UploadUrl;
|
||||
_settings.Username = profile.Username;
|
||||
_settings.Password = profile.Password;
|
||||
_settings.ResponseFormat = profile.ResponseFormat;
|
||||
_settings.WatchFolder = profile.WatchFolder;
|
||||
_settings.IncludePatterns = profile.IncludePatterns;
|
||||
_settings.ExcludePatterns = profile.ExcludePatterns;
|
||||
_settings.MaxConcurrentUploads = profile.MaxConcurrentUploads > 0 ? profile.MaxConcurrentUploads : _settings.MaxConcurrentUploads;
|
||||
_settings.UploadDelayMs = profile.UploadDelayMs;
|
||||
_settingsStore.Save(_settings);
|
||||
|
||||
ApplySettings();
|
||||
UpdateStatus("Profil geladen.");
|
||||
}
|
||||
|
||||
private void SaveProfileButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var label = _settings.EventName ?? "Neues Profil";
|
||||
var profile = new PhotoboothProfile
|
||||
{
|
||||
Label = label,
|
||||
EventName = _settings.EventName,
|
||||
BaseUrl = _settings.BaseUrl,
|
||||
UploadUrl = _settings.UploadUrl,
|
||||
Username = _settings.Username,
|
||||
Password = _settings.Password,
|
||||
ResponseFormat = _settings.ResponseFormat,
|
||||
WatchFolder = _settings.WatchFolder,
|
||||
IncludePatterns = _settings.IncludePatterns,
|
||||
ExcludePatterns = _settings.ExcludePatterns,
|
||||
MaxConcurrentUploads = _settings.MaxConcurrentUploads,
|
||||
UploadDelayMs = _settings.UploadDelayMs,
|
||||
};
|
||||
|
||||
_settings.Profiles.RemoveAll(existing => string.Equals(existing.DisplayName, profile.DisplayName, StringComparison.OrdinalIgnoreCase));
|
||||
_settings.Profiles.Insert(0, profile);
|
||||
_settingsStore.Save(_settings);
|
||||
RefreshProfiles();
|
||||
ProfilesBox.SelectedIndex = 0;
|
||||
UpdateStatus("Profil gespeichert.");
|
||||
}
|
||||
|
||||
private async void LogCopyButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var content = ReadLogForCopy();
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
UpdateStatus("Log ist leer.");
|
||||
return;
|
||||
}
|
||||
|
||||
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
|
||||
if (clipboard is null)
|
||||
{
|
||||
UpdateStatus("Zwischenablage nicht verfuegbar.");
|
||||
return;
|
||||
}
|
||||
|
||||
await clipboard.SetTextAsync(content);
|
||||
UpdateStatus("Log kopiert.");
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
{
|
||||
var line = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss} {message}";
|
||||
_logBuffer.Add(line);
|
||||
while (_logBuffer.Count > 200)
|
||||
{
|
||||
_logBuffer.RemoveAt(0);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.AppendAllLines(_settingsStore.LogPath, new[] { line });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore file errors
|
||||
}
|
||||
}
|
||||
|
||||
private string ReadLogForCopy()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_settingsStore.LogPath))
|
||||
{
|
||||
var lines = File.ReadAllLines(_settingsStore.LogPath);
|
||||
return string.Join(Environment.NewLine, lines.TakeLast(200));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return _logBuffer.Count > 0 ? string.Join(Environment.NewLine, _logBuffer) : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user