Replace sparkbooth upload with photobooth uploader
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,6 +3,8 @@
|
|||||||
/public/build
|
/public/build
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
|
/PhotoboothUploader/bin
|
||||||
|
/PhotoboothUploader/obj
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/storage/framework
|
/storage/framework
|
||||||
/vendor
|
/vendor
|
||||||
|
|||||||
90
PhotoboothUploader/App.axaml
Normal file
90
PhotoboothUploader/App.axaml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="PhotoboothUploader.App"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
<Style>
|
||||||
|
<Style.Resources>
|
||||||
|
<Color x:Key="BrandRose">#FFB6C1</Color>
|
||||||
|
<Color x:Key="BrandRoseStrong">#FF69B4</Color>
|
||||||
|
<Color x:Key="BrandRoseSoft">#FFE5EC</Color>
|
||||||
|
<Color x:Key="BrandGold">#FFD700</Color>
|
||||||
|
<Color x:Key="BrandSky">#87CEEB</Color>
|
||||||
|
<Color x:Key="BrandSkySoft">#E0F5FF</Color>
|
||||||
|
<Color x:Key="BrandNavy">#0F4C75</Color>
|
||||||
|
<Color x:Key="BrandSlate">#1F2937</Color>
|
||||||
|
<Color x:Key="BrandCream">#FFF8F5</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{DynamicResource BrandSlate}" />
|
||||||
|
<SolidColorBrush x:Key="TextMutedBrush" Color="#6B7280" />
|
||||||
|
<SolidColorBrush x:Key="CardBorderBrush" Color="{DynamicResource BrandRoseSoft}" />
|
||||||
|
<SolidColorBrush x:Key="CardBackgroundBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="AccentBackgroundBrush" Color="{DynamicResource BrandSkySoft}" />
|
||||||
|
<SolidColorBrush x:Key="InputBorderBrush" Color="{DynamicResource BrandRoseSoft}" />
|
||||||
|
<SolidColorBrush x:Key="InputBackgroundBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryButtonBrush" Color="{DynamicResource BrandRoseStrong}" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryButtonTextBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="SecondaryButtonBrush" Color="{DynamicResource BrandSky}" />
|
||||||
|
<SolidColorBrush x:Key="SecondaryButtonTextBrush" Color="{DynamicResource BrandNavy}" />
|
||||||
|
|
||||||
|
<LinearGradientBrush x:Key="WindowBackgroundBrush" StartPoint="0,0" EndPoint="1,1">
|
||||||
|
<GradientStop Color="{DynamicResource BrandCream}" Offset="0" />
|
||||||
|
<GradientStop Color="{DynamicResource BrandRoseSoft}" Offset="0.5" />
|
||||||
|
<GradientStop Color="{DynamicResource BrandSkySoft}" Offset="1" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Style.Resources>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Window">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource WindowBackgroundBrush}" />
|
||||||
|
<Setter Property="FontFamily" Value="Inter" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextPrimaryBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBlock.title">
|
||||||
|
<Setter Property="FontSize" Value="20" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBlock.subtitle">
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextMutedBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.card">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource CardBackgroundBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource CardBorderBrush}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.card.accent">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentBackgroundBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource InputBorderBrush}" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource InputBackgroundBrush}" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
<Setter Property="Padding" Value="10,8" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
<Setter Property="Padding" Value="12,8" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.primary">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource PrimaryButtonBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource PrimaryButtonTextBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.secondary">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextBrush}" />
|
||||||
|
</Style>
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
23
PhotoboothUploader/App.axaml.cs
Normal file
23
PhotoboothUploader/App.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
PhotoboothUploader/Assets/app.ico
Normal file
BIN
PhotoboothUploader/Assets/app.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
PhotoboothUploader/Assets/logo.png
Normal file
BIN
PhotoboothUploader/Assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 344 KiB |
BIN
PhotoboothUploader/Assets/sample-upload.png
Normal file
BIN
PhotoboothUploader/Assets/sample-upload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 B |
142
PhotoboothUploader/MainWindow.axaml
Normal file
142
PhotoboothUploader/MainWindow.axaml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="520" d:DesignHeight="360"
|
||||||
|
x:Class="PhotoboothUploader.MainWindow"
|
||||||
|
Width="560" Height="420"
|
||||||
|
MinWidth="520" MinHeight="400"
|
||||||
|
Title="AI Stylegallery - Photobooth Uploader">
|
||||||
|
<Grid Margin="24,32,24,24" RowDefinitions="Auto,*">
|
||||||
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
|
||||||
|
<Border Width="40" Height="40" Classes="card accent" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||||
|
<Image Source="avares://PhotoboothUploader/Assets/logo.png" Width="28" Height="28" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock x:Name="TitleText"
|
||||||
|
Text="AI Stylegallery - Photobooth Uploader"
|
||||||
|
Classes="title" />
|
||||||
|
<TextBlock Text="Sicherer Upload der Fotobox-Fotos ins Event." Classes="subtitle" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,16,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="16" MaxWidth="420">
|
||||||
|
|
||||||
|
<Border Padding="14" Classes="card">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Zugangsdaten" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="Upload-URL" />
|
||||||
|
<TextBox x:Name="ManualUploadUrlBox" Watermark="https://stylegallery.fotospiel.app/api/v1/photobooth/upload" />
|
||||||
|
<TextBlock Text="Benutzername" />
|
||||||
|
<TextBox x:Name="ManualUsernameBox" />
|
||||||
|
<TextBlock Text="Passwort" />
|
||||||
|
<TextBox x:Name="ManualPasswordBox" PasswordChar="•" />
|
||||||
|
<TextBlock Text="Antwort-Format (optional)" />
|
||||||
|
<ComboBox x:Name="ResponseFormatBox" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="Auto" />
|
||||||
|
<ComboBoxItem Content="JSON" />
|
||||||
|
<ComboBoxItem Content="XML" />
|
||||||
|
</ComboBox>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button x:Name="TestConnectionButton" Content="Verbindung testen" Click="TestConnectionButton_Click" Classes="secondary" />
|
||||||
|
<Button x:Name="SaveAdvancedButton" Content="Speichern" Click="SaveAdvancedButton_Click" Classes="primary" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Classes="card">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Upload-Ordner" FontWeight="SemiBold" />
|
||||||
|
<TextBlock x:Name="FolderText" Text="Noch nicht ausgewählt." TextWrapping="Wrap" Classes="subtitle" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button x:Name="DslrBoothPresetButton" Content="DSLrBooth" Click="DslrBoothPresetButton_Click" Classes="secondary" IsVisible="False" />
|
||||||
|
<Button x:Name="SparkboothPresetButton" Content="Photobooth (Sparkbooth)" Click="SparkboothPresetButton_Click" Classes="secondary" IsVisible="False" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button x:Name="PickFolderButton" Content="Ordner auswählen" Click="PickFolderButton_Click" Classes="primary" IsEnabled="False" />
|
||||||
|
<Button x:Name="TestUploadButton" Content="Test-Upload senden" Click="TestUploadButton_Click" Classes="secondary" IsEnabled="False" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Classes="card">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Upload-Optionen" FontWeight="SemiBold" />
|
||||||
|
<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="Profile" />
|
||||||
|
<ComboBox x:Name="ProfilesBox" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button x:Name="LoadProfileButton" Content="Profil laden" Click="LoadProfileButton_Click" Classes="secondary" />
|
||||||
|
<Button x:Name="SaveProfileButton" Content="Profil speichern" Click="SaveProfileButton_Click" Classes="secondary" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ToggleSwitch x:Name="QuietToggle" Content="Ruhiger Modus (nur Fehler anzeigen)" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2" Spacing="16" MaxWidth="380" Margin="0,6,0,0">
|
||||||
|
<Border Padding="14" Classes="card accent">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="Status" FontWeight="SemiBold" />
|
||||||
|
<TextBlock x:Name="StatusText" Text="Nicht verbunden." TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="LastUploadText" Text="Letzter Upload: —" />
|
||||||
|
<TextBlock x:Name="QueueStatusText" Text="Warteschlange: 0 · Läuft: 0 · Fehlgeschlagen: 0" />
|
||||||
|
<TextBlock x:Name="LiveStatusText" Text="Live: —" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Classes="card">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="Details" FontWeight="SemiBold" />
|
||||||
|
<TextBlock x:Name="EventNameText" Text="Benutzername: —" TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="BaseUrlText" Text="Basis-URL: —" TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="UploadUrlText" Text="Upload-URL: —" TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="VersionText" Text="App-Version: —" />
|
||||||
|
<TextBlock x:Name="ConnectExpiryText" Text="Antwort-Format: —" TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="FolderHealthText" Text="Ordner: —" TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="DiskFreeText" Text="Freier Speicher: —" TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="LastSeenText" Text="Letzte Datei: —" TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="LastErrorText" Text="Letzter Fehler: —" TextWrapping="Wrap" />
|
||||||
|
<Button x:Name="LogCopyButton" Content="Log kopieren" Click="LogCopyButton_Click" Classes="secondary" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Classes="card">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Letzte Uploads" FontWeight="SemiBold" />
|
||||||
|
<ItemsControl x:Name="RecentUploadsList" ItemsSource="{Binding RecentUploads}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Background="#14FFFFFF" Padding="10" CornerRadius="8" Margin="0,0,0,8">
|
||||||
|
<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>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button x:Name="RetryFailedButton" Content="Fehlgeschlagene erneut senden" Click="RetryFailedButton_Click" IsEnabled="False" Classes="secondary" />
|
||||||
|
<Button x:Name="ClearFailedButton" Content="Fehlerliste leeren" Click="ClearFailedButton_Click" IsEnabled="False" Classes="secondary" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
1158
PhotoboothUploader/MainWindow.axaml.cs
Normal file
1158
PhotoboothUploader/MainWindow.axaml.cs
Normal file
File diff suppressed because it is too large
Load Diff
33
PhotoboothUploader/Models/PhotoboothConnectResponse.cs
Normal file
33
PhotoboothUploader/Models/PhotoboothConnectResponse.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader.Models;
|
||||||
|
|
||||||
|
public sealed class PhotoboothConnectResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public PhotoboothConnectPayload? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PhotoboothConnectPayload
|
||||||
|
{
|
||||||
|
[JsonPropertyName("event_name")]
|
||||||
|
public string? EventName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("upload_url")]
|
||||||
|
public string? UploadUrl { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("username")]
|
||||||
|
public string? Username { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("password")]
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("expires_at")]
|
||||||
|
public string? ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("response_format")]
|
||||||
|
public string? ResponseFormat { get; set; }
|
||||||
|
}
|
||||||
26
PhotoboothUploader/Models/PhotoboothProfile.cs
Normal file
26
PhotoboothUploader/Models/PhotoboothProfile.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader.Models;
|
||||||
|
|
||||||
|
public sealed class PhotoboothProfile
|
||||||
|
{
|
||||||
|
public string? Label { get; set; }
|
||||||
|
public string? EventName { get; set; }
|
||||||
|
public string? BaseUrl { get; set; }
|
||||||
|
public string? UploadUrl { get; set; }
|
||||||
|
public string? Username { get; set; }
|
||||||
|
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 int MaxConcurrentUploads { get; set; } = 2;
|
||||||
|
public int UploadDelayMs { get; set; } = 500;
|
||||||
|
|
||||||
|
public string DisplayName
|
||||||
|
=> !string.IsNullOrWhiteSpace(Label)
|
||||||
|
? Label
|
||||||
|
: !string.IsNullOrWhiteSpace(EventName)
|
||||||
|
? EventName
|
||||||
|
: UploadUrl ?? BaseUrl ?? "Profil";
|
||||||
|
}
|
||||||
29
PhotoboothUploader/Models/PhotoboothSettings.cs
Normal file
29
PhotoboothUploader/Models/PhotoboothSettings.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader.Models;
|
||||||
|
|
||||||
|
public sealed class PhotoboothSettings
|
||||||
|
{
|
||||||
|
public string? BaseUrl { get; set; }
|
||||||
|
public string? EventName { get; set; }
|
||||||
|
public string? UploadUrl { get; set; }
|
||||||
|
public string? Username { get; set; }
|
||||||
|
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 List<PhotoboothProfile> Profiles { get; set; } = new();
|
||||||
|
public string? ConnectExpiresAt { get; set; }
|
||||||
|
public string? LastSeenFile { get; set; }
|
||||||
|
public string? LastSeenAt { get; set; }
|
||||||
|
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; }
|
||||||
|
}
|
||||||
74
PhotoboothUploader/Models/UploadItem.cs
Normal file
74
PhotoboothUploader/Models/UploadItem.cs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
PhotoboothUploader/PhotoboothUploader.csproj
Normal file
31
PhotoboothUploader/PhotoboothUploader.csproj
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>AIStylegallery.PhotoboothUploader</AssemblyName>
|
||||||
|
<Product>AI Stylegallery Photobooth Uploader</Product>
|
||||||
|
<Company>AI Stylegallery</Company>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<ApplicationIcon>Assets\app.ico</ApplicationIcon>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.3.10" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10" />
|
||||||
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.10">
|
||||||
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Assets\app.ico" />
|
||||||
|
<AvaloniaResource Include="Assets\logo.png" />
|
||||||
|
<AvaloniaResource Include="Assets\sample-upload.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
21
PhotoboothUploader/Program.cs
Normal file
21
PhotoboothUploader/Program.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
|
// yet and stuff might break.
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||||
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.WithInterFont()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
122
PhotoboothUploader/Services/PhotoboothConnectClient.cs
Normal file
122
PhotoboothUploader/Services/PhotoboothConnectClient.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using PhotoboothUploader.Models;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader.Services;
|
||||||
|
|
||||||
|
public sealed class PhotoboothConnectClient
|
||||||
|
{
|
||||||
|
private const int MaxRetries = 2;
|
||||||
|
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public PhotoboothConnectClient(string baseUrl, string userAgent)
|
||||||
|
{
|
||||||
|
_httpClient = new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(baseUrl),
|
||||||
|
Timeout = DefaultTimeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PhotoboothConnectResponse> RedeemAsync(string code, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var request = new { code };
|
||||||
|
|
||||||
|
for (var attempt = 0; attempt <= MaxRetries; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.PostAsJsonAsync("/api/v1/photobooth/connect", request, cancellationToken);
|
||||||
|
var payload = await ReadPayloadAsync(response, cancellationToken);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return payload ?? Fail(response.ReasonPhrase ?? "Verbindung fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.StatusCode is HttpStatusCode.UnprocessableEntity or HttpStatusCode.Conflict or HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
return payload ?? Fail(response.ReasonPhrase ?? "Verbindung fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < MaxRetries && IsTransientStatus(response.StatusCode))
|
||||||
|
{
|
||||||
|
await Task.Delay(GetRetryDelay(attempt), cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload ?? Fail(response.ReasonPhrase ?? "Verbindung fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (attempt < MaxRetries)
|
||||||
|
{
|
||||||
|
await Task.Delay(GetRetryDelay(attempt), cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fail("Zeitüberschreitung bei der Verbindung.");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
if (attempt < MaxRetries)
|
||||||
|
{
|
||||||
|
await Task.Delay(GetRetryDelay(attempt), cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fail("Netzwerkfehler. Bitte Verbindung prüfen.");
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return Fail("Serverantwort konnte nicht gelesen werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fail("Verbindung fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PhotoboothConnectResponse?> ReadPayloadAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (response.Content.Headers.ContentLength == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<PhotoboothConnectResponse>(_jsonOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTransientStatus(HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
return statusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests
|
||||||
|
or HttpStatusCode.BadGateway or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout
|
||||||
|
or HttpStatusCode.InternalServerError;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan GetRetryDelay(int attempt)
|
||||||
|
{
|
||||||
|
return TimeSpan.FromMilliseconds(500 * (attempt + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PhotoboothConnectResponse Fail(string message)
|
||||||
|
{
|
||||||
|
return new PhotoboothConnectResponse
|
||||||
|
{
|
||||||
|
Message = message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
47
PhotoboothUploader/Services/SettingsStore.cs
Normal file
47
PhotoboothUploader/Services/SettingsStore.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using PhotoboothUploader.Models;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader.Services;
|
||||||
|
|
||||||
|
public sealed class SettingsStore
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerOptions _options = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
WriteIndented = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public string SettingsPath { get; }
|
||||||
|
public string LogPath { get; }
|
||||||
|
|
||||||
|
public SettingsStore()
|
||||||
|
{
|
||||||
|
var basePath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"AIStylegallery",
|
||||||
|
"PhotoboothUploader");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(basePath);
|
||||||
|
SettingsPath = Path.Combine(basePath, "settings.json");
|
||||||
|
LogPath = Path.Combine(basePath, "uploader.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PhotoboothSettings Load()
|
||||||
|
{
|
||||||
|
if (!File.Exists(SettingsPath))
|
||||||
|
{
|
||||||
|
return new PhotoboothSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = File.ReadAllText(SettingsPath);
|
||||||
|
return JsonSerializer.Deserialize<PhotoboothSettings>(json, _options) ?? new PhotoboothSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(PhotoboothSettings settings)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(settings, _options);
|
||||||
|
File.WriteAllText(SettingsPath, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
297
PhotoboothUploader/Services/UploadService.cs
Normal file
297
PhotoboothUploader/Services/UploadService.cs
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using PhotoboothUploader.Models;
|
||||||
|
|
||||||
|
namespace PhotoboothUploader.Services;
|
||||||
|
|
||||||
|
public sealed class UploadService
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(20);
|
||||||
|
private static readonly TimeSpan RetryBaseDelay = TimeSpan.FromSeconds(2);
|
||||||
|
private const int MaxRetries = 2;
|
||||||
|
private readonly Channel<string> _queue = Channel.CreateUnbounded<string>();
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private string _userAgent = "AIStylegalleryPhotoboothUploader";
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private readonly List<Task> _workers = new();
|
||||||
|
|
||||||
|
public void Configure(string userAgent)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(userAgent))
|
||||||
|
{
|
||||||
|
_userAgent = userAgent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(
|
||||||
|
PhotoboothSettings settings,
|
||||||
|
Action<string> onQueued,
|
||||||
|
Action<string> onUploading,
|
||||||
|
Action<string> onSuccess,
|
||||||
|
Action<string, string> onFailure)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
var workerCount = GetWorkerCount(settings);
|
||||||
|
for (var i = 0; i < workerCount; i++)
|
||||||
|
{
|
||||||
|
_workers.Add(Task.Run(() => WorkerAsync(settings, onQueued, onUploading, onSuccess, onFailure, _cts.Token)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts = null;
|
||||||
|
_pending.Clear();
|
||||||
|
_workers.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Enqueue(string path, Action<string> onQueued)
|
||||||
|
{
|
||||||
|
if (!_pending.TryAdd(path, 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queue.Writer.TryWrite(path);
|
||||||
|
onQueued(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WorkerAsync(
|
||||||
|
PhotoboothSettings settings,
|
||||||
|
Action<string> onQueued,
|
||||||
|
Action<string> onUploading,
|
||||||
|
Action<string> onSuccess,
|
||||||
|
Action<string, string> onFailure,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(settings.UploadUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var client = new HttpClient();
|
||||||
|
client.Timeout = DefaultTimeout;
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent);
|
||||||
|
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
while (await _queue.Reader.WaitToReadAsync(token))
|
||||||
|
{
|
||||||
|
while (_queue.Reader.TryRead(out var path))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
onUploading(path);
|
||||||
|
var error = await UploadWithRetryAsync(client, settings, path, token);
|
||||||
|
if (error is null)
|
||||||
|
{
|
||||||
|
onSuccess(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
onFailure(path, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pending.TryRemove(path, out _);
|
||||||
|
if (settings.UploadDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(settings.UploadDelayMs, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string?> UploadWithRetryAsync(
|
||||||
|
HttpClient client,
|
||||||
|
PhotoboothSettings settings,
|
||||||
|
string path,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
for (var attempt = 0; attempt <= MaxRetries; attempt++)
|
||||||
|
{
|
||||||
|
var attemptError = await UploadOnceAsync(client, settings, path, token);
|
||||||
|
if (attemptError.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attemptError.Retryable || attempt >= MaxRetries)
|
||||||
|
{
|
||||||
|
return attemptError.Error ?? "Upload fehlgeschlagen.";
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(GetRetryDelay(attempt), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Upload fehlgeschlagen.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<UploadAttempt> UploadOnceAsync(
|
||||||
|
HttpClient client,
|
||||||
|
PhotoboothSettings settings,
|
||||||
|
string path,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
var readyError = await WaitForFileReadyAsync(path, token);
|
||||||
|
if (readyError is not null)
|
||||||
|
{
|
||||||
|
return UploadAttempt.Fail(readyError, retryable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return UploadAttempt.Fail("Datei nicht gefunden.", retryable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.Username))
|
||||||
|
{
|
||||||
|
content.Add(new StringContent(settings.Username), "username");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.Password))
|
||||||
|
{
|
||||||
|
content.Add(new StringContent(settings.Password), "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.ResponseFormat))
|
||||||
|
{
|
||||||
|
content.Add(new StringContent(settings.ResponseFormat), "response_format");
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = File.OpenRead(path);
|
||||||
|
var fileContent = new StreamContent(stream);
|
||||||
|
fileContent.Headers.ContentType = new MediaTypeHeaderValue(ResolveContentType(path));
|
||||||
|
content.Add(fileContent, "media", Path.GetFileName(path));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await client.PostAsync(settings.UploadUrl, content, token);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return UploadAttempt.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = await ReadResponseBodyAsync(response, token);
|
||||||
|
var status = $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
||||||
|
var message = string.IsNullOrWhiteSpace(body) ? status : $"{status} – {body}";
|
||||||
|
return UploadAttempt.Fail(message, IsRetryableStatus(response.StatusCode));
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException) when (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return UploadAttempt.Fail("Zeitüberschreitung beim Upload.", retryable: true);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
return UploadAttempt.Fail("Netzwerkfehler beim Upload.", retryable: true);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return UploadAttempt.Fail("Datei konnte nicht gelesen werden.", retryable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string?> WaitForFileReadyAsync(string path, CancellationToken token)
|
||||||
|
{
|
||||||
|
var lastSize = -1L;
|
||||||
|
|
||||||
|
for (var attempts = 0; attempts < 10; attempts++)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
await Task.Delay(500, token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = new FileInfo(path);
|
||||||
|
var size = info.Length;
|
||||||
|
|
||||||
|
if (size > 0 && size == lastSize)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSize = size;
|
||||||
|
await Task.Delay(700, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Datei ist noch in Bearbeitung.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveContentType(string path)
|
||||||
|
{
|
||||||
|
return Path.GetExtension(path)?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
".png" => "image/png",
|
||||||
|
".webp" => "image/webp",
|
||||||
|
_ => "image/jpeg",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRetryableStatus(System.Net.HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
var numeric = (int)statusCode;
|
||||||
|
return numeric >= 500 || statusCode is System.Net.HttpStatusCode.RequestTimeout or System.Net.HttpStatusCode.TooManyRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan GetRetryDelay(int attempt)
|
||||||
|
{
|
||||||
|
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(100, 350));
|
||||||
|
return TimeSpan.FromMilliseconds(RetryBaseDelay.TotalMilliseconds * Math.Pow(2, attempt)) + jitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string?> ReadResponseBodyAsync(HttpResponseMessage response, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (response.Content is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync(token);
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
body = body.Trim();
|
||||||
|
return body.Length > 200 ? body[..200] + "…" : body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetWorkerCount(PhotoboothSettings settings)
|
||||||
|
{
|
||||||
|
var count = settings.MaxConcurrentUploads;
|
||||||
|
if (count < 1)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 5 ? 5 : count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct UploadAttempt(bool Success, bool Retryable, string? Error)
|
||||||
|
{
|
||||||
|
public static UploadAttempt Ok() => new(true, false, null);
|
||||||
|
|
||||||
|
public static UploadAttempt Fail(string error, bool retryable) => new(false, retryable, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
PhotoboothUploader/app.manifest
Normal file
18
PhotoboothUploader/app.manifest
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<!-- This manifest is used on Windows only.
|
||||||
|
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||||
|
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="AIStylegallery.PhotoboothUploader"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
@@ -13,7 +13,7 @@ use Filament\Tables\Contracts\HasTable;
|
|||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class SparkboothConnections extends Page implements HasTable
|
class PhotoboothConnections extends Page implements HasTable
|
||||||
{
|
{
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|
||||||
@@ -23,16 +23,16 @@ class SparkboothConnections extends Page implements HasTable
|
|||||||
|
|
||||||
protected static ?int $navigationSort = 11;
|
protected static ?int $navigationSort = 11;
|
||||||
|
|
||||||
protected static ?string $title = 'Sparkbooth Verbindungen';
|
protected static ?string $title = 'Photobooth Verbindungen';
|
||||||
|
|
||||||
protected ?string $heading = 'Sparkbooth Verbindungen';
|
protected ?string $heading = 'Photobooth Verbindungen';
|
||||||
|
|
||||||
protected string $view = 'filament.pages.sparkbooth-connections';
|
protected string $view = 'filament.pages.photobooth-connections';
|
||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->heading('Vorhandene Sparkbooth-Verbindungen')
|
->heading('Vorhandene Photobooth-Verbindungen')
|
||||||
->query(
|
->query(
|
||||||
Gallery::query()
|
Gallery::query()
|
||||||
->whereNotNull('upload_token_hash')
|
->whereNotNull('upload_token_hash')
|
||||||
@@ -73,7 +73,7 @@ class SparkboothConnections extends Page implements HasTable
|
|||||||
$data = [
|
$data = [
|
||||||
'gallery' => $record->only(['id', 'name', 'slug', 'images_path']),
|
'gallery' => $record->only(['id', 'name', 'slug', 'images_path']),
|
||||||
'upload_token' => $plainToken,
|
'upload_token' => $plainToken,
|
||||||
'upload_url' => route('api.sparkbooth.upload'),
|
'upload_url' => route('api.photobooth.upload'),
|
||||||
'gallery_url' => route('gallery.show', $record),
|
'gallery_url' => route('gallery.show', $record),
|
||||||
'sparkbooth_username' => $record->sparkbooth_username,
|
'sparkbooth_username' => $record->sparkbooth_username,
|
||||||
'sparkbooth_password' => $record->sparkbooth_password,
|
'sparkbooth_password' => $record->sparkbooth_password,
|
||||||
@@ -86,26 +86,26 @@ class SparkboothConnections extends Page implements HasTable
|
|||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return view('filament.pages.partials.sparkbooth-token', $data);
|
return view('filament.pages.partials.photobooth-token', $data);
|
||||||
}),
|
}),
|
||||||
Action::make('deleteConnection')
|
Action::make('deleteConnection')
|
||||||
->label('Verbindung loeschen')
|
->label('Verbindung loeschen')
|
||||||
->icon('heroicon-o-trash')
|
->icon('heroicon-o-trash')
|
||||||
->color('danger')
|
->color('danger')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->modalHeading('Sparkbooth-Verbindung entfernen')
|
->modalHeading('Photobooth-Verbindung entfernen')
|
||||||
->modalDescription('Die Galerie bleibt erhalten, aber Upload-Token und Zugangsdaten werden geloescht.')
|
->modalDescription('Die Galerie bleibt erhalten, aber Upload-Token und Zugangsdaten werden geloescht.')
|
||||||
->action(function (Gallery $record): void {
|
->action(function (Gallery $record): void {
|
||||||
$record->clearSparkboothConnection();
|
$record->clearSparkboothConnection();
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Sparkbooth-Verbindung entfernt.')
|
->title('Photobooth-Verbindung entfernt.')
|
||||||
->body('Der Upload-Token und die Zugangsdaten wurden geloescht.')
|
->body('Der Upload-Token und die Zugangsdaten wurden geloescht.')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->emptyStateHeading('Keine Sparkbooth-Verbindungen')
|
->emptyStateHeading('Keine Photobooth-Verbindungen')
|
||||||
->emptyStateDescription('Lege eine neue Verbindung an oder aktiviere Uploads fuer eine Galerie.');
|
->emptyStateDescription('Lege eine neue Verbindung an oder aktiviere Uploads fuer eine Galerie.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ use Filament\Schemas\Schema;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class SparkboothSetup extends Page implements HasForms
|
class PhotoboothSetup extends Page implements HasForms
|
||||||
{
|
{
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class SparkboothSetup extends Page implements HasForms
|
|||||||
|
|
||||||
protected static ?int $navigationSort = 10;
|
protected static ?int $navigationSort = 10;
|
||||||
|
|
||||||
protected string $view = 'filament.pages.sparkbooth-setup';
|
protected string $view = 'filament.pages.photobooth-setup';
|
||||||
|
|
||||||
public ?array $data = [];
|
public ?array $data = [];
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class SparkboothSetup extends Page implements HasForms
|
|||||||
|
|
||||||
public function getTitle(): string
|
public function getTitle(): string
|
||||||
{
|
{
|
||||||
return 'Sparkbooth Setup';
|
return 'Photobooth Setup';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function form(Schema $schema): Schema
|
public function form(Schema $schema): Schema
|
||||||
@@ -64,8 +64,8 @@ class SparkboothSetup extends Page implements HasForms
|
|||||||
->label('Uploads aktivieren')
|
->label('Uploads aktivieren')
|
||||||
->default(true),
|
->default(true),
|
||||||
TextInput::make('sparkbooth_username')
|
TextInput::make('sparkbooth_username')
|
||||||
->label('Sparkbooth Benutzername')
|
->label('Photobooth Benutzername')
|
||||||
->helperText('Wird in Sparkbooth unter „Username“ eingetragen. Erlaubt sind Buchstaben, Zahlen sowie ._-')
|
->helperText('Wird im Photobooth Uploader unter „Benutzername“ eingetragen. Erlaubt sind Buchstaben, Zahlen sowie ._-')
|
||||||
->default(fn (): string => 'spark-'.Str::lower(Str::random(6)))
|
->default(fn (): string => 'spark-'.Str::lower(Str::random(6)))
|
||||||
->required()
|
->required()
|
||||||
->maxLength(64)
|
->maxLength(64)
|
||||||
@@ -73,7 +73,7 @@ class SparkboothSetup extends Page implements HasForms
|
|||||||
->unique(table: Gallery::class, column: 'sparkbooth_username'),
|
->unique(table: Gallery::class, column: 'sparkbooth_username'),
|
||||||
Select::make('sparkbooth_response_format')
|
Select::make('sparkbooth_response_format')
|
||||||
->label('Standard-Antwortformat')
|
->label('Standard-Antwortformat')
|
||||||
->helperText('Sparkbooth kann JSON oder XML erwarten. JSON ist empfohlen.')
|
->helperText('Der Photobooth Uploader kann JSON oder XML erwarten. JSON ist empfohlen.')
|
||||||
->options([
|
->options([
|
||||||
'json' => 'JSON',
|
'json' => 'JSON',
|
||||||
'xml' => 'XML',
|
'xml' => 'XML',
|
||||||
@@ -113,7 +113,7 @@ class SparkboothSetup extends Page implements HasForms
|
|||||||
$this->result = [
|
$this->result = [
|
||||||
'gallery' => $gallery->only(['id', 'name', 'slug', 'images_path']),
|
'gallery' => $gallery->only(['id', 'name', 'slug', 'images_path']),
|
||||||
'upload_token' => $plainToken,
|
'upload_token' => $plainToken,
|
||||||
'upload_url' => route('api.sparkbooth.upload'),
|
'upload_url' => route('api.photobooth.upload'),
|
||||||
'gallery_url' => route('gallery.show', $gallery),
|
'gallery_url' => route('gallery.show', $gallery),
|
||||||
'sparkbooth_username' => $sparkboothUsername,
|
'sparkbooth_username' => $sparkboothUsername,
|
||||||
'sparkbooth_password' => $sparkboothPassword,
|
'sparkbooth_password' => $sparkboothPassword,
|
||||||
@@ -79,14 +79,14 @@ class GalleryForm
|
|||||||
'url' => $record ? URL::route('gallery.show', $record) : null,
|
'url' => $record ? URL::route('gallery.show', $record) : null,
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
Tab::make('Sparkbooth')
|
Tab::make('Photobooth')
|
||||||
->schema([
|
->schema([
|
||||||
Section::make('Sparkbooth Upload')
|
Section::make('Photobooth Upload')
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->schema([
|
->schema([
|
||||||
Toggle::make('upload_enabled')
|
Toggle::make('upload_enabled')
|
||||||
->label('Uploads aktivieren')
|
->label('Uploads aktivieren')
|
||||||
->helperText('Steuert, ob Sparkbooth-Uploads erlaubt sind.')
|
->helperText('Steuert, ob Photobooth-Uploads erlaubt sind.')
|
||||||
->default(false),
|
->default(false),
|
||||||
Select::make('sparkbooth_response_format')
|
Select::make('sparkbooth_response_format')
|
||||||
->label('Standard-Antwortformat')
|
->label('Standard-Antwortformat')
|
||||||
@@ -96,25 +96,25 @@ class GalleryForm
|
|||||||
])
|
])
|
||||||
->default('json'),
|
->default('json'),
|
||||||
TextInput::make('sparkbooth_username')
|
TextInput::make('sparkbooth_username')
|
||||||
->label('Sparkbooth Benutzername')
|
->label('Photobooth Benutzername')
|
||||||
->helperText('Wird in Sparkbooth unter „Username“ eingetragen. Erlaubt sind Buchstaben, Zahlen sowie ._-')
|
->helperText('Wird im Photobooth Uploader unter „Benutzername“ eingetragen. Erlaubt sind Buchstaben, Zahlen sowie ._-')
|
||||||
->maxLength(64)
|
->maxLength(64)
|
||||||
->rule('regex:/^[A-Za-z0-9._-]+$/')
|
->rule('regex:/^[A-Za-z0-9._-]+$/')
|
||||||
->unique(table: \App\Models\Gallery::class, column: 'sparkbooth_username', ignoreRecord: true)
|
->unique(table: \App\Models\Gallery::class, column: 'sparkbooth_username', ignoreRecord: true)
|
||||||
->dehydrateStateUsing(fn (?string $state): ?string => $state ? Str::of($state)->lower()->trim()->value() : null),
|
->dehydrateStateUsing(fn (?string $state): ?string => $state ? Str::of($state)->lower()->trim()->value() : null),
|
||||||
TextInput::make('sparkbooth_password')
|
TextInput::make('sparkbooth_password')
|
||||||
->label('Sparkbooth Passwort (neu)')
|
->label('Photobooth Passwort (neu)')
|
||||||
->password()
|
->password()
|
||||||
->revealable()
|
->revealable()
|
||||||
->dehydrated(false)
|
->dehydrated(false)
|
||||||
->afterStateHydrated(fn (callable $set) => $set('sparkbooth_password', null))
|
->afterStateHydrated(fn (callable $set) => $set('sparkbooth_password', null))
|
||||||
->helperText('Leer lassen, um das bestehende Passwort zu behalten.'),
|
->helperText('Leer lassen, um das bestehende Passwort zu behalten.'),
|
||||||
]),
|
]),
|
||||||
View::make('filament.pages.partials.sparkbooth-token')
|
View::make('filament.pages.partials.photobooth-token')
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->visible(fn (?object $record) => (bool) $record?->id)
|
->visible(fn (?object $record) => (bool) $record?->id)
|
||||||
->viewData(fn (?object $record) => [
|
->viewData(fn (?object $record) => [
|
||||||
'upload_url' => URL::route('api.sparkbooth.upload'),
|
'upload_url' => URL::route('api.photobooth.upload'),
|
||||||
'gallery_url' => $record ? URL::route('gallery.show', $record) : null,
|
'gallery_url' => $record ? URL::route('gallery.show', $record) : null,
|
||||||
'sparkbooth_username' => $record?->sparkbooth_username,
|
'sparkbooth_username' => $record?->sparkbooth_username,
|
||||||
'sparkbooth_password' => $record?->sparkbooth_password,
|
'sparkbooth_password' => $record?->sparkbooth_password,
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Benutzername</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Photobooth Benutzername</p>
|
||||||
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $username }}</p>
|
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $username }}</p>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen im Sparkbooth Custom Upload Dialog unter „Username“.</p>
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen im Photobooth Uploader unter „Benutzername“.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Passwort</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Photobooth Passwort</p>
|
||||||
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $password }}</p>
|
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $password }}</p>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen unter „Password“.</p>
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen im Photobooth Uploader unter „Passwort“.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,14 +31,14 @@
|
|||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Antwortformat</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Antwortformat</p>
|
||||||
<p class="mt-1 font-semibold text-gray-900 dark:text-white">{{ $format }}</p>
|
<p class="mt-1 font-semibold text-gray-900 dark:text-white">{{ $format }}</p>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">In Sparkbooth „JSON Response“ oder „XML Response“ passend auswählen.</p>
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Im Photobooth Uploader das passende Antwort-Format auswählen.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Hinweise</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Photobooth Hinweise</p>
|
||||||
<ul class="mt-2 list-disc space-y-1 pl-4 text-xs text-gray-600 dark:text-gray-300">
|
<ul class="mt-2 list-disc space-y-1 pl-4 text-xs text-gray-600 dark:text-gray-300">
|
||||||
<li>Uploader „Custom Upload“ wählen.</li>
|
<li>Uploader starten und Zugangsdaten eintragen.</li>
|
||||||
<li>Username & Password wie oben eintragen.</li>
|
<li>Username & Password wie oben eintragen.</li>
|
||||||
<li>Optional: Name/Email/Message Felder in Sparkbooth setzen.</li>
|
<li>Optional: Name/Email/Message Felder im Uploader setzen.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,14 +15,14 @@
|
|||||||
|
|
||||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Benutzername</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Photobooth Benutzername</p>
|
||||||
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['sparkbooth_username'] }}</p>
|
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['sparkbooth_username'] }}</p>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen in Sparkbooth ➜ Settings ➜ Upload ➜ Custom Upload ➜ Username.</p>
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen im Photobooth Uploader unter „Benutzername“.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Passwort</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Photobooth Passwort</p>
|
||||||
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['sparkbooth_password'] }}</p>
|
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['sparkbooth_password'] }}</p>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen in Sparkbooth unter „Password“.</p>
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen im Photobooth Uploader unter „Passwort“.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Standard-Antwortformat</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Standard-Antwortformat</p>
|
||||||
<p class="mt-1 font-semibold text-gray-900 dark:text-white">{{ strtoupper($result['response_format']) }}</p>
|
<p class="mt-1 font-semibold text-gray-900 dark:text-white">{{ strtoupper($result['response_format']) }}</p>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Muss mit der Auswahl „JSON Response“ oder „XML Response“ in Sparkbooth übereinstimmen.</p>
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Muss mit dem Antwort-Format im Photobooth Uploader übereinstimmen.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -46,11 +46,11 @@
|
|||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Optional: Für bestehende Integrationen nutzbar (Feld „token“).</p>
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Optional: Für bestehende Integrationen nutzbar (Feld „token“).</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Hinweise</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Photobooth Hinweise</p>
|
||||||
<ul class="mt-2 list-disc space-y-1 pl-4 text-xs text-gray-600 dark:text-gray-300">
|
<ul class="mt-2 list-disc space-y-1 pl-4 text-xs text-gray-600 dark:text-gray-300">
|
||||||
<li>Uploader: „Custom Upload“ wählen.</li>
|
<li>Uploader starten und Zugangsdaten eintragen.</li>
|
||||||
<li>URL: {{ $result['upload_url'] }}</li>
|
<li>URL: {{ $result['upload_url'] }}</li>
|
||||||
<li>Username/Password eintragen, optional Message.</li>
|
<li>Benutzername/Passwort eintragen.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
<div class="mt-6 space-y-4">
|
<div class="mt-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Beispiel (Custom Upload)</p>
|
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Photobooth Uploader Beispiel</p>
|
||||||
<pre class="mt-2 rounded-xl border border-gray-200 bg-gray-900 p-4 text-xs text-gray-100 dark:border-white/10">
|
<pre class="mt-2 rounded-xl border border-gray-200 bg-gray-900 p-4 text-xs text-gray-100 dark:border-white/10">
|
||||||
curl -X POST {{ $result['upload_url'] }} \
|
curl -X POST {{ $result['upload_url'] }} \
|
||||||
-F "media=@your-photo.jpg" \
|
-F "media=@your-photo.jpg" \
|
||||||
@@ -27,9 +27,9 @@ Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
|||||||
|
|
||||||
Route::get('/ai-status', [AiStatusController::class, 'checkStatus']);
|
Route::get('/ai-status', [AiStatusController::class, 'checkStatus']);
|
||||||
Route::post('/ai-status/update', [AiStatusController::class, 'checkAndUpdateStatus']);
|
Route::post('/ai-status/update', [AiStatusController::class, 'checkAndUpdateStatus']);
|
||||||
Route::post('/sparkbooth/upload', [SparkboothUploadController::class, 'store'])
|
Route::post('/v1/photobooth/upload', [SparkboothUploadController::class, 'store'])
|
||||||
->middleware('throttle:30,1')
|
->middleware('throttle:30,1')
|
||||||
->name('api.sparkbooth.upload');
|
->name('api.photobooth.upload');
|
||||||
|
|
||||||
Route::post('/admin/navigation-state', [NavigationStateController::class, 'store'])->middleware('auth:sanctum');
|
Route::post('/admin/navigation-state', [NavigationStateController::class, 'store'])->middleware('auth:sanctum');
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Filament;
|
namespace Tests\Feature\Filament;
|
||||||
|
|
||||||
use App\Filament\Pages\SparkboothConnections;
|
use App\Filament\Pages\PhotoboothConnections;
|
||||||
use App\Models\Gallery;
|
use App\Models\Gallery;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -11,7 +11,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class SparkboothConnectionsTest extends TestCase
|
class PhotoboothConnectionsTest extends TestCase
|
||||||
{
|
{
|
||||||
use DatabaseTransactions;
|
use DatabaseTransactions;
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class SparkboothConnectionsTest extends TestCase
|
|||||||
$gallery->setUploadToken('tokentest');
|
$gallery->setUploadToken('tokentest');
|
||||||
$gallery->save();
|
$gallery->save();
|
||||||
|
|
||||||
Livewire::test(SparkboothConnections::class)
|
Livewire::test(PhotoboothConnections::class)
|
||||||
->callTableAction('deleteConnection', $gallery);
|
->callTableAction('deleteConnection', $gallery);
|
||||||
|
|
||||||
$updated = $gallery->fresh();
|
$updated = $gallery->fresh();
|
||||||
@@ -27,7 +27,7 @@ class SparkboothUploadTest extends TestCase
|
|||||||
|
|
||||||
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
|
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
|
||||||
|
|
||||||
$response = $this->postJson(route('api.sparkbooth.upload'), [
|
$response = $this->postJson(route('api.photobooth.upload'), [
|
||||||
'username' => 'spark-user',
|
'username' => 'spark-user',
|
||||||
'password' => 'secret-123',
|
'password' => 'secret-123',
|
||||||
'media' => $file,
|
'media' => $file,
|
||||||
@@ -66,7 +66,7 @@ class SparkboothUploadTest extends TestCase
|
|||||||
|
|
||||||
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
|
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
|
||||||
|
|
||||||
$response = $this->post(route('api.sparkbooth.upload'), [
|
$response = $this->post(route('api.photobooth.upload'), [
|
||||||
'username' => 'spark-user',
|
'username' => 'spark-user',
|
||||||
'password' => 'wrong',
|
'password' => 'wrong',
|
||||||
'media' => $file,
|
'media' => $file,
|
||||||
@@ -96,7 +96,7 @@ class SparkboothUploadTest extends TestCase
|
|||||||
$binary = base64_encode(file_get_contents($fake->getRealPath()));
|
$binary = base64_encode(file_get_contents($fake->getRealPath()));
|
||||||
$dataUri = 'data:image/png;base64,'.$binary;
|
$dataUri = 'data:image/png;base64,'.$binary;
|
||||||
|
|
||||||
$response = $this->postJson(route('api.sparkbooth.upload'), [
|
$response = $this->postJson(route('api.photobooth.upload'), [
|
||||||
'token' => $token,
|
'token' => $token,
|
||||||
'media' => $dataUri,
|
'media' => $dataUri,
|
||||||
'filename' => 'custom.png',
|
'filename' => 'custom.png',
|
||||||
|
|||||||
Reference in New Issue
Block a user