Polish uploader UI and queue handling
This commit is contained in:
@@ -6,18 +6,58 @@
|
||||
x:Class="PhotoboothUploader.MainWindow"
|
||||
Width="520" Height="360"
|
||||
Title="Fotospiel Photobooth Uploader">
|
||||
<StackPanel Spacing="12" Margin="24" MaxWidth="420">
|
||||
<TextBlock Text="Fotospiel Photobooth Uploader" FontSize="20" FontWeight="SemiBold" />
|
||||
<TextBlock Text="Gib den 6-stelligen Verbindungscode ein." TextWrapping="Wrap" />
|
||||
<TextBox x:Name="CodeBox" MaxLength="6" Watermark="123456" />
|
||||
<Button x:Name="ConnectButton" Content="Verbinden" Click="ConnectButton_Click" />
|
||||
<Grid Margin="24" ColumnDefinitions="*,8,*">
|
||||
<StackPanel Grid.Column="0" Spacing="12" MaxWidth="420">
|
||||
<TextBlock Text="Fotospiel Photobooth Uploader" FontSize="20" FontWeight="SemiBold" />
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Upload-Ordner" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="FolderText" Text="Noch nicht ausgewählt." TextWrapping="Wrap" />
|
||||
<Button x:Name="PickFolderButton" Content="Ordner auswählen" Click="PickFolderButton_Click" IsEnabled="False" />
|
||||
<Border Background="#1F000000" Padding="12" CornerRadius="8">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Schritte" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="StepCodeText" Text="1. Code eingeben" />
|
||||
<TextBlock x:Name="StepFolderText" Text="2. Upload-Ordner wählen" />
|
||||
<TextBlock x:Name="StepReadyText" Text="3. Upload läuft" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Gib den 6-stelligen Verbindungscode ein." TextWrapping="Wrap" />
|
||||
<TextBox x:Name="CodeBox" MaxLength="6" Watermark="123456" />
|
||||
<Button x:Name="ConnectButton" Content="Verbinden" Click="ConnectButton_Click" />
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Upload-Ordner" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="FolderText" Text="Noch nicht ausgewählt." TextWrapping="Wrap" />
|
||||
<Button x:Name="PickFolderButton" Content="Ordner auswählen" Click="PickFolderButton_Click" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
|
||||
<ToggleSwitch x:Name="QuietToggle" Content="Ruhiger Modus (nur Fehler anzeigen)" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="StatusText" Text="Nicht verbunden." TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="12" MaxWidth="380">
|
||||
<Border Background="#1F000000" Padding="12" CornerRadius="8">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Status" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="StatusText" Text="Nicht verbunden." TextWrapping="Wrap" />
|
||||
<TextBlock x:Name="LastUploadText" Text="Letzter Upload: —" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Letzte Uploads" FontWeight="SemiBold" />
|
||||
<ItemsControl x:Name="RecentUploadsList" ItemsSource="{Binding RecentUploads}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#14000000" Padding="8" CornerRadius="6" Margin="0,0,0,6">
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding FileName}" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding StatusLabel}" />
|
||||
<TextBlock Grid.Column="0" Grid.Row="1" Text="{Binding UpdatedLabel}" Opacity="0.7" FontSize="11" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button x:Name="RetryFailedButton" Content="Fehlgeschlagene erneut senden" Click="RetryFailedButton_Click" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
@@ -18,6 +20,10 @@ public partial class MainWindow : Window
|
||||
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);
|
||||
|
||||
public ObservableCollection<UploadItem> RecentUploads { get; } = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -26,6 +32,7 @@ public partial class MainWindow : Window
|
||||
_settings.BaseUrl ??= DefaultBaseUrl;
|
||||
_client = new PhotoboothConnectClient(_settings.BaseUrl);
|
||||
_settingsStore.Save(_settings);
|
||||
DataContext = this;
|
||||
ApplySettings();
|
||||
}
|
||||
|
||||
@@ -100,17 +107,21 @@ public partial class MainWindow : Window
|
||||
PickFolderButton.IsEnabled = true;
|
||||
StartUploadPipelineIfReady();
|
||||
}
|
||||
|
||||
UpdateSteps();
|
||||
}
|
||||
|
||||
private void StartUploadPipelineIfReady()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_settings.UploadUrl) || string.IsNullOrWhiteSpace(_settings.WatchFolder))
|
||||
{
|
||||
UpdateSteps();
|
||||
return;
|
||||
}
|
||||
|
||||
_uploadService.Start(_settings, UpdateStatus);
|
||||
_uploadService.Start(_settings, OnQueued, OnUploading, OnSuccess, OnFailure);
|
||||
StartWatcher(_settings.WatchFolder);
|
||||
UpdateSteps();
|
||||
}
|
||||
|
||||
private void StartWatcher(string folder)
|
||||
@@ -135,7 +146,7 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
_uploadService.Enqueue(e.FullPath);
|
||||
_uploadService.Enqueue(e.FullPath, OnQueued);
|
||||
}
|
||||
|
||||
private void OnFileRenamed(object sender, RenamedEventArgs e)
|
||||
@@ -145,7 +156,7 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
_uploadService.Enqueue(e.FullPath);
|
||||
_uploadService.Enqueue(e.FullPath, OnQueued);
|
||||
}
|
||||
|
||||
private bool IsSupportedImage(string path)
|
||||
@@ -160,6 +171,99 @@ public partial class MainWindow : Window
|
||||
Dispatcher.UIThread.Post(() => StatusText.Text = message);
|
||||
}
|
||||
|
||||
private void OnQueued(string path)
|
||||
{
|
||||
UpdateUpload(path, UploadStatus.Queued);
|
||||
UpdateStatusIfAllowed($"Wartet: {Path.GetFileName(path)}", false);
|
||||
}
|
||||
|
||||
private void OnUploading(string path)
|
||||
{
|
||||
UpdateUpload(path, UploadStatus.Uploading);
|
||||
UpdateStatusIfAllowed($"Upload läuft: {Path.GetFileName(path)}", false);
|
||||
}
|
||||
|
||||
private void OnSuccess(string path)
|
||||
{
|
||||
_failedPaths.Remove(path);
|
||||
UpdateUpload(path, UploadStatus.Success);
|
||||
UpdateStatusIfAllowed($"Hochgeladen: {Path.GetFileName(path)}", false);
|
||||
}
|
||||
|
||||
private void OnFailure(string path)
|
||||
{
|
||||
_failedPaths.Add(path);
|
||||
UpdateUpload(path, UploadStatus.Failed);
|
||||
UpdateStatusIfAllowed($"Upload fehlgeschlagen: {Path.GetFileName(path)}", true);
|
||||
UpdateRetryButton();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
foreach (var path in _failedPaths.ToList())
|
||||
{
|
||||
_uploadService.Enqueue(path, OnQueued);
|
||||
}
|
||||
|
||||
_failedPaths.Clear();
|
||||
UpdateRetryButton();
|
||||
}
|
||||
|
||||
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 string? ResolveUploadUrl(string? uploadUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uploadUrl))
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace PhotoboothUploader.Models;
|
||||
|
||||
public enum UploadStatus
|
||||
{
|
||||
Queued,
|
||||
Uploading,
|
||||
Success,
|
||||
Failed,
|
||||
}
|
||||
|
||||
public sealed class UploadItem : INotifyPropertyChanged
|
||||
{
|
||||
private UploadStatus _status;
|
||||
private DateTimeOffset _updatedAt;
|
||||
|
||||
public UploadItem(string path)
|
||||
{
|
||||
Path = path;
|
||||
FileName = System.IO.Path.GetFileName(path);
|
||||
UpdatedAt = DateTimeOffset.Now;
|
||||
Status = UploadStatus.Queued;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public string FileName { get; }
|
||||
|
||||
public UploadStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (_status != value)
|
||||
{
|
||||
_status = value;
|
||||
UpdatedAt = DateTimeOffset.Now;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(StatusLabel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset UpdatedAt
|
||||
{
|
||||
get => _updatedAt;
|
||||
private set
|
||||
{
|
||||
_updatedAt = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UpdatedLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusLabel => Status switch
|
||||
{
|
||||
UploadStatus.Uploading => "Upload läuft",
|
||||
UploadStatus.Success => "Fertig",
|
||||
UploadStatus.Failed => "Fehlgeschlagen",
|
||||
_ => "Wartet",
|
||||
};
|
||||
|
||||
public string UpdatedLabel => $"{UpdatedAt:HH:mm}";
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -16,12 +16,17 @@ public sealed class UploadService
|
||||
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public void Start(PhotoboothSettings settings, Action<string> setStatus)
|
||||
public void Start(
|
||||
PhotoboothSettings settings,
|
||||
Action<string> onQueued,
|
||||
Action<string> onUploading,
|
||||
Action<string> onSuccess,
|
||||
Action<string> onFailure)
|
||||
{
|
||||
Stop();
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = Task.Run(() => WorkerAsync(settings, setStatus, _cts.Token));
|
||||
_ = Task.Run(() => WorkerAsync(settings, onQueued, onUploading, onSuccess, onFailure, _cts.Token));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
@@ -31,7 +36,7 @@ public sealed class UploadService
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
public void Enqueue(string path)
|
||||
public void Enqueue(string path, Action<string> onQueued)
|
||||
{
|
||||
if (!_pending.TryAdd(path, 0))
|
||||
{
|
||||
@@ -39,9 +44,16 @@ public sealed class UploadService
|
||||
}
|
||||
|
||||
_queue.Writer.TryWrite(path);
|
||||
onQueued(path);
|
||||
}
|
||||
|
||||
private async Task WorkerAsync(PhotoboothSettings settings, Action<string> setStatus, CancellationToken token)
|
||||
private async Task WorkerAsync(
|
||||
PhotoboothSettings settings,
|
||||
Action<string> onQueued,
|
||||
Action<string> onUploading,
|
||||
Action<string> onSuccess,
|
||||
Action<string> onFailure,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(settings.UploadUrl))
|
||||
{
|
||||
@@ -56,9 +68,10 @@ public sealed class UploadService
|
||||
{
|
||||
try
|
||||
{
|
||||
onUploading(path);
|
||||
await WaitForFileReadyAsync(path, token);
|
||||
await UploadAsync(client, settings, path, token);
|
||||
setStatus($"Hochgeladen: {Path.GetFileName(path)}");
|
||||
onSuccess(path);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -66,7 +79,7 @@ public sealed class UploadService
|
||||
}
|
||||
catch
|
||||
{
|
||||
setStatus($"Upload fehlgeschlagen: {Path.GetFileName(path)}");
|
||||
onFailure(path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user