refactor: remove PhotoAnalysisView and SingleItemView (replaced by NewListingView)
This commit is contained in:
@@ -1,682 +0,0 @@
|
|||||||
<UserControl x:Class="EbayListingTool.Views.PhotoAnalysisView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
|
|
||||||
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks">
|
|
||||||
|
|
||||||
<UserControl.Resources>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
|
||||||
Styles
|
|
||||||
================================================================ -->
|
|
||||||
<Style x:Key="SectionCard" TargetType="Border">
|
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
<Setter Property="CornerRadius" Value="6"/>
|
|
||||||
<Setter Property="Padding" Value="14,12"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,10"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource MahApps.Brushes.Gray8}"/>
|
|
||||||
<Setter Property="Background" Value="{DynamicResource MahApps.Brushes.Gray10}"/>
|
|
||||||
</Style>
|
|
||||||
<Style x:Key="SectionHeading" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontSize" Value="10"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
|
||||||
</Style>
|
|
||||||
<Style x:Key="FieldLabel" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="FontSize" Value="12"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource MahApps.Brushes.Gray2}"/>
|
|
||||||
</Style>
|
|
||||||
<Style x:Key="ResultValue" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource MahApps.Brushes.Gray1}"/>
|
|
||||||
</Style>
|
|
||||||
<Style x:Key="AiButton" TargetType="Button"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.Button.Square}">
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Background">
|
|
||||||
<Setter.Value>
|
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
|
||||||
<GradientStop Color="#7C3AED" Offset="0"/>
|
|
||||||
<GradientStop Color="#4F46E5" Offset="1"/>
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter Property="Background">
|
|
||||||
<Setter.Value>
|
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
|
||||||
<GradientStop Color="#8B5CF6" Offset="0"/>
|
|
||||||
<GradientStop Color="#6366F1" Offset="1"/>
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
|
||||||
<Setter Property="Opacity" Value="0.45"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Small icon-only clipboard button -->
|
|
||||||
<Style x:Key="CopyButton" TargetType="Button"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.Button.Square}">
|
|
||||||
<Setter Property="Width" Value="28"/>
|
|
||||||
<Setter Property="Height" Value="28"/>
|
|
||||||
<Setter Property="Padding" Value="4"/>
|
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
<Setter Property="ToolTip" Value="Copy to clipboard"/>
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
|
||||||
Drop zone dashed border animation
|
|
||||||
================================================================ -->
|
|
||||||
<Style x:Key="DashedDropBorder" TargetType="Border">
|
|
||||||
<Setter Property="BorderThickness" Value="2"/>
|
|
||||||
<Setter Property="CornerRadius" Value="10"/>
|
|
||||||
<Setter Property="Background" Value="{DynamicResource MahApps.Brushes.Gray10}"/>
|
|
||||||
<Setter Property="AllowDrop" Value="True"/>
|
|
||||||
<Setter Property="MinHeight" Value="320"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
|
||||||
Results reveal animation
|
|
||||||
================================================================ -->
|
|
||||||
<Storyboard x:Key="ResultsReveal">
|
|
||||||
<DoubleAnimation Storyboard.TargetName="ResultsPanel"
|
|
||||||
Storyboard.TargetProperty="Opacity"
|
|
||||||
From="0" To="1" Duration="0:0:0.25"/>
|
|
||||||
<DoubleAnimation Storyboard.TargetName="ResultsTranslate"
|
|
||||||
Storyboard.TargetProperty="Y"
|
|
||||||
From="20" To="0" Duration="0:0:0.25">
|
|
||||||
<DoubleAnimation.EasingFunction>
|
|
||||||
<CubicEase EasingMode="EaseOut"/>
|
|
||||||
</DoubleAnimation.EasingFunction>
|
|
||||||
</DoubleAnimation>
|
|
||||||
</Storyboard>
|
|
||||||
|
|
||||||
<!-- Camera icon pulse animation — both axes target the same ScaleTransform -->
|
|
||||||
<Storyboard x:Key="CameraPulse" RepeatBehavior="Forever">
|
|
||||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CameraScale"
|
|
||||||
Storyboard.TargetProperty="ScaleX">
|
|
||||||
<EasingDoubleKeyFrame KeyTime="0:0:0.0" Value="1.0"/>
|
|
||||||
<EasingDoubleKeyFrame KeyTime="0:0:1.0" Value="1.08">
|
|
||||||
<EasingDoubleKeyFrame.EasingFunction><SineEase EasingMode="EaseInOut"/></EasingDoubleKeyFrame.EasingFunction>
|
|
||||||
</EasingDoubleKeyFrame>
|
|
||||||
<EasingDoubleKeyFrame KeyTime="0:0:2.0" Value="1.0">
|
|
||||||
<EasingDoubleKeyFrame.EasingFunction><SineEase EasingMode="EaseInOut"/></EasingDoubleKeyFrame.EasingFunction>
|
|
||||||
</EasingDoubleKeyFrame>
|
|
||||||
</DoubleAnimationUsingKeyFrames>
|
|
||||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CameraScale"
|
|
||||||
Storyboard.TargetProperty="ScaleY">
|
|
||||||
<EasingDoubleKeyFrame KeyTime="0:0:0.0" Value="1.0"/>
|
|
||||||
<EasingDoubleKeyFrame KeyTime="0:0:1.0" Value="1.08">
|
|
||||||
<EasingDoubleKeyFrame.EasingFunction><SineEase EasingMode="EaseInOut"/></EasingDoubleKeyFrame.EasingFunction>
|
|
||||||
</EasingDoubleKeyFrame>
|
|
||||||
<EasingDoubleKeyFrame KeyTime="0:0:2.0" Value="1.0">
|
|
||||||
<EasingDoubleKeyFrame.EasingFunction><SineEase EasingMode="EaseInOut"/></EasingDoubleKeyFrame.EasingFunction>
|
|
||||||
</EasingDoubleKeyFrame>
|
|
||||||
</DoubleAnimationUsingKeyFrames>
|
|
||||||
</Storyboard>
|
|
||||||
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<!-- Start animations when control loads -->
|
|
||||||
<UserControl.Triggers>
|
|
||||||
<EventTrigger RoutedEvent="Loaded">
|
|
||||||
<BeginStoryboard Storyboard="{StaticResource CameraPulse}"/>
|
|
||||||
</EventTrigger>
|
|
||||||
</UserControl.Triggers>
|
|
||||||
|
|
||||||
<Grid Margin="16,12">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="340"/>
|
|
||||||
<ColumnDefinition Width="12"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
|
||||||
LEFT: Photo drop zone + analyse button
|
|
||||||
================================================================ -->
|
|
||||||
<Grid Grid.Column="0">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Drop zone with dashed border drawn via Rectangle overlay -->
|
|
||||||
<Grid Grid.Row="0">
|
|
||||||
<!-- Dashed border rectangle -->
|
|
||||||
<Rectangle x:Name="DropBorderRect"
|
|
||||||
RadiusX="10" RadiusY="10"
|
|
||||||
StrokeDashArray="6,4"
|
|
||||||
StrokeThickness="2"
|
|
||||||
Stroke="{DynamicResource MahApps.Brushes.Gray6}"
|
|
||||||
Fill="Transparent"
|
|
||||||
IsHitTestVisible="False"/>
|
|
||||||
|
|
||||||
<Border x:Name="DropZone"
|
|
||||||
CornerRadius="10"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray10}"
|
|
||||||
AllowDrop="True"
|
|
||||||
Drop="DropZone_Drop"
|
|
||||||
DragOver="DropZone_DragOver"
|
|
||||||
DragEnter="DropZone_DragEnter"
|
|
||||||
DragLeave="DropZone_DragLeave"
|
|
||||||
MinHeight="320"
|
|
||||||
Cursor="Hand"
|
|
||||||
MouseLeftButtonUp="DropZone_Click">
|
|
||||||
|
|
||||||
<!-- Wrapper grid so Border has only one child; children overlap via shared cell -->
|
|
||||||
<Grid>
|
|
||||||
<!-- Empty state hint -->
|
|
||||||
<Grid x:Name="DropHint" VerticalAlignment="Center" HorizontalAlignment="Center">
|
|
||||||
<StackPanel HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="CameraPlus"
|
|
||||||
x:Name="CameraIcon"
|
|
||||||
Width="64" Height="64"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
Margin="0,0,0,16"
|
|
||||||
RenderTransformOrigin="0.5,0.5">
|
|
||||||
<iconPacks:PackIconMaterial.RenderTransform>
|
|
||||||
<ScaleTransform x:Name="CameraScale" ScaleX="1" ScaleY="1"/>
|
|
||||||
</iconPacks:PackIconMaterial.RenderTransform>
|
|
||||||
</iconPacks:PackIconMaterial>
|
|
||||||
<TextBlock Text="Drop a photo here" FontSize="16" FontWeight="SemiBold"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray3}"/>
|
|
||||||
<TextBlock Text="or click to browse" FontSize="12" Margin="0,4,0,0"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
|
||||||
<TextBlock Text="JPG · PNG · GIF · WEBP" FontSize="11" Margin="0,14,0,0"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray7}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Loaded photo with rounded clip and drop shadow -->
|
|
||||||
<Grid x:Name="PhotoPreviewContainer" Visibility="Collapsed">
|
|
||||||
<Grid.Effect>
|
|
||||||
<DropShadowEffect BlurRadius="12" ShadowDepth="3" Opacity="0.25" Color="Black"/>
|
|
||||||
</Grid.Effect>
|
|
||||||
<Image x:Name="PhotoPreview"
|
|
||||||
Stretch="Uniform"
|
|
||||||
Margin="4"
|
|
||||||
RenderOptions.BitmapScalingMode="HighQuality">
|
|
||||||
<Image.Clip>
|
|
||||||
<RectangleGeometry x:Name="PhotoClip" RadiusX="8" RadiusY="8"/>
|
|
||||||
</Image.Clip>
|
|
||||||
</Image>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Clear photo button (top-right overlay) -->
|
|
||||||
<Button x:Name="ClearPhotoBtn"
|
|
||||||
Visibility="Collapsed"
|
|
||||||
Click="ClearPhoto_Click"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="0,8,8,0"
|
|
||||||
Width="24" Height="24"
|
|
||||||
Padding="3"
|
|
||||||
ToolTip="Remove photo"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Background="#CC222222"
|
|
||||||
BorderThickness="0">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Close" Width="12" Height="12" Foreground="White"/>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Photo filename label -->
|
|
||||||
<TextBlock x:Name="PhotoFilename" Grid.Row="1"
|
|
||||||
Text="" FontSize="11" Margin="0,6,0,0"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
TextTrimming="CharacterEllipsis"/>
|
|
||||||
|
|
||||||
<!-- Thumbnail strip (hidden until 2+ photos loaded) -->
|
|
||||||
<ScrollViewer Grid.Row="2"
|
|
||||||
x:Name="ThumbStripScroller"
|
|
||||||
HorizontalScrollBarVisibility="Auto"
|
|
||||||
VerticalScrollBarVisibility="Disabled"
|
|
||||||
Visibility="Collapsed"
|
|
||||||
Margin="0,8,0,0">
|
|
||||||
<WrapPanel x:Name="PhotoThumbStrip"
|
|
||||||
Orientation="Horizontal"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<!-- Analyse button -->
|
|
||||||
<Button Grid.Row="3" x:Name="AnalyseBtn"
|
|
||||||
Click="Analyse_Click"
|
|
||||||
Style="{StaticResource AiButton}"
|
|
||||||
Height="42" FontSize="15" Margin="0,10,0,0"
|
|
||||||
IsEnabled="False">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<mah:ProgressRing x:Name="AnalyseSpinner"
|
|
||||||
Width="18" Height="18" Margin="0,0,8,0"
|
|
||||||
Foreground="White" Visibility="Collapsed"/>
|
|
||||||
<iconPacks:PackIconMaterial x:Name="AnalyseIcon"
|
|
||||||
Kind="Magnify" Width="18" Height="18"
|
|
||||||
Margin="0,0,8,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock x:Name="AnalyseBtnText"
|
|
||||||
Text="Identify & Price with AI"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
|
||||||
RIGHT: Results panel
|
|
||||||
================================================================ -->
|
|
||||||
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel>
|
|
||||||
|
|
||||||
<!-- Idle state -->
|
|
||||||
<Border x:Name="IdlePanel" Style="{StaticResource SectionCard}"
|
|
||||||
MinHeight="400">
|
|
||||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="TagOutline"
|
|
||||||
Width="52" Height="52"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray7}"
|
|
||||||
Margin="0,0,0,16"/>
|
|
||||||
<TextBlock Text="Drop a photo and click Identify"
|
|
||||||
FontSize="15" FontWeight="SemiBold"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
|
||||||
<TextBlock Text="Claude will identify the item, write a listing description and suggest a realistic eBay UK selling price."
|
|
||||||
FontSize="12" Margin="0,8,0,0"
|
|
||||||
HorizontalAlignment="Center" TextAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray7}"
|
|
||||||
TextWrapping="Wrap" MaxWidth="320"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Loading state (shown during analysis) -->
|
|
||||||
<Border x:Name="LoadingPanel" Style="{StaticResource SectionCard}"
|
|
||||||
MinHeight="400" Visibility="Collapsed">
|
|
||||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
|
||||||
<mah:ProgressRing Width="48" Height="48"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Margin="0,0,0,20"/>
|
|
||||||
<TextBlock x:Name="LoadingStepText"
|
|
||||||
Text="Examining the photo…"
|
|
||||||
FontSize="15" FontWeight="SemiBold"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray3}"/>
|
|
||||||
<TextBlock Text="This usually takes 10–20 seconds"
|
|
||||||
FontSize="11" Margin="0,8,0,0"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray7}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Results (hidden until analysis complete) -->
|
|
||||||
<StackPanel x:Name="ResultsPanel" Visibility="Collapsed" Opacity="0">
|
|
||||||
<StackPanel.RenderTransform>
|
|
||||||
<TranslateTransform x:Name="ResultsTranslate" Y="20"/>
|
|
||||||
</StackPanel.RenderTransform>
|
|
||||||
|
|
||||||
<!-- Identified item -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
|
||||||
<iconPacks:PackIconMaterial Kind="CartOutline" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="ITEM IDENTIFIED" Style="{StaticResource SectionHeading}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Item name — large bold -->
|
|
||||||
<TextBlock x:Name="ItemNameText"
|
|
||||||
FontSize="20" FontWeight="Bold"
|
|
||||||
TextWrapping="Wrap" Margin="0,0,0,8"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray1}"/>
|
|
||||||
|
|
||||||
<!-- Brand/model pill badge -->
|
|
||||||
<Border x:Name="BrandPill"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
CornerRadius="12"
|
|
||||||
Padding="10,3"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Margin="0,0,0,10"
|
|
||||||
Visibility="Collapsed">
|
|
||||||
<TextBlock x:Name="BrandModelText"
|
|
||||||
FontSize="11" FontWeight="SemiBold"
|
|
||||||
Foreground="White"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Condition notes — green tinted box with eye icon -->
|
|
||||||
<Border Background="#1A4CAF50" BorderBrush="#4CAF50"
|
|
||||||
BorderThickness="1" CornerRadius="5" Padding="10,8">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Eye" Width="13" Height="13"
|
|
||||||
Margin="0,0,8,0" VerticalAlignment="Top"
|
|
||||||
Foreground="#4CAF50"/>
|
|
||||||
<TextBlock x:Name="ConditionText"
|
|
||||||
FontSize="12" TextWrapping="Wrap"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray2}"
|
|
||||||
MaxWidth="340"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Confidence badge (High / Medium / Low) -->
|
|
||||||
<Border x:Name="ConfidenceBadge"
|
|
||||||
CornerRadius="10" Padding="8,3"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Margin="0,10,0,0"
|
|
||||||
Visibility="Collapsed">
|
|
||||||
<TextBlock x:Name="ConfidenceText"
|
|
||||||
FontSize="10" FontWeight="SemiBold"
|
|
||||||
Foreground="White"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Confidence notes -->
|
|
||||||
<TextBlock x:Name="ConfidenceNotesText"
|
|
||||||
FontSize="11" FontStyle="Italic"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,6,0,0"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
Visibility="Collapsed"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Price -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
|
||||||
<iconPacks:PackIconMaterial Kind="CurrencyGbp" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="SUGGESTED PRICE" Style="{StaticResource SectionHeading}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Prominent price badge -->
|
|
||||||
<Border HorizontalAlignment="Left"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
CornerRadius="10"
|
|
||||||
Padding="18,8"
|
|
||||||
Margin="0,0,0,10">
|
|
||||||
<TextBlock x:Name="PriceSuggestedText"
|
|
||||||
FontSize="38" FontWeight="Bold"
|
|
||||||
Foreground="White"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Min · Suggested · Max visual bar -->
|
|
||||||
<Grid x:Name="PriceRangeBar" Margin="0,0,0,12" Visibility="Collapsed">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<!-- Connecting line -->
|
|
||||||
<Border Grid.Row="0" Height="2" Margin="12,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray7}"
|
|
||||||
CornerRadius="1"/>
|
|
||||||
<!-- Three dots + labels -->
|
|
||||||
<Grid Grid.Row="0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Ellipse Grid.Column="0" Width="10" Height="10"
|
|
||||||
Fill="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Ellipse Grid.Column="2" Width="14" Height="14"
|
|
||||||
Fill="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Ellipse Grid.Column="4" Width="10" Height="10"
|
|
||||||
Fill="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
</Grid>
|
|
||||||
<!-- Labels row -->
|
|
||||||
<Grid Grid.Row="1" Margin="0,4,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel Grid.Column="0" HorizontalAlignment="Center">
|
|
||||||
<TextBlock Text="MIN" FontSize="9" FontWeight="SemiBold"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray6}"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
<TextBlock x:Name="PriceMinText" FontSize="11"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray6}"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2" HorizontalAlignment="Center">
|
|
||||||
<TextBlock Text="SUGGESTED" FontSize="9" FontWeight="SemiBold"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="4" HorizontalAlignment="Center">
|
|
||||||
<TextBlock Text="MAX" FontSize="9" FontWeight="SemiBold"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray6}"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
<TextBlock x:Name="PriceMaxText" FontSize="11"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray6}"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Editable price override -->
|
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="Override price:" FontSize="11"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
|
||||||
<mah:NumericUpDown x:Name="PriceOverride"
|
|
||||||
Width="110" Height="32"
|
|
||||||
Minimum="0" Maximum="99999"
|
|
||||||
StringFormat="F2" Interval="0.5"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Live eBay price status -->
|
|
||||||
<StackPanel x:Name="LivePriceRow" Orientation="Horizontal"
|
|
||||||
Margin="0,6,0,0" Visibility="Collapsed">
|
|
||||||
<mah:ProgressRing x:Name="LivePriceSpinner"
|
|
||||||
Width="11" Height="11" Margin="0,0,6,0"/>
|
|
||||||
<TextBlock x:Name="LivePriceStatus"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
TextWrapping="Wrap"/>
|
|
||||||
</StackPanel>
|
|
||||||
<!-- Price reasoning -->
|
|
||||||
<TextBlock x:Name="PriceReasoningText"
|
|
||||||
FontSize="11" FontStyle="Italic"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,8,0,0"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
Visibility="Collapsed"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8"
|
|
||||||
HorizontalAlignment="Stretch">
|
|
||||||
<iconPacks:PackIconMaterial Kind="TagOutline" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="LISTING TITLE" Style="{StaticResource SectionHeading}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Button x:Name="CopyTitleBtn"
|
|
||||||
Style="{StaticResource CopyButton}"
|
|
||||||
Click="CopyTitle_Click"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
ToolTip="Copy title to clipboard">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ContentCopy" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBox x:Name="TitleBox"
|
|
||||||
MaxLength="80"
|
|
||||||
mah:TextBoxHelper.Watermark="Listing title (max 80 chars)"
|
|
||||||
TextChanged="TitleBox_TextChanged"/>
|
|
||||||
<TextBlock x:Name="TitleCount" Text="0 / 80" FontSize="10"
|
|
||||||
HorizontalAlignment="Right" Margin="0,3,0,0"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
|
||||||
<iconPacks:PackIconMaterial Kind="TextBox" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="DESCRIPTION" Style="{StaticResource SectionHeading}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Button x:Name="CopyDescBtn"
|
|
||||||
Style="{StaticResource CopyButton}"
|
|
||||||
Click="CopyDescription_Click"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
ToolTip="Copy description to clipboard">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ContentCopy" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBox x:Name="DescriptionBox"
|
|
||||||
Height="180" AcceptsReturn="True"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
|
||||||
Style="{DynamicResource MahApps.Styles.TextBox}"
|
|
||||||
FontSize="12"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Corrections for AI refinement -->
|
|
||||||
<Border BorderThickness="1" CornerRadius="8" Padding="14,10"
|
|
||||||
Margin="0,0,0,10"
|
|
||||||
BorderBrush="{DynamicResource MahApps.Brushes.Gray7}"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray9}">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Pencil" Width="12" Height="12"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="CORRECTIONS" FontSize="10" FontWeight="SemiBold"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBox x:Name="CorrectionsBox"
|
|
||||||
AcceptsReturn="False"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Height="52"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
|
||||||
Style="{DynamicResource MahApps.Styles.TextBox}"
|
|
||||||
FontSize="12"
|
|
||||||
mah:TextBoxHelper.Watermark="e.g. earrings are white gold with diamonds, not silver and zirconium"
|
|
||||||
Margin="0,0,0,8"/>
|
|
||||||
<Button x:Name="RefineBtn"
|
|
||||||
Click="Refine_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="32" Padding="12,0" HorizontalAlignment="Left">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial x:Name="RefineIcon" Kind="AutoFix" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<mah:ProgressRing x:Name="RefineSpinner" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" Visibility="Collapsed"/>
|
|
||||||
<TextBlock x:Name="RefineBtnText" Text="Refine with AI"
|
|
||||||
VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Actions + toast overlay -->
|
|
||||||
<Grid Margin="0,4,0,16" ClipToBounds="False">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Button x:Name="UseDetailsBtn"
|
|
||||||
Content="Use for New Listing →"
|
|
||||||
Click="UseDetails_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
|
||||||
Height="36" Padding="16,0" FontSize="13" FontWeight="SemiBold"/>
|
|
||||||
<Button x:Name="SaveListingBtn"
|
|
||||||
Click="SaveListing_Click"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="36" Padding="14,0">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="BookmarkOutline" Width="14" Height="14"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
x:Name="SaveIcon"/>
|
|
||||||
<iconPacks:PackIconMaterial Kind="BookmarkCheck" Width="14" Height="14"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
x:Name="SavedIcon" Visibility="Collapsed"
|
|
||||||
Foreground="LimeGreen"/>
|
|
||||||
<TextBlock x:Name="SaveBtnText" Text="Save Listing"
|
|
||||||
VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button x:Name="ReAnalyseBtn"
|
|
||||||
Click="ReAnalyse_Click"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="36" Padding="12,0"
|
|
||||||
ToolTip="Re-run AI analysis with the same photo(s)">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Refresh" Width="14" Height="14"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Re-analyse" VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Content="Analyse Another"
|
|
||||||
Click="AnalyseAnother_Click"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="36" Padding="12,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Save confirmation toast -->
|
|
||||||
<Border x:Name="SaveToast"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
CornerRadius="6"
|
|
||||||
Padding="14,8"
|
|
||||||
Margin="0,0,0,-48"
|
|
||||||
Visibility="Collapsed"
|
|
||||||
IsHitTestVisible="False">
|
|
||||||
<Border.RenderTransform>
|
|
||||||
<TranslateTransform x:Name="ToastTranslate" Y="40"/>
|
|
||||||
</Border.RenderTransform>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Check" Width="14" Height="14"
|
|
||||||
Margin="0,0,8,0" VerticalAlignment="Center"
|
|
||||||
Foreground="White"/>
|
|
||||||
<TextBlock Text="Saved to Saved Listings"
|
|
||||||
FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="White" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,710 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Animation;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Threading;
|
|
||||||
using EbayListingTool.Models;
|
|
||||||
using EbayListingTool.Services;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace EbayListingTool.Views;
|
|
||||||
|
|
||||||
public partial class PhotoAnalysisView : UserControl
|
|
||||||
{
|
|
||||||
private AiAssistantService? _aiService;
|
|
||||||
private SavedListingsService? _savedService;
|
|
||||||
private EbayPriceResearchService? _priceService;
|
|
||||||
private List<string> _currentImagePaths = new();
|
|
||||||
private PhotoAnalysisResult? _lastResult;
|
|
||||||
private int _activePhotoIndex = 0;
|
|
||||||
private DispatcherTimer? _saveBtnTimer; // M1: field so we can stop it on Unloaded
|
|
||||||
private DispatcherTimer? _holdTimer; // Q2: field so we can stop it on Unloaded
|
|
||||||
|
|
||||||
private const int MaxPhotos = 4;
|
|
||||||
|
|
||||||
// Loading step cycling
|
|
||||||
private readonly DispatcherTimer _loadingTimer;
|
|
||||||
private int _loadingStep;
|
|
||||||
private static readonly string[] LoadingSteps =
|
|
||||||
[
|
|
||||||
"Examining the photo\u2026",
|
|
||||||
"Identifying the item\u2026",
|
|
||||||
"Researching eBay prices\u2026",
|
|
||||||
"Writing description\u2026"
|
|
||||||
];
|
|
||||||
|
|
||||||
// Event raised when user clicks "Use for New Listing" — Q1: passes all loaded photos
|
|
||||||
public event Action<PhotoAnalysisResult, IReadOnlyList<string>, decimal>? UseDetailsRequested;
|
|
||||||
|
|
||||||
public PhotoAnalysisView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
_loadingTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
|
||||||
_loadingTimer.Tick += LoadingTimer_Tick;
|
|
||||||
|
|
||||||
// M1 / Q2: stop timers when control is unloaded to avoid memory leaks
|
|
||||||
Unloaded += (_, _) => { _saveBtnTimer?.Stop(); _holdTimer?.Stop(); };
|
|
||||||
|
|
||||||
// Keep photo clip geometry in sync with container size
|
|
||||||
PhotoPreviewContainer.SizeChanged += PhotoPreviewContainer_SizeChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialise(AiAssistantService aiService, SavedListingsService savedService,
|
|
||||||
EbayPriceResearchService priceService)
|
|
||||||
{
|
|
||||||
_aiService = aiService;
|
|
||||||
_savedService = savedService;
|
|
||||||
_priceService = priceService;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Photo clip geometry sync ----
|
|
||||||
|
|
||||||
private void PhotoPreviewContainer_SizeChanged(object sender, SizeChangedEventArgs e)
|
|
||||||
{
|
|
||||||
PhotoClip.Rect = new System.Windows.Rect(0, 0, e.NewSize.Width, e.NewSize.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Drop zone ----
|
|
||||||
|
|
||||||
private void DropZone_DragOver(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
|
|
||||||
? DragDropEffects.Copy : DragDropEffects.None;
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DropZone_DragEnter(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
// Solid accent border on drag-enter
|
|
||||||
DropBorderRect.Stroke = (Brush)FindResource("MahApps.Brushes.Accent");
|
|
||||||
DropBorderRect.StrokeDashArray = null;
|
|
||||||
DropBorderRect.StrokeThickness = 2.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DropZone_DragLeave(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
DropBorderRect.Stroke = (Brush)FindResource("MahApps.Brushes.Gray6");
|
|
||||||
DropBorderRect.StrokeDashArray = new DoubleCollection([6, 4]);
|
|
||||||
DropBorderRect.StrokeThickness = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DropZone_Drop(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
DropZone_DragLeave(sender, e);
|
|
||||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
|
||||||
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
|
||||||
foreach (var file in files.Where(IsImageFile))
|
|
||||||
{
|
|
||||||
LoadPhoto(file);
|
|
||||||
if (_currentImagePaths.Count >= MaxPhotos) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DropZone_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
var dlg = new OpenFileDialog
|
|
||||||
{
|
|
||||||
Title = "Select photo(s) of the item",
|
|
||||||
Filter = "Images|*.jpg;*.jpeg;*.png;*.gif;*.webp|All files|*.*",
|
|
||||||
Multiselect = true
|
|
||||||
};
|
|
||||||
if (dlg.ShowDialog() != true) return;
|
|
||||||
foreach (var file in dlg.FileNames)
|
|
||||||
{
|
|
||||||
LoadPhoto(file);
|
|
||||||
if (_currentImagePaths.Count >= MaxPhotos) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearPhoto_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
_currentImagePaths.Clear();
|
|
||||||
_activePhotoIndex = 0;
|
|
||||||
PhotoPreview.Source = null;
|
|
||||||
PhotoPreviewContainer.Visibility = Visibility.Collapsed;
|
|
||||||
ClearPhotoBtn.Visibility = Visibility.Collapsed;
|
|
||||||
DropHint.Visibility = Visibility.Visible;
|
|
||||||
PhotoFilename.Text = "";
|
|
||||||
AnalyseBtn.IsEnabled = false;
|
|
||||||
|
|
||||||
UpdateThumbStrip();
|
|
||||||
ResultsPanel.Visibility = Visibility.Collapsed;
|
|
||||||
ResultsPanel.Opacity = 0;
|
|
||||||
IdlePanel.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds <paramref name="path"/> to the photo list (up to MaxPhotos).
|
|
||||||
/// The preview always shows the most recently added image.
|
|
||||||
/// </summary>
|
|
||||||
private void LoadPhoto(string path)
|
|
||||||
{
|
|
||||||
if (_currentImagePaths.Contains(path)) return;
|
|
||||||
if (_currentImagePaths.Count >= MaxPhotos)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"You can add up to {MaxPhotos} photos. Remove one before adding more.",
|
|
||||||
"Photo limit reached", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_currentImagePaths.Add(path);
|
|
||||||
|
|
||||||
var bmp = new BitmapImage();
|
|
||||||
bmp.BeginInit();
|
|
||||||
bmp.UriSource = new Uri(path, UriKind.Absolute); // W1
|
|
||||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
|
||||||
bmp.DecodePixelWidth = 600;
|
|
||||||
bmp.EndInit();
|
|
||||||
bmp.Freeze(); // M2: cross-thread safe, reduces GC pressure
|
|
||||||
|
|
||||||
PhotoPreview.Source = bmp;
|
|
||||||
PhotoPreviewContainer.Visibility = Visibility.Visible;
|
|
||||||
ClearPhotoBtn.Visibility = Visibility.Visible;
|
|
||||||
DropHint.Visibility = Visibility.Collapsed;
|
|
||||||
|
|
||||||
_activePhotoIndex = _currentImagePaths.Count - 1;
|
|
||||||
|
|
||||||
UpdatePhotoFilenameLabel();
|
|
||||||
UpdateThumbStrip();
|
|
||||||
AnalyseBtn.IsEnabled = true;
|
|
||||||
|
|
||||||
// Collapse results so user re-analyses after adding photos
|
|
||||||
ResultsPanel.Visibility = Visibility.Collapsed;
|
|
||||||
ResultsPanel.Opacity = 0;
|
|
||||||
IdlePanel.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_currentImagePaths.Remove(path);
|
|
||||||
MessageBox.Show($"Could not load image: {ex.Message}", "Error",
|
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePhotoFilenameLabel()
|
|
||||||
{
|
|
||||||
PhotoFilename.Text = _currentImagePaths.Count switch
|
|
||||||
{
|
|
||||||
0 => "",
|
|
||||||
1 => Path.GetFileName(_currentImagePaths[0]),
|
|
||||||
_ => $"{_currentImagePaths.Count} photos loaded"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Analyse ----
|
|
||||||
|
|
||||||
private async void Analyse_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_aiService == null || _currentImagePaths.Count == 0) return;
|
|
||||||
|
|
||||||
SetAnalysing(true);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _aiService.AnalyseItemFromPhotosAsync(_currentImagePaths);
|
|
||||||
_lastResult = result;
|
|
||||||
ShowResults(result);
|
|
||||||
// Fire live price lookup in background — updates price display when ready
|
|
||||||
_ = UpdateLivePricesAsync(result.Title);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"Analysis failed:\n\n{ex.Message}", "AI Error",
|
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
SetAnalysing(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-analyse simply repeats the same call — idempotent by design
|
|
||||||
private void ReAnalyse_Click(object sender, RoutedEventArgs e)
|
|
||||||
=> Analyse_Click(sender, e);
|
|
||||||
|
|
||||||
private async void Refine_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_aiService == null || _lastResult == null) return;
|
|
||||||
|
|
||||||
var corrections = CorrectionsBox.Text.Trim();
|
|
||||||
if (string.IsNullOrEmpty(corrections))
|
|
||||||
{
|
|
||||||
CorrectionsBox.Focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var title = TitleBox.Text;
|
|
||||||
var description = DescriptionBox.Text;
|
|
||||||
var price = (decimal)(PriceOverride.Value ?? (double)_lastResult.PriceSuggested);
|
|
||||||
|
|
||||||
SetRefining(true);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var (newTitle, newDesc, newPrice, newReasoning) =
|
|
||||||
await _aiService.RefineWithCorrectionsAsync(title, description, price, corrections);
|
|
||||||
|
|
||||||
TitleBox.Text = newTitle;
|
|
||||||
DescriptionBox.Text = newDesc;
|
|
||||||
PriceOverride.Value = (double)Math.Round(newPrice, 2); // Issue 6
|
|
||||||
PriceSuggestedText.Text = newPrice > 0 ? $"£{newPrice:F2}" : "—";
|
|
||||||
|
|
||||||
_lastResult.Title = newTitle;
|
|
||||||
_lastResult.Description = newDesc;
|
|
||||||
_lastResult.PriceSuggested = newPrice;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(newReasoning))
|
|
||||||
{
|
|
||||||
PriceReasoningText.Text = newReasoning;
|
|
||||||
PriceReasoningText.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the corrections box now they're applied
|
|
||||||
CorrectionsBox.Text = "";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"Refinement failed:\n\n{ex.Message}", "AI Error",
|
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
SetRefining(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetRefining(bool busy)
|
|
||||||
{
|
|
||||||
RefineBtn.IsEnabled = !busy;
|
|
||||||
RefineIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
RefineSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
RefineBtnText.Text = busy ? "Refining…" : "Refine with AI";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowResults(PhotoAnalysisResult r)
|
|
||||||
{
|
|
||||||
IdlePanel.Visibility = Visibility.Collapsed;
|
|
||||||
LoadingPanel.Visibility = Visibility.Collapsed;
|
|
||||||
ResultsPanel.Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
// Item identification
|
|
||||||
ItemNameText.Text = r.ItemName;
|
|
||||||
|
|
||||||
var brandModel = string.Join(" \u00b7 ",
|
|
||||||
new[] { r.Brand, r.Model }.Where(s => !string.IsNullOrWhiteSpace(s)));
|
|
||||||
if (!string.IsNullOrWhiteSpace(brandModel))
|
|
||||||
{
|
|
||||||
BrandModelText.Text = brandModel;
|
|
||||||
BrandPill.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BrandPill.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConditionText.Text = r.ConditionNotes;
|
|
||||||
|
|
||||||
// Confidence badge
|
|
||||||
ConfidenceBadge.Visibility = Visibility.Collapsed;
|
|
||||||
if (!string.IsNullOrWhiteSpace(r.IdentificationConfidence))
|
|
||||||
{
|
|
||||||
ConfidenceText.Text = r.IdentificationConfidence;
|
|
||||||
ConfidenceBadge.Background = r.IdentificationConfidence.ToLower() switch
|
|
||||||
{
|
|
||||||
"high" => new SolidColorBrush(Color.FromRgb(34, 139, 34)),
|
|
||||||
"medium" => new SolidColorBrush(Color.FromRgb(210, 140, 0)),
|
|
||||||
_ => new SolidColorBrush(Color.FromRgb(192, 0, 0))
|
|
||||||
};
|
|
||||||
ConfidenceBadge.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
ConfidenceNotesText.Text = r.ConfidenceNotes;
|
|
||||||
ConfidenceNotesText.Visibility = string.IsNullOrWhiteSpace(r.ConfidenceNotes)
|
|
||||||
? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
|
|
||||||
// Price badge
|
|
||||||
PriceSuggestedText.Text = r.PriceSuggested > 0 ? $"\u00a3{r.PriceSuggested:F2}" : "\u2014";
|
|
||||||
|
|
||||||
// Price range bar
|
|
||||||
if (r.PriceMin > 0 && r.PriceMax > 0)
|
|
||||||
{
|
|
||||||
PriceMinText.Text = $"\u00a3{r.PriceMin:F2}";
|
|
||||||
PriceMaxText.Text = $"\u00a3{r.PriceMax:F2}";
|
|
||||||
PriceRangeBar.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PriceRangeBar.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
PriceOverride.Value = (double)Math.Round(r.PriceSuggested, 2); // Issue 6
|
|
||||||
|
|
||||||
// Price reasoning
|
|
||||||
PriceReasoningText.Text = r.PriceReasoning;
|
|
||||||
PriceReasoningText.Visibility = string.IsNullOrWhiteSpace(r.PriceReasoning)
|
|
||||||
? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
|
|
||||||
// Editable fields
|
|
||||||
TitleBox.Text = r.Title;
|
|
||||||
DescriptionBox.Text = r.Description;
|
|
||||||
|
|
||||||
// Reset live price row until lookup completes
|
|
||||||
LivePriceRow.Visibility = Visibility.Collapsed;
|
|
||||||
|
|
||||||
// Animate results in
|
|
||||||
var sb = (Storyboard)FindResource("ResultsReveal");
|
|
||||||
sb.Begin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateLivePricesAsync(string query)
|
|
||||||
{
|
|
||||||
if (_priceService == null) return;
|
|
||||||
|
|
||||||
// Issue 7: guard against off-thread callers (fire-and-forget may lose sync context)
|
|
||||||
if (!Dispatcher.CheckAccess())
|
|
||||||
{
|
|
||||||
await Dispatcher.InvokeAsync(() => UpdateLivePricesAsync(query)).Task.Unwrap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Issue 1: spinner-show inside try so a disposed control doesn't crash the caller
|
|
||||||
LivePriceRow.Visibility = Visibility.Visible;
|
|
||||||
LivePriceSpinner.Visibility = Visibility.Visible;
|
|
||||||
LivePriceStatus.Text = "Checking live eBay UK prices…";
|
|
||||||
|
|
||||||
var live = await _priceService.GetLivePricesAsync(query);
|
|
||||||
|
|
||||||
if (live.Count == 0)
|
|
||||||
{
|
|
||||||
LivePriceStatus.Text = "No matching live listings found.";
|
|
||||||
LivePriceSpinner.Visibility = Visibility.Collapsed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update range bar with real data
|
|
||||||
PriceMinText.Text = $"£{live.Min:F2}";
|
|
||||||
PriceMaxText.Text = $"£{live.Max:F2}";
|
|
||||||
PriceRangeBar.Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
// Update suggested price to 40th percentile (competitive but not cheapest)
|
|
||||||
var suggested = live.Suggested;
|
|
||||||
PriceSuggestedText.Text = $"£{suggested:F2}";
|
|
||||||
PriceOverride.Value = (double)Math.Round(suggested, 2); // Issue 6: avoid decimal→double drift
|
|
||||||
if (_lastResult != null) _lastResult.PriceSuggested = suggested;
|
|
||||||
|
|
||||||
// Update status label
|
|
||||||
LivePriceSpinner.Visibility = Visibility.Collapsed;
|
|
||||||
LivePriceStatus.Text =
|
|
||||||
$"Based on {live.Count} live eBay UK listing{(live.Count == 1 ? "" : "s")} " +
|
|
||||||
$"(range £{live.Min:F2} – £{live.Max:F2})";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LivePriceSpinner.Visibility = Visibility.Collapsed;
|
|
||||||
LivePriceStatus.Text = $"Live price lookup unavailable: {ex.Message}";
|
|
||||||
}
|
|
||||||
catch { /* control may be unloaded by the time catch runs */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TitleBox_TextChanged(object sender, TextChangedEventArgs e)
|
|
||||||
{
|
|
||||||
var len = TitleBox.Text.Length;
|
|
||||||
TitleCount.Text = $"{len} / 80";
|
|
||||||
TitleCount.Foreground = len > 75
|
|
||||||
? Brushes.OrangeRed
|
|
||||||
: (Brush)FindResource("MahApps.Brushes.Gray5");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UseDetails_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_lastResult == null) return;
|
|
||||||
|
|
||||||
// Copy any edits back into result before passing on
|
|
||||||
_lastResult.Title = TitleBox.Text;
|
|
||||||
_lastResult.Description = DescriptionBox.Text;
|
|
||||||
|
|
||||||
var price = (decimal)(PriceOverride.Value ?? (double)_lastResult.PriceSuggested);
|
|
||||||
UseDetailsRequested?.Invoke(_lastResult, _currentImagePaths, price); // Q1: pass all photos
|
|
||||||
|
|
||||||
// Switch to New Listing tab
|
|
||||||
if (Window.GetWindow(this) is MainWindow mw)
|
|
||||||
mw.SwitchToNewListingTab();
|
|
||||||
|
|
||||||
GetWindow()?.SetStatus($"Details loaded for: {_lastResult.Title}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveListing_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_lastResult == null || _savedService == null) return;
|
|
||||||
|
|
||||||
// Use edited title/description if the user changed them
|
|
||||||
var title = TitleBox.Text.Trim();
|
|
||||||
var description = DescriptionBox.Text.Trim();
|
|
||||||
var price = (decimal)(PriceOverride.Value ?? (double)_lastResult.PriceSuggested);
|
|
||||||
|
|
||||||
_savedService.Save(title, description, price,
|
|
||||||
_lastResult.CategoryKeyword, _lastResult.ConditionNotes,
|
|
||||||
_currentImagePaths);
|
|
||||||
|
|
||||||
// Brief visual confirmation on the button — M1: use field timer, stop previous if re-saved quickly
|
|
||||||
_saveBtnTimer?.Stop();
|
|
||||||
SaveIcon.Visibility = Visibility.Collapsed;
|
|
||||||
SavedIcon.Visibility = Visibility.Visible;
|
|
||||||
SaveBtnText.Text = "Saved!";
|
|
||||||
|
|
||||||
_saveBtnTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
|
||||||
_saveBtnTimer.Tick += (s, _) =>
|
|
||||||
{
|
|
||||||
_saveBtnTimer?.Stop();
|
|
||||||
SaveIcon.Visibility = Visibility.Visible;
|
|
||||||
SavedIcon.Visibility = Visibility.Collapsed;
|
|
||||||
SaveBtnText.Text = "Save Listing";
|
|
||||||
};
|
|
||||||
_saveBtnTimer.Start();
|
|
||||||
|
|
||||||
ShowSaveToast();
|
|
||||||
|
|
||||||
// Notify main window to refresh the gallery
|
|
||||||
(Window.GetWindow(this) as MainWindow)?.RefreshSavedListings();
|
|
||||||
GetWindow()?.SetStatus($"Saved: {title}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AnalyseAnother_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
_currentImagePaths.Clear();
|
|
||||||
_lastResult = null;
|
|
||||||
_activePhotoIndex = 0;
|
|
||||||
PhotoPreview.Source = null;
|
|
||||||
PhotoPreviewContainer.Visibility = Visibility.Collapsed;
|
|
||||||
ClearPhotoBtn.Visibility = Visibility.Collapsed;
|
|
||||||
DropHint.Visibility = Visibility.Visible;
|
|
||||||
PhotoFilename.Text = "";
|
|
||||||
AnalyseBtn.IsEnabled = false;
|
|
||||||
UpdateThumbStrip();
|
|
||||||
ResultsPanel.Visibility = Visibility.Collapsed;
|
|
||||||
ResultsPanel.Opacity = 0;
|
|
||||||
IdlePanel.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Thumb strip ----
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rebuilds the thumbnail strip from <see cref="_currentImagePaths"/>.
|
|
||||||
/// Shows/hides the strip based on whether 2+ photos are loaded.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateThumbStrip()
|
|
||||||
{
|
|
||||||
PhotoThumbStrip.Children.Clear();
|
|
||||||
|
|
||||||
if (_currentImagePaths.Count < 2)
|
|
||||||
{
|
|
||||||
ThumbStripScroller.Visibility = Visibility.Collapsed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ThumbStripScroller.Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
var accentBrush = (Brush)FindResource("MahApps.Brushes.Accent");
|
|
||||||
var neutralBrush = (Brush)FindResource("MahApps.Brushes.Gray7");
|
|
||||||
|
|
||||||
for (int i = 0; i < _currentImagePaths.Count; i++)
|
|
||||||
{
|
|
||||||
var index = i; // capture for closure
|
|
||||||
var path = _currentImagePaths[i];
|
|
||||||
|
|
||||||
BitmapImage? thumb = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
thumb = new BitmapImage();
|
|
||||||
thumb.BeginInit();
|
|
||||||
thumb.UriSource = new Uri(path, UriKind.Absolute); // W1
|
|
||||||
thumb.CacheOption = BitmapCacheOption.OnLoad;
|
|
||||||
thumb.DecodePixelWidth = 80;
|
|
||||||
thumb.EndInit();
|
|
||||||
thumb.Freeze(); // M2
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Skip thumbnails that fail to load
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isActive = (index == _activePhotoIndex);
|
|
||||||
|
|
||||||
var img = new Image
|
|
||||||
{
|
|
||||||
Source = thumb,
|
|
||||||
Width = 60,
|
|
||||||
Height = 60,
|
|
||||||
Stretch = Stretch.UniformToFill
|
|
||||||
};
|
|
||||||
RenderOptions.SetBitmapScalingMode(img, BitmapScalingMode.HighQuality);
|
|
||||||
// Clip image to rounded rect
|
|
||||||
img.Clip = new System.Windows.Media.RectangleGeometry(
|
|
||||||
new System.Windows.Rect(0, 0, 60, 60), 4, 4);
|
|
||||||
|
|
||||||
var border = new Border
|
|
||||||
{
|
|
||||||
Width = 64,
|
|
||||||
Height = 64,
|
|
||||||
Margin = new Thickness(3),
|
|
||||||
CornerRadius = new CornerRadius(5),
|
|
||||||
BorderThickness = new Thickness(isActive ? 2.5 : 1.5),
|
|
||||||
BorderBrush = isActive ? accentBrush : neutralBrush,
|
|
||||||
Background = System.Windows.Media.Brushes.Transparent,
|
|
||||||
Cursor = System.Windows.Input.Cursors.Hand,
|
|
||||||
Child = img
|
|
||||||
};
|
|
||||||
|
|
||||||
border.MouseLeftButtonUp += (_, _) =>
|
|
||||||
{
|
|
||||||
_activePhotoIndex = index;
|
|
||||||
|
|
||||||
// Load that photo into main preview
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bmp = new BitmapImage();
|
|
||||||
bmp.BeginInit();
|
|
||||||
bmp.UriSource = new Uri(_currentImagePaths[index], UriKind.Absolute); // W1
|
|
||||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
|
||||||
bmp.DecodePixelWidth = 600;
|
|
||||||
bmp.EndInit();
|
|
||||||
bmp.Freeze(); // M2
|
|
||||||
PhotoPreview.Source = bmp;
|
|
||||||
}
|
|
||||||
catch { /* silently ignore */ }
|
|
||||||
|
|
||||||
// Q3: full rebuild avoids index-desync when thumbnails skipped on load error
|
|
||||||
UpdateThumbStrip();
|
|
||||||
};
|
|
||||||
|
|
||||||
PhotoThumbStrip.Children.Add(border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates only the border highlights on the existing thumb strip children
|
|
||||||
/// after the active index changes, avoiding a full thumbnail reload.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateThumbStripHighlight()
|
|
||||||
{
|
|
||||||
var accentBrush = (Brush)FindResource("MahApps.Brushes.Accent");
|
|
||||||
var neutralBrush = (Brush)FindResource("MahApps.Brushes.Gray7");
|
|
||||||
|
|
||||||
int childIndex = 0;
|
|
||||||
for (int i = 0; i < _currentImagePaths.Count; i++)
|
|
||||||
{
|
|
||||||
if (childIndex >= PhotoThumbStrip.Children.Count) break;
|
|
||||||
if (PhotoThumbStrip.Children[childIndex] is Border b)
|
|
||||||
{
|
|
||||||
bool isActive = (i == _activePhotoIndex);
|
|
||||||
b.BorderBrush = isActive ? accentBrush : neutralBrush;
|
|
||||||
b.BorderThickness = new Thickness(isActive ? 2.5 : 1.5);
|
|
||||||
}
|
|
||||||
childIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Save toast ----
|
|
||||||
|
|
||||||
private void ShowSaveToast()
|
|
||||||
{
|
|
||||||
// Issue 8: always restart — stop any in-progress hold timer and cancel the running
|
|
||||||
// animation so the flag can never get permanently stuck and rapid saves feel responsive.
|
|
||||||
_holdTimer?.Stop();
|
|
||||||
_holdTimer = null;
|
|
||||||
ToastTranslate.BeginAnimation(TranslateTransform.YProperty, null); // cancel current animation
|
|
||||||
|
|
||||||
SaveToast.Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
// Slide in: Y from +40 to 0
|
|
||||||
var slideIn = new DoubleAnimation(40, 0, new Duration(TimeSpan.FromMilliseconds(220)))
|
|
||||||
{
|
|
||||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
|
||||||
};
|
|
||||||
|
|
||||||
// After 2.5 s total: slide out Y from 0 to +40, then hide
|
|
||||||
slideIn.Completed += (_, _) =>
|
|
||||||
{
|
|
||||||
_holdTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(2500 - 220) }; // Q2: field
|
|
||||||
_holdTimer.Tick += (s2, _) =>
|
|
||||||
{
|
|
||||||
_holdTimer.Stop();
|
|
||||||
var slideOut = new DoubleAnimation(0, 40, new Duration(TimeSpan.FromMilliseconds(180)))
|
|
||||||
{
|
|
||||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
|
|
||||||
};
|
|
||||||
slideOut.Completed += (_, _) =>
|
|
||||||
{
|
|
||||||
SaveToast.Visibility = Visibility.Collapsed;
|
|
||||||
};
|
|
||||||
ToastTranslate.BeginAnimation(TranslateTransform.YProperty, slideOut);
|
|
||||||
};
|
|
||||||
_holdTimer.Start();
|
|
||||||
};
|
|
||||||
|
|
||||||
ToastTranslate.BeginAnimation(TranslateTransform.YProperty, slideIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Copy buttons ----
|
|
||||||
|
|
||||||
private void CopyTitle_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(TitleBox.Text))
|
|
||||||
Clipboard.SetText(TitleBox.Text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyDescription_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(DescriptionBox.Text))
|
|
||||||
Clipboard.SetText(DescriptionBox.Text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Loading step cycling ----
|
|
||||||
|
|
||||||
private void LoadingTimer_Tick(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
_loadingStep = (_loadingStep + 1) % LoadingSteps.Length;
|
|
||||||
LoadingStepText.Text = LoadingSteps[_loadingStep];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Helpers ----
|
|
||||||
|
|
||||||
private void SetAnalysing(bool busy)
|
|
||||||
{
|
|
||||||
AnalyseSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
AnalyseIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
AnalyseBtnText.Text = busy ? "Analysing\u2026" : "Identify & Price with AI";
|
|
||||||
AnalyseBtn.IsEnabled = !busy;
|
|
||||||
IsEnabled = !busy;
|
|
||||||
|
|
||||||
if (busy)
|
|
||||||
{
|
|
||||||
IdlePanel.Visibility = Visibility.Collapsed;
|
|
||||||
ResultsPanel.Visibility = Visibility.Collapsed;
|
|
||||||
LoadingPanel.Visibility = Visibility.Visible;
|
|
||||||
_loadingStep = 0;
|
|
||||||
LoadingStepText.Text = LoadingSteps[0];
|
|
||||||
_loadingTimer.Start();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_loadingTimer.Stop();
|
|
||||||
LoadingPanel.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsImageFile(string path)
|
|
||||||
{
|
|
||||||
var ext = Path.GetExtension(path).ToLower();
|
|
||||||
return ext is ".jpg" or ".jpeg" or ".png" or ".gif" or ".webp" or ".bmp";
|
|
||||||
}
|
|
||||||
|
|
||||||
private MainWindow? GetWindow() => Window.GetWindow(this) as MainWindow;
|
|
||||||
}
|
|
||||||
@@ -1,566 +0,0 @@
|
|||||||
<UserControl x:Class="EbayListingTool.Views.SingleItemView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
|
|
||||||
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
|
||||||
Loaded="UserControl_Loaded">
|
|
||||||
|
|
||||||
<UserControl.Resources>
|
|
||||||
|
|
||||||
<!-- Section card — subtle bordered/shaded panel wrapping a group of fields -->
|
|
||||||
<Style x:Key="SectionCard" TargetType="Border">
|
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
<Setter Property="CornerRadius" Value="4"/>
|
|
||||||
<Setter Property="Padding" Value="14,12"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,10"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource MahApps.Brushes.Gray8}"/>
|
|
||||||
<Setter Property="Background" Value="{DynamicResource MahApps.Brushes.Gray10}"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Upper-case accent section heading used inside each card -->
|
|
||||||
<Style x:Key="SectionHeading" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontSize" Value="10"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,0"/>
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Standard field label inside a card -->
|
|
||||||
<Style x:Key="FieldLabel" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="FontSize" Value="12"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource MahApps.Brushes.Gray2}"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Small red asterisk for required fields -->
|
|
||||||
<Style x:Key="RequiredAsterisk" TargetType="TextBlock">
|
|
||||||
<Setter Property="Text" Value=" *"/>
|
|
||||||
<Setter Property="FontWeight" Value="Bold"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="Foreground" Value="#E53935"/>
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
|
||||||
<Setter Property="ToolTip" Value="Required"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- AI buttons: indigo-to-violet gradient, icon + label -->
|
|
||||||
<Style x:Key="AiButton" TargetType="Button"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.Button.Square}">
|
|
||||||
<Setter Property="Height" Value="28"/>
|
|
||||||
<Setter Property="Padding" Value="8,0"/>
|
|
||||||
<Setter Property="FontSize" Value="12"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Background">
|
|
||||||
<Setter.Value>
|
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
|
||||||
<GradientStop Color="#7C3AED" Offset="0"/>
|
|
||||||
<GradientStop Color="#4F46E5" Offset="1"/>
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter Property="Background">
|
|
||||||
<Setter.Value>
|
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
|
||||||
<GradientStop Color="#8B5CF6" Offset="0"/>
|
|
||||||
<GradientStop Color="#6366F1" Offset="1"/>
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
|
||||||
<Setter Property="Opacity" Value="0.45"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Primary post/action button -->
|
|
||||||
<Style x:Key="PostButton" TargetType="Button"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.Button.Square.Accent}">
|
|
||||||
<Setter Property="Height" Value="36"/>
|
|
||||||
<Setter Property="Padding" Value="20,0"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Grid Margin="16,12,16,12">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="8"/>
|
|
||||||
<ColumnDefinition Width="290"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
|
||||||
LEFT COLUMN — form fields grouped into section cards
|
|
||||||
================================================================ -->
|
|
||||||
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel>
|
|
||||||
|
|
||||||
<!-- LISTING DETAILS -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
|
|
||||||
<!-- Card header row -->
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
|
||||||
<iconPacks:PackIconMaterial Kind="TagOutline" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="LISTING DETAILS" Style="{StaticResource SectionHeading}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Title label row with required asterisk -->
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="Title"/>
|
|
||||||
<TextBlock Style="{StaticResource RequiredAsterisk}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<!-- Title + AI Title button -->
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBox x:Name="TitleBox"
|
|
||||||
mah:TextBoxHelper.Watermark="Item title (max 80 characters)"
|
|
||||||
MaxLength="80"
|
|
||||||
TextChanged="TitleBox_TextChanged"/>
|
|
||||||
<Button Grid.Column="1" Margin="6,0,0,0"
|
|
||||||
Style="{StaticResource AiButton}"
|
|
||||||
Click="AiTitle_Click"
|
|
||||||
ToolTip="Ask Claude to write a keyword-rich eBay title based on your item details">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<mah:ProgressRing x:Name="TitleSpinner"
|
|
||||||
Width="11" Height="11" Margin="0,0,4,0"
|
|
||||||
Foreground="White" Visibility="Collapsed"/>
|
|
||||||
<iconPacks:PackIconMaterial x:Name="TitleAiIcon"
|
|
||||||
Kind="AutoFix" Width="12" Height="12"
|
|
||||||
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="AI Title" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Visual character-count progress bar -->
|
|
||||||
<Grid Margin="0,5,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<!-- Track -->
|
|
||||||
<Border Grid.Column="0" Height="4" CornerRadius="2"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray8}"
|
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0">
|
|
||||||
<!-- Fill — width set in code-behind via TitleCountBar.Width -->
|
|
||||||
<Border x:Name="TitleCountBar" HorizontalAlignment="Left"
|
|
||||||
Height="4" CornerRadius="2" Width="0"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
</Border>
|
|
||||||
<TextBlock x:Name="TitleCount" Grid.Column="1"
|
|
||||||
Text="0 / 80" FontSize="10"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Category label row with required asterisk -->
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="Category" Margin="0,0,0,4"/>
|
|
||||||
<TextBlock Style="{StaticResource RequiredAsterisk}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<!-- Category search -->
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBox x:Name="CategoryBox"
|
|
||||||
mah:TextBoxHelper.Watermark="Start typing to search categories..."
|
|
||||||
TextChanged="CategoryBox_TextChanged"
|
|
||||||
KeyDown="CategoryBox_KeyDown"/>
|
|
||||||
<Border Grid.Column="1" Margin="8,0,0,0" CornerRadius="3"
|
|
||||||
Padding="6,2" VerticalAlignment="Center"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray8}">
|
|
||||||
<TextBlock x:Name="CategoryIdLabel" FontSize="11"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray4}"
|
|
||||||
Text="no category"/>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
<!-- Category dropdown suggestions -->
|
|
||||||
<ListBox x:Name="CategorySuggestionsList" MaxHeight="140"
|
|
||||||
SelectionChanged="CategorySuggestionsList_SelectionChanged"
|
|
||||||
Visibility="Collapsed" Margin="0,2,0,0"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray9}"
|
|
||||||
BorderBrush="{DynamicResource MahApps.Brushes.Gray7}"
|
|
||||||
BorderThickness="1">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TextBlock Text="{Binding CategoryPath}" FontSize="12" Padding="4,3"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<!-- Condition + Format -->
|
|
||||||
<Grid Margin="0,10,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="12"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="Condition"/>
|
|
||||||
<ComboBox x:Name="ConditionBox"
|
|
||||||
SelectionChanged="ConditionBox_SelectionChanged">
|
|
||||||
<ComboBoxItem Content="New" Tag="New"
|
|
||||||
ToolTip="Brand new, unopened (eBay: New)"/>
|
|
||||||
<ComboBoxItem Content="Open Box" Tag="OpenBox"
|
|
||||||
ToolTip="Opened but unused (eBay: Open box)"/>
|
|
||||||
<ComboBoxItem Content="Refurbished" Tag="Refurbished"
|
|
||||||
ToolTip="Professionally restored (eBay: Seller refurbished)"/>
|
|
||||||
<ComboBoxItem Content="Used" Tag="Used" IsSelected="True"
|
|
||||||
ToolTip="Previously used, working (eBay: Used)"/>
|
|
||||||
<ComboBoxItem Content="For Parts / Not Working" Tag="ForParts"
|
|
||||||
ToolTip="Not fully working (eBay: For parts or not working)"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2">
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="Format"/>
|
|
||||||
<ComboBox x:Name="FormatBox">
|
|
||||||
<ComboBoxItem Content="Buy It Now" IsSelected="True"/>
|
|
||||||
<ComboBoxItem Content="Auction"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- DESCRIPTION -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<Grid Margin="0,0,0,10">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="TextBox" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="DESCRIPTION" Style="{StaticResource SectionHeading}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<Button Grid.Column="1" Style="{StaticResource AiButton}"
|
|
||||||
Click="AiDescription_Click"
|
|
||||||
ToolTip="Ask Claude to write a full product description">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<mah:ProgressRing x:Name="DescSpinner"
|
|
||||||
Width="11" Height="11" Margin="0,0,4,0"
|
|
||||||
Foreground="White" Visibility="Collapsed"/>
|
|
||||||
<iconPacks:PackIconMaterial x:Name="DescAiIcon"
|
|
||||||
Kind="AutoFix" Width="12" Height="12"
|
|
||||||
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="AI Description" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
<TextBox x:Name="DescriptionBox" Height="150" AcceptsReturn="True"
|
|
||||||
TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"
|
|
||||||
mah:TextBoxHelper.Watermark="Describe the item, condition, what's included..."
|
|
||||||
Style="{DynamicResource MahApps.Styles.TextBox}"
|
|
||||||
TextChanged="DescriptionBox_TextChanged"/>
|
|
||||||
|
|
||||||
<!-- Description character count progress bar (soft limit 2000) -->
|
|
||||||
<Grid Margin="0,5,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Border Grid.Column="0" Height="4" CornerRadius="2"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray8}"
|
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0">
|
|
||||||
<Border x:Name="DescCountBar" HorizontalAlignment="Left"
|
|
||||||
Height="4" CornerRadius="2" Width="0"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
</Border>
|
|
||||||
<TextBlock x:Name="DescCount" Grid.Column="1"
|
|
||||||
Text="0 / 2000" FontSize="10"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- PRICING & LOGISTICS -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
|
||||||
<iconPacks:PackIconMaterial Kind="CurrencyGbp" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="PRICING & LOGISTICS" Style="{StaticResource SectionHeading}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="12"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="12"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Price with inline AI button -->
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Grid.Column="0" Style="{StaticResource FieldLabel}" Text="Price (£)"/>
|
|
||||||
<TextBlock Grid.Column="1" Style="{StaticResource RequiredAsterisk}"/>
|
|
||||||
<Button Grid.Column="3" Style="{StaticResource AiButton}"
|
|
||||||
Click="AiPrice_Click"
|
|
||||||
Height="22" Padding="6,0" FontSize="11"
|
|
||||||
Margin="4,0,0,4"
|
|
||||||
ToolTip="Ask Claude to suggest a competitive eBay UK price">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<mah:ProgressRing x:Name="PriceSpinner"
|
|
||||||
Width="9" Height="9" Margin="0,0,3,0"
|
|
||||||
Foreground="White" Visibility="Collapsed"/>
|
|
||||||
<iconPacks:PackIconMaterial x:Name="PriceAiIcon"
|
|
||||||
Kind="AutoFix" Width="10" Height="10"
|
|
||||||
Margin="0,0,3,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="AI Price" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
<mah:NumericUpDown x:Name="PriceBox" Minimum="0" Maximum="99999"
|
|
||||||
StringFormat="F2" Interval="0.5" Value="0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="2">
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="Quantity"/>
|
|
||||||
<mah:NumericUpDown x:Name="QuantityBox" Minimum="1" Maximum="999"
|
|
||||||
Value="1" StringFormat="0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="4">
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="Postage"/>
|
|
||||||
<ComboBox x:Name="PostageBox">
|
|
||||||
<ComboBoxItem Content="Royal Mail 1st Class (~£1.55)" IsSelected="True"/>
|
|
||||||
<ComboBoxItem Content="Royal Mail 2nd Class (~£1.20)"/>
|
|
||||||
<ComboBoxItem Content="Royal Mail Tracked 24 (~£2.90)"/>
|
|
||||||
<ComboBoxItem Content="Royal Mail Tracked 48 (~£2.60)"/>
|
|
||||||
<ComboBoxItem Content="Free Postage"/>
|
|
||||||
<ComboBoxItem Content="Collection Only"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Postcode — narrower input, left-aligned -->
|
|
||||||
<StackPanel Margin="0,10,0,0">
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="Item Postcode"/>
|
|
||||||
<TextBox x:Name="PostcodeBox"
|
|
||||||
mah:TextBoxHelper.Watermark="e.g. NR1 1AA"
|
|
||||||
MaxLength="10" Width="150"
|
|
||||||
HorizontalAlignment="Left"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- AI price suggestion result -->
|
|
||||||
<Border x:Name="PriceSuggestionPanel" Visibility="Collapsed"
|
|
||||||
CornerRadius="4" Margin="0,12,0,0" Padding="12,10"
|
|
||||||
Background="#1A7C3AED"
|
|
||||||
BorderBrush="#7C3AED" BorderThickness="1">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
|
||||||
<iconPacks:PackIconMaterial Kind="AutoFix" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="#7C3AED"/>
|
|
||||||
<TextBlock Text="AI Price Suggestion" FontWeight="Bold"
|
|
||||||
FontSize="12" Foreground="#7C3AED"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock x:Name="PriceSuggestionText" TextWrapping="Wrap"
|
|
||||||
FontSize="12" Margin="0,0,0,8"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray2}"/>
|
|
||||||
<Button Content="Use this price" HorizontalAlignment="Left"
|
|
||||||
Click="UseSuggestedPrice_Click"
|
|
||||||
Style="{StaticResource AiButton}"
|
|
||||||
Height="26" Padding="10,0" FontSize="11"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- ACTION BUTTONS -->
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,2,0,8">
|
|
||||||
<!-- Post: primary accent + spinner overlay -->
|
|
||||||
<Button x:Name="PostBtn" Style="{StaticResource PostButton}"
|
|
||||||
Click="PostListing_Click">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<mah:ProgressRing x:Name="PostSpinner"
|
|
||||||
Width="14" Height="14" Margin="0,0,6,0"
|
|
||||||
Foreground="White" Visibility="Collapsed"/>
|
|
||||||
<iconPacks:PackIconMaterial x:Name="PostIcon"
|
|
||||||
Kind="Send" Width="14" Height="14"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Post to eBay" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button x:Name="SaveDraftBtn" Content="Save Draft"
|
|
||||||
Margin="8,0,0,0" Click="SaveDraft_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="36" Padding="14,0" FontSize="13"/>
|
|
||||||
<Button x:Name="NewListingBtn" Margin="8,0,0,0" Click="NewListing_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="36" Padding="14,0" FontSize="13">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Refresh" Width="13" Height="13"
|
|
||||||
Margin="0,0,5,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Clear" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Posted success banner -->
|
|
||||||
<Border x:Name="SuccessPanel" Visibility="Collapsed"
|
|
||||||
CornerRadius="4" Padding="14,10"
|
|
||||||
Background="#1A4CAF50" BorderBrush="#4CAF50" BorderThickness="1">
|
|
||||||
<StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="CheckCircle" Width="16" Height="16"
|
|
||||||
Margin="0,0,8,0" VerticalAlignment="Center"
|
|
||||||
Foreground="#4CAF50"/>
|
|
||||||
<TextBlock Text="Posted! " FontWeight="Bold" VerticalAlignment="Center"
|
|
||||||
Foreground="#4CAF50"/>
|
|
||||||
<TextBlock x:Name="ListingUrlText" Foreground="{DynamicResource MahApps.Brushes.Accent}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Cursor="Hand" TextDecorations="Underline"
|
|
||||||
MouseLeftButtonUp="ListingUrl_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
|
||||||
<Button Content="Copy URL" Height="24" Padding="8,0" FontSize="11"
|
|
||||||
Click="CopyUrl_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
ToolTip="Copy listing URL to clipboard"/>
|
|
||||||
<Button Content="Copy Title" Height="24" Padding="8,0" FontSize="11"
|
|
||||||
Margin="6,0,0,0" Click="CopyTitle_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
ToolTip="Copy listing title to clipboard"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
|
||||||
RIGHT COLUMN — photos panel
|
|
||||||
================================================================ -->
|
|
||||||
<Grid Grid.Column="2">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Header: label + count badge -->
|
|
||||||
<Grid Grid.Row="0" Margin="0,0,0,8">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ImageMultiple" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock Text="PHOTOS" Style="{StaticResource SectionHeading}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<!-- Photo count badge -->
|
|
||||||
<Border Grid.Column="1" CornerRadius="10" Padding="8,2"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray8}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBlock x:Name="PhotoCountBadge" Text="0"
|
|
||||||
FontSize="12" FontWeight="Bold"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray2}"/>
|
|
||||||
<TextBlock Text=" / 12" FontSize="12"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Drop zone with dashed border, hover highlight -->
|
|
||||||
<Border Grid.Row="1"
|
|
||||||
x:Name="DropZone"
|
|
||||||
BorderBrush="{DynamicResource MahApps.Brushes.Gray7}"
|
|
||||||
BorderThickness="2" CornerRadius="4"
|
|
||||||
MinHeight="220"
|
|
||||||
AllowDrop="True" Drop="Photos_Drop" DragOver="Photos_DragOver"
|
|
||||||
DragEnter="DropZone_DragEnter" DragLeave="DropZone_DragLeave"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray10}">
|
|
||||||
<Border.Resources>
|
|
||||||
<!-- Dashed border via VisualBrush trickery is complex in WPF;
|
|
||||||
we use a solid thin border with hover accent colour instead -->
|
|
||||||
</Border.Resources>
|
|
||||||
<Grid>
|
|
||||||
<!-- Empty-state hint (hidden once photos added) -->
|
|
||||||
<StackPanel x:Name="DropHint" VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
IsHitTestVisible="False">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ImagePlus"
|
|
||||||
Width="40" Height="40"
|
|
||||||
Margin="0,0,0,8"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray7}"/>
|
|
||||||
<TextBlock Text="Drag & drop photos here"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
FontSize="12" HorizontalAlignment="Center"/>
|
|
||||||
<TextBlock Text="or use Browse below"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray7}"
|
|
||||||
FontSize="11" HorizontalAlignment="Center" Margin="0,3,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Thumbnails: each built in code-behind as a Grid with hover X overlay -->
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
|
||||||
<WrapPanel x:Name="PhotosPanel" Margin="6"/>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Browse / Clear actions -->
|
|
||||||
<Grid Grid.Row="2" Margin="0,8,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="8"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Button Grid.Column="0" Click="BrowsePhotos_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
|
||||||
Height="30">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="FolderImage" Width="13" Height="13"
|
|
||||||
Margin="0,0,5,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Browse..." VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Grid.Column="2" Click="ClearPhotos_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="30" Padding="10,0">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="TrashCanOutline" Width="13" Height="13"
|
|
||||||
Margin="0,0,5,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Clear" VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,694 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using EbayListingTool.Models;
|
|
||||||
using EbayListingTool.Services;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace EbayListingTool.Views;
|
|
||||||
|
|
||||||
public partial class SingleItemView : UserControl
|
|
||||||
{
|
|
||||||
private EbayListingService? _listingService;
|
|
||||||
private EbayCategoryService? _categoryService;
|
|
||||||
private AiAssistantService? _aiService;
|
|
||||||
private EbayAuthService? _auth;
|
|
||||||
|
|
||||||
private ListingDraft _draft = new();
|
|
||||||
private System.Threading.CancellationTokenSource? _categoryCts;
|
|
||||||
private bool _suppressCategoryLookup;
|
|
||||||
private string _suggestedPriceValue = "";
|
|
||||||
|
|
||||||
// Photo drag-reorder
|
|
||||||
private Point _dragStartPoint;
|
|
||||||
private bool _isDragging;
|
|
||||||
|
|
||||||
public SingleItemView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
PostcodeBox.TextChanged += (s, e) => _draft.Postcode = PostcodeBox.Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
// Re-run the count bar calculations now that the layout has rendered
|
|
||||||
// and the track Border has a non-zero ActualWidth.
|
|
||||||
TitleBox_TextChanged(this, null!);
|
|
||||||
DescriptionBox_TextChanged(this, null!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialise(EbayListingService listingService, EbayCategoryService categoryService,
|
|
||||||
AiAssistantService aiService, EbayAuthService auth)
|
|
||||||
{
|
|
||||||
_listingService = listingService;
|
|
||||||
_categoryService = categoryService;
|
|
||||||
_aiService = aiService;
|
|
||||||
_auth = auth;
|
|
||||||
|
|
||||||
PostcodeBox.Text = App.Configuration["Ebay:DefaultPostcode"] ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Pre-fills the form from a Photo Analysis result.</summary>
|
|
||||||
public async void PopulateFromAnalysis(PhotoAnalysisResult result, IReadOnlyList<string> imagePaths, decimal price)
|
|
||||||
{
|
|
||||||
// Q6: reset form directly — calling NewListing_Click shows a confirmation dialog which
|
|
||||||
// is unexpected when arriving here automatically from the Photo Analysis tab.
|
|
||||||
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
|
|
||||||
TitleBox.Text = "";
|
|
||||||
DescriptionBox.Text = "";
|
|
||||||
CategoryBox.Text = "";
|
|
||||||
CategoryIdLabel.Text = "(no category)";
|
|
||||||
PriceBox.Value = 0;
|
|
||||||
QuantityBox.Value = 1;
|
|
||||||
ConditionBox.SelectedIndex = 3; // Used
|
|
||||||
FormatBox.SelectedIndex = 0;
|
|
||||||
PhotosPanel.Children.Clear();
|
|
||||||
UpdatePhotoPanel();
|
|
||||||
SuccessPanel.Visibility = Visibility.Collapsed;
|
|
||||||
PriceSuggestionPanel.Visibility = Visibility.Collapsed;
|
|
||||||
|
|
||||||
TitleBox.Text = result.Title;
|
|
||||||
DescriptionBox.Text = result.Description;
|
|
||||||
PriceBox.Value = (double)price;
|
|
||||||
|
|
||||||
// Auto-fill the top eBay category from the analysis keyword; user can override
|
|
||||||
await AutoFillCategoryAsync(result.CategoryKeyword);
|
|
||||||
|
|
||||||
// Q1: load all photos from analysis
|
|
||||||
var validPaths = imagePaths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToArray();
|
|
||||||
if (validPaths.Length > 0)
|
|
||||||
AddPhotos(validPaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Title ----
|
|
||||||
|
|
||||||
private void TitleBox_TextChanged(object sender, TextChangedEventArgs e)
|
|
||||||
{
|
|
||||||
_draft.Title = TitleBox.Text;
|
|
||||||
var len = TitleBox.Text.Length;
|
|
||||||
TitleCount.Text = $"{len} / 80";
|
|
||||||
|
|
||||||
var overLimit = len > 75;
|
|
||||||
TitleCount.Foreground = overLimit
|
|
||||||
? System.Windows.Media.Brushes.OrangeRed
|
|
||||||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
|
||||||
|
|
||||||
// Update the progress bar fill width proportionally
|
|
||||||
var trackBorder = TitleCountBar.Parent as Border;
|
|
||||||
double trackWidth = trackBorder?.ActualWidth ?? 0;
|
|
||||||
if (trackWidth > 0)
|
|
||||||
TitleCountBar.Width = trackWidth * (len / 80.0);
|
|
||||||
|
|
||||||
TitleCountBar.Background = overLimit
|
|
||||||
? System.Windows.Media.Brushes.OrangeRed
|
|
||||||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void AiTitle_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_aiService == null) return;
|
|
||||||
var condition = GetSelectedCondition().ToString();
|
|
||||||
var current = TitleBox.Text;
|
|
||||||
|
|
||||||
SetTitleSpinner(true);
|
|
||||||
SetBusy(true, "Generating title...");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var title = await _aiService.GenerateTitleAsync(current, condition);
|
|
||||||
TitleBox.Text = title.Trim().TrimEnd('.').Trim('"');
|
|
||||||
|
|
||||||
// Auto-fill category from the generated title if not already set
|
|
||||||
if (string.IsNullOrWhiteSpace(_draft.CategoryId))
|
|
||||||
await AutoFillCategoryAsync(TitleBox.Text);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ShowError("AI Title", ex.Message);
|
|
||||||
}
|
|
||||||
finally { SetBusy(false); SetTitleSpinner(false); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Category ----
|
|
||||||
|
|
||||||
private async void CategoryBox_TextChanged(object sender, TextChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_suppressCategoryLookup) return;
|
|
||||||
|
|
||||||
_categoryCts?.Cancel();
|
|
||||||
_categoryCts?.Dispose();
|
|
||||||
_categoryCts = new System.Threading.CancellationTokenSource();
|
|
||||||
var cts = _categoryCts;
|
|
||||||
|
|
||||||
if (CategoryBox.Text.Length < 3)
|
|
||||||
{
|
|
||||||
CategorySuggestionsList.Visibility = Visibility.Collapsed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(400, cts.Token);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cts.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var suggestions = await _categoryService!.GetCategorySuggestionsAsync(CategoryBox.Text);
|
|
||||||
if (cts.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
CategorySuggestionsList.ItemsSource = suggestions;
|
|
||||||
CategorySuggestionsList.Visibility = suggestions.Count > 0
|
|
||||||
? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) { /* superseded by newer keystroke */ }
|
|
||||||
catch { /* ignore transient network errors */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DescriptionBox_TextChanged(object sender, TextChangedEventArgs e)
|
|
||||||
{
|
|
||||||
_draft.Description = DescriptionBox.Text;
|
|
||||||
var len = DescriptionBox.Text.Length;
|
|
||||||
var softCap = 2000;
|
|
||||||
DescCount.Text = $"{len} / {softCap}";
|
|
||||||
|
|
||||||
var overLimit = len > softCap;
|
|
||||||
DescCount.Foreground = overLimit
|
|
||||||
? System.Windows.Media.Brushes.OrangeRed
|
|
||||||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
|
||||||
|
|
||||||
var trackBorder = DescCountBar.Parent as Border;
|
|
||||||
double trackWidth = trackBorder?.ActualWidth ?? 0;
|
|
||||||
if (trackWidth > 0)
|
|
||||||
DescCountBar.Width = Math.Min(trackWidth, trackWidth * (len / (double)softCap));
|
|
||||||
|
|
||||||
DescCountBar.Background = overLimit
|
|
||||||
? System.Windows.Media.Brushes.OrangeRed
|
|
||||||
: new System.Windows.Media.SolidColorBrush(
|
|
||||||
System.Windows.Media.Color.FromRgb(0xF5, 0x9E, 0x0B)); // amber
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CategoryBox_KeyDown(object sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Key == Key.Escape)
|
|
||||||
{
|
|
||||||
CategorySuggestionsList.Visibility = Visibility.Collapsed;
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CategorySuggestionsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (CategorySuggestionsList.SelectedItem is CategorySuggestion cat)
|
|
||||||
{
|
|
||||||
_draft.CategoryId = cat.CategoryId;
|
|
||||||
_draft.CategoryName = cat.CategoryName;
|
|
||||||
CategoryBox.Text = cat.CategoryName;
|
|
||||||
CategoryIdLabel.Text = $"ID: {cat.CategoryId}";
|
|
||||||
CategorySuggestionsList.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches the top eBay category suggestion for <paramref name="keyword"/> and auto-fills
|
|
||||||
/// the category fields. The suggestions list is shown so the user can override.
|
|
||||||
/// </summary>
|
|
||||||
private async Task AutoFillCategoryAsync(string keyword)
|
|
||||||
{
|
|
||||||
if (_categoryService == null || string.IsNullOrWhiteSpace(keyword)) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var suggestions = await _categoryService.GetCategorySuggestionsAsync(keyword);
|
|
||||||
if (suggestions.Count == 0) return;
|
|
||||||
|
|
||||||
var top = suggestions[0];
|
|
||||||
_suppressCategoryLookup = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_draft.CategoryId = top.CategoryId;
|
|
||||||
_draft.CategoryName = top.CategoryName;
|
|
||||||
CategoryBox.Text = top.CategoryName;
|
|
||||||
CategoryIdLabel.Text = $"ID: {top.CategoryId}";
|
|
||||||
}
|
|
||||||
finally { _suppressCategoryLookup = false; }
|
|
||||||
|
|
||||||
// Show the full list so user can see alternatives and override
|
|
||||||
CategorySuggestionsList.ItemsSource = suggestions;
|
|
||||||
CategorySuggestionsList.Visibility = suggestions.Count > 1
|
|
||||||
? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
catch { /* non-critical — leave category blank if lookup fails */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Condition ----
|
|
||||||
|
|
||||||
private void ConditionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
_draft.Condition = GetSelectedCondition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ItemCondition GetSelectedCondition()
|
|
||||||
{
|
|
||||||
var tag = (ConditionBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Used";
|
|
||||||
return tag switch
|
|
||||||
{
|
|
||||||
"New" => ItemCondition.New,
|
|
||||||
"OpenBox" => ItemCondition.OpenBox,
|
|
||||||
"Refurbished" => ItemCondition.Refurbished,
|
|
||||||
"ForParts" => ItemCondition.ForPartsOrNotWorking,
|
|
||||||
_ => ItemCondition.Used
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Description ----
|
|
||||||
|
|
||||||
private async void AiDescription_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_aiService == null) return;
|
|
||||||
SetDescSpinner(true);
|
|
||||||
SetBusy(true, "Writing description...");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var description = await _aiService.WriteDescriptionAsync(
|
|
||||||
TitleBox.Text, GetSelectedCondition().ToString(), DescriptionBox.Text);
|
|
||||||
DescriptionBox.Text = description;
|
|
||||||
}
|
|
||||||
catch (Exception ex) { ShowError("AI Description", ex.Message); }
|
|
||||||
finally { SetBusy(false); SetDescSpinner(false); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Price ----
|
|
||||||
|
|
||||||
private async void AiPrice_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_aiService == null) return;
|
|
||||||
SetPriceSpinner(true);
|
|
||||||
SetBusy(true, "Researching price...");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _aiService.SuggestPriceAsync(
|
|
||||||
TitleBox.Text, GetSelectedCondition().ToString());
|
|
||||||
PriceSuggestionText.Text = result;
|
|
||||||
|
|
||||||
// Extract price line for "Use this price"
|
|
||||||
var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var priceLine = lines.FirstOrDefault(l => l.StartsWith("PRICE:", StringComparison.OrdinalIgnoreCase));
|
|
||||||
_suggestedPriceValue = priceLine?.Replace("PRICE:", "", StringComparison.OrdinalIgnoreCase).Trim() ?? "";
|
|
||||||
|
|
||||||
PriceSuggestionPanel.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
catch (Exception ex) { ShowError("AI Price", ex.Message); }
|
|
||||||
finally { SetBusy(false); SetPriceSpinner(false); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UseSuggestedPrice_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (decimal.TryParse(_suggestedPriceValue, out var price))
|
|
||||||
PriceBox.Value = (double)price;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Photos ----
|
|
||||||
|
|
||||||
private void Photos_DragOver(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
|
|
||||||
? DragDropEffects.Copy : DragDropEffects.None;
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Photos_Drop(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
|
||||||
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
|
||||||
// Remove highlight when drop completes
|
|
||||||
DropZone.BorderBrush = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray7");
|
|
||||||
DropZone.Background = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray10");
|
|
||||||
AddPhotos(files);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DropZone_DragEnter(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
|
||||||
DropZone.BorderBrush = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent");
|
|
||||||
DropZone.Background = new System.Windows.Media.SolidColorBrush(
|
|
||||||
System.Windows.Media.Color.FromArgb(20, 0x5C, 0x6B, 0xC0)); // subtle indigo tint
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DropZone_DragLeave(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
DropZone.BorderBrush = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray7");
|
|
||||||
DropZone.Background = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray10");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BrowsePhotos_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var dlg = new OpenFileDialog
|
|
||||||
{
|
|
||||||
Title = "Select photos",
|
|
||||||
Filter = "Images|*.jpg;*.jpeg;*.png;*.gif;*.bmp|All files|*.*",
|
|
||||||
Multiselect = true
|
|
||||||
};
|
|
||||||
if (dlg.ShowDialog() == true)
|
|
||||||
AddPhotos(dlg.FileNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPhotos(string[] paths)
|
|
||||||
{
|
|
||||||
var imageExts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{ ".jpg", ".jpeg", ".png", ".gif", ".bmp" };
|
|
||||||
|
|
||||||
foreach (var path in paths)
|
|
||||||
{
|
|
||||||
if (!imageExts.Contains(Path.GetExtension(path))) continue;
|
|
||||||
if (_draft.PhotoPaths.Count >= 12) break;
|
|
||||||
if (_draft.PhotoPaths.Contains(path)) continue;
|
|
||||||
_draft.PhotoPaths.Add(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
RebuildPhotoThumbnails();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears and recreates all photo thumbnails from <see cref="ListingDraft.PhotoPaths"/>.
|
|
||||||
/// Called after any add, remove, or reorder operation so the panel always matches the list.
|
|
||||||
/// </summary>
|
|
||||||
private void RebuildPhotoThumbnails()
|
|
||||||
{
|
|
||||||
PhotosPanel.Children.Clear();
|
|
||||||
for (int i = 0; i < _draft.PhotoPaths.Count; i++)
|
|
||||||
AddPhotoThumbnail(_draft.PhotoPaths[i], i);
|
|
||||||
UpdatePhotoPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPhotoThumbnail(string path, int index)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bmp = new BitmapImage();
|
|
||||||
bmp.BeginInit();
|
|
||||||
bmp.UriSource = new Uri(path, UriKind.Absolute);
|
|
||||||
bmp.DecodePixelWidth = 128;
|
|
||||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
|
||||||
bmp.EndInit();
|
|
||||||
bmp.Freeze();
|
|
||||||
|
|
||||||
var img = new System.Windows.Controls.Image
|
|
||||||
{
|
|
||||||
Width = 72, Height = 72,
|
|
||||||
Stretch = System.Windows.Media.Stretch.UniformToFill,
|
|
||||||
Source = bmp,
|
|
||||||
ToolTip = Path.GetFileName(path)
|
|
||||||
};
|
|
||||||
img.Clip = new System.Windows.Media.RectangleGeometry(new Rect(0, 0, 72, 72), 4, 4);
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
var removeBtn = new Button
|
|
||||||
{
|
|
||||||
Width = 18, Height = 18,
|
|
||||||
Cursor = Cursors.Hand,
|
|
||||||
ToolTip = "Remove photo",
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
|
||||||
Margin = new Thickness(0, 2, 2, 0),
|
|
||||||
Padding = new Thickness(0),
|
|
||||||
Background = new System.Windows.Media.SolidColorBrush(
|
|
||||||
System.Windows.Media.Color.FromArgb(200, 30, 30, 30)),
|
|
||||||
Foreground = System.Windows.Media.Brushes.White,
|
|
||||||
BorderThickness = new Thickness(0),
|
|
||||||
FontSize = 11, FontWeight = FontWeights.Bold,
|
|
||||||
Content = "✕",
|
|
||||||
Opacity = 0
|
|
||||||
};
|
|
||||||
removeBtn.Click += (s, e) =>
|
|
||||||
{
|
|
||||||
e.Handled = true; // don't bubble and trigger drag
|
|
||||||
_draft.PhotoPaths.Remove(path);
|
|
||||||
RebuildPhotoThumbnails();
|
|
||||||
};
|
|
||||||
|
|
||||||
// "Cover" badge on the first photo — it becomes the eBay gallery hero image
|
|
||||||
Border? coverBadge = null;
|
|
||||||
if (index == 0)
|
|
||||||
{
|
|
||||||
coverBadge = new Border
|
|
||||||
{
|
|
||||||
CornerRadius = new CornerRadius(3),
|
|
||||||
Background = new System.Windows.Media.SolidColorBrush(
|
|
||||||
System.Windows.Media.Color.FromArgb(210, 60, 90, 200)),
|
|
||||||
Padding = new Thickness(3, 1, 3, 1),
|
|
||||||
Margin = new Thickness(2, 2, 0, 0),
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Left,
|
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
|
||||||
IsHitTestVisible = false, // don't block drag
|
|
||||||
Child = new TextBlock
|
|
||||||
{
|
|
||||||
Text = "Cover",
|
|
||||||
FontSize = 8,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = System.Windows.Media.Brushes.White
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var container = new Grid
|
|
||||||
{
|
|
||||||
Width = 72, Height = 72,
|
|
||||||
Margin = new Thickness(4),
|
|
||||||
Cursor = Cursors.SizeAll, // signal draggability
|
|
||||||
AllowDrop = true,
|
|
||||||
Tag = path // stable identifier used by drop handler
|
|
||||||
};
|
|
||||||
container.Children.Add(img);
|
|
||||||
if (coverBadge != null) container.Children.Add(coverBadge);
|
|
||||||
container.Children.Add(removeBtn);
|
|
||||||
|
|
||||||
// Hover: reveal remove button
|
|
||||||
container.MouseEnter += (s, e) => removeBtn.Opacity = 1;
|
|
||||||
container.MouseLeave += (s, e) => removeBtn.Opacity = 0;
|
|
||||||
|
|
||||||
// Drag initiation
|
|
||||||
container.MouseLeftButtonDown += (s, e) =>
|
|
||||||
{
|
|
||||||
_dragStartPoint = e.GetPosition(null);
|
|
||||||
};
|
|
||||||
container.MouseMove += (s, e) =>
|
|
||||||
{
|
|
||||||
if (e.LeftButton != MouseButtonState.Pressed || _isDragging) return;
|
|
||||||
var pos = e.GetPosition(null);
|
|
||||||
if (Math.Abs(pos.X - _dragStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
|
|
||||||
Math.Abs(pos.Y - _dragStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
|
|
||||||
{
|
|
||||||
_isDragging = true;
|
|
||||||
DragDrop.DoDragDrop(container, path, DragDropEffects.Move);
|
|
||||||
_isDragging = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Drop target
|
|
||||||
container.DragOver += (s, e) =>
|
|
||||||
{
|
|
||||||
if (e.Data.GetDataPresent(typeof(string)) &&
|
|
||||||
(string)e.Data.GetData(typeof(string)) != path)
|
|
||||||
{
|
|
||||||
e.Effects = DragDropEffects.Move;
|
|
||||||
container.Opacity = 0.45; // dim to signal insertion point
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
e.Effects = DragDropEffects.None;
|
|
||||||
}
|
|
||||||
e.Handled = true;
|
|
||||||
};
|
|
||||||
container.DragLeave += (s, e) => container.Opacity = 1.0;
|
|
||||||
container.Drop += (s, e) =>
|
|
||||||
{
|
|
||||||
container.Opacity = 1.0;
|
|
||||||
if (!e.Data.GetDataPresent(typeof(string))) return;
|
|
||||||
|
|
||||||
var sourcePath = (string)e.Data.GetData(typeof(string));
|
|
||||||
var targetPath = (string)container.Tag;
|
|
||||||
if (sourcePath == targetPath) return;
|
|
||||||
|
|
||||||
var sourceIdx = _draft.PhotoPaths.IndexOf(sourcePath);
|
|
||||||
var targetIdx = _draft.PhotoPaths.IndexOf(targetPath);
|
|
||||||
if (sourceIdx < 0 || targetIdx < 0) return;
|
|
||||||
|
|
||||||
_draft.PhotoPaths.RemoveAt(sourceIdx);
|
|
||||||
_draft.PhotoPaths.Insert(targetIdx, sourcePath);
|
|
||||||
|
|
||||||
RebuildPhotoThumbnails();
|
|
||||||
e.Handled = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
PhotosPanel.Children.Add(container);
|
|
||||||
}
|
|
||||||
catch { /* skip unreadable files */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePhotoPanel()
|
|
||||||
{
|
|
||||||
var count = _draft.PhotoPaths.Count;
|
|
||||||
DropHint.Visibility = count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
PhotoCountBadge.Text = count.ToString();
|
|
||||||
// Tint the badge red when at the limit
|
|
||||||
PhotoCountBadge.Foreground = count >= 12
|
|
||||||
? System.Windows.Media.Brushes.OrangeRed
|
|
||||||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray2");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearPhotos_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
_draft.PhotoPaths.Clear();
|
|
||||||
RebuildPhotoThumbnails();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Post / Save ----
|
|
||||||
|
|
||||||
private async void PostListing_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!ValidateDraft()) return;
|
|
||||||
|
|
||||||
_draft.Title = TitleBox.Text.Trim();
|
|
||||||
_draft.Description = DescriptionBox.Text.Trim();
|
|
||||||
_draft.Price = (decimal)(PriceBox.Value ?? 0);
|
|
||||||
_draft.Quantity = (int)(QuantityBox.Value ?? 1);
|
|
||||||
_draft.Condition = GetSelectedCondition();
|
|
||||||
_draft.Format = FormatBox.SelectedIndex == 0 ? ListingFormat.FixedPrice : ListingFormat.Auction;
|
|
||||||
_draft.Postcode = PostcodeBox.Text;
|
|
||||||
|
|
||||||
SetPostSpinner(true);
|
|
||||||
SetBusy(true, "Posting to eBay...");
|
|
||||||
PostBtn.IsEnabled = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var url = await _listingService!.PostListingAsync(_draft);
|
|
||||||
ListingUrlText.Text = url;
|
|
||||||
SuccessPanel.Visibility = Visibility.Visible;
|
|
||||||
GetWindow()?.SetStatus($"Listed: {_draft.Title}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ShowError("Post Failed", ex.Message);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
SetBusy(false);
|
|
||||||
SetPostSpinner(false);
|
|
||||||
PostBtn.IsEnabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ListingUrl_Click(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_draft.EbayListingUrl))
|
|
||||||
Process.Start(new ProcessStartInfo(_draft.EbayListingUrl) { UseShellExecute = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyUrl_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var url = ListingUrlText.Text;
|
|
||||||
if (!string.IsNullOrEmpty(url))
|
|
||||||
System.Windows.Clipboard.SetText(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyTitle_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_draft.Title))
|
|
||||||
System.Windows.Clipboard.SetText(_draft.Title);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveDraft_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
// Drafts: future feature — for now just confirm save
|
|
||||||
MessageBox.Show("Draft saved (local save to be implemented in a future update).",
|
|
||||||
"Save Draft", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NewListing_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(TitleBox.Text))
|
|
||||||
{
|
|
||||||
var result = MessageBox.Show(
|
|
||||||
"Start a new listing? Current details will be lost.",
|
|
||||||
"New Listing",
|
|
||||||
MessageBoxButton.OKCancel,
|
|
||||||
MessageBoxImage.Question);
|
|
||||||
if (result != MessageBoxResult.OK) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
|
|
||||||
TitleBox.Text = "";
|
|
||||||
DescriptionBox.Text = "";
|
|
||||||
CategoryBox.Text = "";
|
|
||||||
CategoryIdLabel.Text = "(no category)";
|
|
||||||
PriceBox.Value = 0;
|
|
||||||
QuantityBox.Value = 1;
|
|
||||||
ConditionBox.SelectedIndex = 3; // Used
|
|
||||||
FormatBox.SelectedIndex = 0;
|
|
||||||
PhotosPanel.Children.Clear();
|
|
||||||
UpdatePhotoPanel();
|
|
||||||
SuccessPanel.Visibility = Visibility.Collapsed;
|
|
||||||
PriceSuggestionPanel.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Helpers ----
|
|
||||||
|
|
||||||
private bool ValidateDraft()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(TitleBox.Text))
|
|
||||||
{ ShowError("Validation", "Please enter a title."); return false; }
|
|
||||||
if (TitleBox.Text.Length > 80)
|
|
||||||
{ ShowError("Validation", "Title must be 80 characters or fewer."); return false; }
|
|
||||||
if (string.IsNullOrEmpty(_draft.CategoryId))
|
|
||||||
{ ShowError("Validation", "Please select a category."); return false; }
|
|
||||||
if ((PriceBox.Value ?? 0) <= 0)
|
|
||||||
{ ShowError("Validation", "Please enter a price greater than zero."); return false; }
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetBusy(bool busy, string message = "")
|
|
||||||
{
|
|
||||||
IsEnabled = !busy;
|
|
||||||
GetWindow()?.SetStatus(busy ? message : "Ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPostSpinner(bool spinning)
|
|
||||||
{
|
|
||||||
PostSpinner.Visibility = spinning ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
PostIcon.Visibility = spinning ? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetTitleSpinner(bool spinning)
|
|
||||||
{
|
|
||||||
TitleSpinner.Visibility = spinning ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
TitleAiIcon.Visibility = spinning ? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetDescSpinner(bool spinning)
|
|
||||||
{
|
|
||||||
DescSpinner.Visibility = spinning ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
DescAiIcon.Visibility = spinning ? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPriceSpinner(bool spinning)
|
|
||||||
{
|
|
||||||
PriceSpinner.Visibility = spinning ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
PriceAiIcon.Visibility = spinning ? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowError(string title, string message)
|
|
||||||
=> MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
|
|
||||||
private MainWindow? GetWindow() => Window.GetWindow(this) as MainWindow;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user