Add layered price lookup and category auto-fill
- PriceLookupService: eBay live data → saved listing history → AI estimate, each result labelled by source so the user knows how reliable it is - Revalue row: new "Check eBay" button fetches suggestion and pre-populates the price field; shows source label beneath (or "No suggestion available") - Category auto-fill: AutoFillCategoryAsync takes the top eBay category suggestion and fills the field automatically after photo analysis or AI title generation; dropdown stays visible so user can override Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
83
EbayListingTool/Services/PriceLookupService.cs
Normal file
83
EbayListingTool/Services/PriceLookupService.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using EbayListingTool.Models;
|
||||||
|
|
||||||
|
namespace EbayListingTool.Services;
|
||||||
|
|
||||||
|
public record PriceSuggestion(decimal Price, string Source, string Label);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Layered price suggestion: eBay live data → own listing history → AI estimate.
|
||||||
|
/// Returns the first source that produces a result, labelled so the UI can show
|
||||||
|
/// where the suggestion came from.
|
||||||
|
/// </summary>
|
||||||
|
public class PriceLookupService
|
||||||
|
{
|
||||||
|
private readonly EbayPriceResearchService _ebay;
|
||||||
|
private readonly SavedListingsService _history;
|
||||||
|
private readonly AiAssistantService _ai;
|
||||||
|
|
||||||
|
private static readonly Regex PriceRegex =
|
||||||
|
new(@"PRICE:\s*(\d+\.?\d*)", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public PriceLookupService(
|
||||||
|
EbayPriceResearchService ebay,
|
||||||
|
SavedListingsService history,
|
||||||
|
AiAssistantService ai)
|
||||||
|
{
|
||||||
|
_ebay = ebay;
|
||||||
|
_history = history;
|
||||||
|
_ai = ai;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PriceSuggestion?> GetSuggestionAsync(SavedListing listing)
|
||||||
|
{
|
||||||
|
// 1. eBay live listings
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _ebay.GetLivePricesAsync(listing.Title);
|
||||||
|
if (result.HasSuggestion)
|
||||||
|
return new PriceSuggestion(
|
||||||
|
result.Suggested,
|
||||||
|
"ebay",
|
||||||
|
$"eBay suggests £{result.Suggested:F2} (from {result.Count} listings)");
|
||||||
|
}
|
||||||
|
catch { /* eBay unavailable — fall through */ }
|
||||||
|
|
||||||
|
// 2. Own saved listing history — same category, at least 2 data points
|
||||||
|
var sameCat = _history.Listings
|
||||||
|
.Where(l => l.Id != listing.Id
|
||||||
|
&& !string.IsNullOrWhiteSpace(l.Category)
|
||||||
|
&& l.Category.Equals(listing.Category, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& l.Price > 0)
|
||||||
|
.Select(l => l.Price)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (sameCat.Count >= 2)
|
||||||
|
{
|
||||||
|
var avg = Math.Round(sameCat.Average(), 2);
|
||||||
|
return new PriceSuggestion(
|
||||||
|
avg,
|
||||||
|
"history",
|
||||||
|
$"Your avg for {listing.Category}: £{avg:F2} ({sameCat.Count} listings)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. AI estimate
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _ai.SuggestPriceAsync(listing.Title, listing.ConditionNotes);
|
||||||
|
var match = PriceRegex.Match(response);
|
||||||
|
if (match.Success
|
||||||
|
&& decimal.TryParse(match.Groups[1].Value,
|
||||||
|
System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var price)
|
||||||
|
&& price > 0)
|
||||||
|
{
|
||||||
|
return new PriceSuggestion(price, "ai", $"AI estimate: £{price:F2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* AI unavailable */ }
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public partial class MainWindow : MetroWindow
|
|||||||
private readonly BulkImportService _bulkService;
|
private readonly BulkImportService _bulkService;
|
||||||
private readonly SavedListingsService _savedService;
|
private readonly SavedListingsService _savedService;
|
||||||
private readonly EbayPriceResearchService _priceService;
|
private readonly EbayPriceResearchService _priceService;
|
||||||
|
private readonly PriceLookupService _priceLookupService;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -28,13 +29,14 @@ public partial class MainWindow : MetroWindow
|
|||||||
_bulkService = new BulkImportService();
|
_bulkService = new BulkImportService();
|
||||||
_savedService = new SavedListingsService();
|
_savedService = new SavedListingsService();
|
||||||
_priceService = new EbayPriceResearchService(_auth);
|
_priceService = new EbayPriceResearchService(_auth);
|
||||||
|
_priceLookupService = new PriceLookupService(_priceService, _savedService, _aiService);
|
||||||
|
|
||||||
// Photo Analysis tab — no eBay needed
|
// Photo Analysis tab — no eBay needed
|
||||||
PhotoView.Initialise(_aiService, _savedService, _priceService);
|
PhotoView.Initialise(_aiService, _savedService, _priceService);
|
||||||
PhotoView.UseDetailsRequested += OnUseDetailsRequested;
|
PhotoView.UseDetailsRequested += OnUseDetailsRequested;
|
||||||
|
|
||||||
// Saved Listings tab
|
// Saved Listings tab
|
||||||
SavedView.Initialise(_savedService);
|
SavedView.Initialise(_savedService, _priceLookupService);
|
||||||
|
|
||||||
// New Listing + Bulk tabs
|
// New Listing + Bulk tabs
|
||||||
SingleView.Initialise(_listingService, _categoryService, _aiService, _auth);
|
SingleView.Initialise(_listingService, _categoryService, _aiService, _auth);
|
||||||
|
|||||||
@@ -205,13 +205,69 @@
|
|||||||
<TextBlock x:Name="DetailTitle" Grid.Column="0"
|
<TextBlock x:Name="DetailTitle" Grid.Column="0"
|
||||||
FontSize="17" FontWeight="Bold" TextWrapping="Wrap"
|
FontSize="17" FontWeight="Bold" TextWrapping="Wrap"
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray1}"/>
|
Foreground="{DynamicResource MahApps.Brushes.Gray1}"/>
|
||||||
<Border Grid.Column="1"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Accent}"
|
<!-- Price display + quick revalue -->
|
||||||
CornerRadius="6" Padding="10,4" Margin="10,0,0,0"
|
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Top">
|
||||||
VerticalAlignment="Top">
|
|
||||||
|
<!-- Normal price badge + Revalue button -->
|
||||||
|
<StackPanel x:Name="PriceDisplayRow" Orientation="Horizontal">
|
||||||
|
<Border Background="{DynamicResource MahApps.Brushes.Accent}"
|
||||||
|
CornerRadius="6" Padding="10,4">
|
||||||
<TextBlock x:Name="DetailPrice"
|
<TextBlock x:Name="DetailPrice"
|
||||||
FontSize="16" FontWeight="Bold" Foreground="White"/>
|
FontSize="16" FontWeight="Bold" Foreground="White"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
<Button x:Name="RevalueBtn" Click="RevalueBtn_Click"
|
||||||
|
Height="28" Padding="8,0" Margin="6,0,0,0"
|
||||||
|
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
||||||
|
ToolTip="Quick-change the price">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial Kind="CurrencyGbp" Width="11" Height="11"
|
||||||
|
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Revalue" FontSize="11" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Inline revalue editor (hidden by default) -->
|
||||||
|
<StackPanel x:Name="RevalueRow" Orientation="Vertical"
|
||||||
|
Visibility="Collapsed" Margin="0,4,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<mah:NumericUpDown x:Name="RevaluePrice"
|
||||||
|
Minimum="0" Maximum="99999"
|
||||||
|
StringFormat="F2" Interval="0.5"
|
||||||
|
Width="110" Height="30"/>
|
||||||
|
<Button x:Name="CheckEbayBtn" Click="CheckEbayBtn_Click"
|
||||||
|
Height="30" Padding="8,0" Margin="6,0,4,0"
|
||||||
|
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
||||||
|
ToolTip="Check eBay for a suggested price">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="CheckEbayIcon"
|
||||||
|
Kind="Magnify" Width="11" Height="11"
|
||||||
|
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="CheckEbayText" Text="Check eBay"
|
||||||
|
FontSize="11" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Click="RevalueSave_Click"
|
||||||
|
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
||||||
|
Height="30" Padding="10,0" Margin="0,0,4,0"
|
||||||
|
ToolTip="Save new price">
|
||||||
|
<iconPacks:PackIconMaterial Kind="Check" Width="13" Height="13"/>
|
||||||
|
</Button>
|
||||||
|
<Button Click="RevalueCancel_Click"
|
||||||
|
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
||||||
|
Height="30" Padding="8,0"
|
||||||
|
ToolTip="Cancel">
|
||||||
|
<iconPacks:PackIconMaterial Kind="Close" Width="11" Height="11"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="PriceSuggestionLabel"
|
||||||
|
FontSize="10" Margin="0,5,0,0"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Visibility="Collapsed" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Meta row: category · date -->
|
<!-- Meta row: category · date -->
|
||||||
@@ -362,6 +418,9 @@
|
|||||||
|
|
||||||
<!-- Photos -->
|
<!-- Photos -->
|
||||||
<TextBlock Text="PHOTOS" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
<TextBlock Text="PHOTOS" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
|
||||||
|
<TextBlock Text="First photo is the listing cover. Use ◀ ▶ to reorder."
|
||||||
|
FontSize="10" Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
||||||
VerticalScrollBarVisibility="Disabled"
|
VerticalScrollBarVisibility="Disabled"
|
||||||
Margin="0,0,0,10">
|
Margin="0,0,0,10">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace EbayListingTool.Views;
|
|||||||
public partial class SavedListingsView : UserControl
|
public partial class SavedListingsView : UserControl
|
||||||
{
|
{
|
||||||
private SavedListingsService? _service;
|
private SavedListingsService? _service;
|
||||||
|
private PriceLookupService? _priceLookup;
|
||||||
private SavedListing? _selected;
|
private SavedListing? _selected;
|
||||||
|
|
||||||
// Edit mode working state
|
// Edit mode working state
|
||||||
@@ -33,9 +34,10 @@ public partial class SavedListingsView : UserControl
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialise(SavedListingsService service)
|
public void Initialise(SavedListingsService service, PriceLookupService? priceLookup = null)
|
||||||
{
|
{
|
||||||
_service = service;
|
_service = service;
|
||||||
|
_priceLookup = priceLookup;
|
||||||
RefreshList();
|
RefreshList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +257,10 @@ public partial class SavedListingsView : UserControl
|
|||||||
EmptyDetail.Visibility = Visibility.Collapsed;
|
EmptyDetail.Visibility = Visibility.Collapsed;
|
||||||
DetailPanel.Visibility = Visibility.Visible;
|
DetailPanel.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
// Reset revalue UI
|
||||||
|
PriceDisplayRow.Visibility = Visibility.Visible;
|
||||||
|
RevalueRow.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
DetailTitle.Text = listing.Title;
|
DetailTitle.Text = listing.Title;
|
||||||
DetailPrice.Text = listing.PriceDisplay;
|
DetailPrice.Text = listing.PriceDisplay;
|
||||||
DetailCategory.Text = listing.Category;
|
DetailCategory.Text = listing.Category;
|
||||||
@@ -332,6 +338,77 @@ public partial class SavedListingsView : UserControl
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Quick revalue ----
|
||||||
|
|
||||||
|
private void RevalueBtn_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
RevaluePrice.Value = (double)_selected.Price;
|
||||||
|
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
|
||||||
|
PriceSuggestionLabel.Text = "";
|
||||||
|
CheckEbayBtn.IsEnabled = _priceLookup != null;
|
||||||
|
PriceDisplayRow.Visibility = Visibility.Collapsed;
|
||||||
|
RevalueRow.Visibility = Visibility.Visible;
|
||||||
|
RevaluePrice.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RevalueSave_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null || _service == null) return;
|
||||||
|
_selected.Price = (decimal)(RevaluePrice.Value ?? 0);
|
||||||
|
_service.Update(_selected);
|
||||||
|
DetailPrice.Text = _selected.PriceDisplay;
|
||||||
|
PriceDisplayRow.Visibility = Visibility.Visible;
|
||||||
|
RevalueRow.Visibility = Visibility.Collapsed;
|
||||||
|
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
|
||||||
|
RefreshList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RevalueCancel_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
PriceDisplayRow.Visibility = Visibility.Visible;
|
||||||
|
RevalueRow.Visibility = Visibility.Collapsed;
|
||||||
|
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CheckEbayBtn_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null || _priceLookup == null) return;
|
||||||
|
|
||||||
|
CheckEbayBtn.IsEnabled = false;
|
||||||
|
CheckEbayIcon.Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.Loading;
|
||||||
|
CheckEbayText.Text = "Checking…";
|
||||||
|
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var suggestion = await _priceLookup.GetSuggestionAsync(_selected);
|
||||||
|
|
||||||
|
if (suggestion != null)
|
||||||
|
{
|
||||||
|
RevaluePrice.Value = (double)suggestion.Price;
|
||||||
|
PriceSuggestionLabel.Text = suggestion.Label;
|
||||||
|
PriceSuggestionLabel.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PriceSuggestionLabel.Text = "No suggestion available — enter price manually.";
|
||||||
|
PriceSuggestionLabel.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PriceSuggestionLabel.Text = $"Lookup failed: {ex.Message}";
|
||||||
|
PriceSuggestionLabel.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CheckEbayIcon.Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.Magnify;
|
||||||
|
CheckEbayText.Text = "Check eBay";
|
||||||
|
CheckEbayBtn.IsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Edit mode ----
|
// ---- Edit mode ----
|
||||||
|
|
||||||
private void EditListing_Click(object sender, RoutedEventArgs e)
|
private void EditListing_Click(object sender, RoutedEventArgs e)
|
||||||
@@ -372,9 +449,17 @@ public partial class SavedListingsView : UserControl
|
|||||||
var path = _editPhotoPaths[i];
|
var path = _editPhotoPaths[i];
|
||||||
var index = i; // capture for lambdas
|
var index = i; // capture for lambdas
|
||||||
|
|
||||||
var container = new Grid { Width = 120, Height = 120, Margin = new Thickness(0, 0, 8, 0) };
|
// Outer StackPanel: photo tile on top, reorder buttons below
|
||||||
|
var outer = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Vertical,
|
||||||
|
Margin = new Thickness(0, 0, 8, 0),
|
||||||
|
Width = 120
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Photo tile (image + cover badge + remove button) ---
|
||||||
|
var photoGrid = new Grid { Width = 120, Height = 120 };
|
||||||
|
|
||||||
// Photo image
|
|
||||||
var imgBorder = new Border
|
var imgBorder = new Border
|
||||||
{
|
{
|
||||||
Width = 120, Height = 120,
|
Width = 120, Height = 120,
|
||||||
@@ -403,9 +488,9 @@ public partial class SavedListingsView : UserControl
|
|||||||
AddPhotoIcon(imgBorder);
|
AddPhotoIcon(imgBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.Children.Add(imgBorder);
|
photoGrid.Children.Add(imgBorder);
|
||||||
|
|
||||||
// "Cover" badge on the first photo
|
// "Cover" badge — top-left, only on first photo
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
{
|
{
|
||||||
var badge = new Border
|
var badge = new Border
|
||||||
@@ -418,10 +503,10 @@ public partial class SavedListingsView : UserControl
|
|||||||
Margin = new Thickness(4, 4, 0, 0)
|
Margin = new Thickness(4, 4, 0, 0)
|
||||||
};
|
};
|
||||||
badge.Child = new TextBlock { Text = "Cover", FontSize = 9, Foreground = Brushes.White };
|
badge.Child = new TextBlock { Text = "Cover", FontSize = 9, Foreground = Brushes.White };
|
||||||
container.Children.Add(badge);
|
photoGrid.Children.Add(badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove (×) button — top-right corner
|
// Remove (×) button — top-right corner of image
|
||||||
var removeBtn = new Button
|
var removeBtn = new Button
|
||||||
{
|
{
|
||||||
Content = "×",
|
Content = "×",
|
||||||
@@ -442,31 +527,35 @@ public partial class SavedListingsView : UserControl
|
|||||||
_editPhotoPaths.RemoveAt(index);
|
_editPhotoPaths.RemoveAt(index);
|
||||||
BuildEditPhotoStrip();
|
BuildEditPhotoStrip();
|
||||||
};
|
};
|
||||||
container.Children.Add(removeBtn);
|
photoGrid.Children.Add(removeBtn);
|
||||||
|
|
||||||
// Left/right reorder buttons — bottom-centre
|
outer.Children.Add(photoGrid);
|
||||||
|
|
||||||
|
// --- Reorder buttons below the photo ---
|
||||||
var reorderPanel = new StackPanel
|
var reorderPanel = new StackPanel
|
||||||
{
|
{
|
||||||
Orientation = Orientation.Horizontal,
|
Orientation = Orientation.Horizontal,
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
VerticalAlignment = VerticalAlignment.Bottom,
|
Margin = new Thickness(0, 4, 0, 0)
|
||||||
Margin = new Thickness(0, 0, 0, 4)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
var leftBtn = new Button
|
var leftBtn = new Button
|
||||||
{
|
{
|
||||||
Content = "◀",
|
Width = 52, Height = 24,
|
||||||
Width = 26, Height = 20,
|
FontSize = 10,
|
||||||
FontSize = 9,
|
|
||||||
Padding = new Thickness(0),
|
Padding = new Thickness(0),
|
||||||
Margin = new Thickness(0, 0, 2, 0),
|
Margin = new Thickness(0, 0, 2, 0),
|
||||||
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
||||||
Foreground = Brushes.White,
|
ToolTip = "Move left",
|
||||||
Background = new SolidColorBrush(Color.FromArgb(180, 40, 40, 40)),
|
IsEnabled = i > 0
|
||||||
ToolTip = "Move left"
|
|
||||||
};
|
};
|
||||||
|
leftBtn.Content = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
|
||||||
|
((StackPanel)leftBtn.Content).Children.Add(new MahApps.Metro.IconPacks.PackIconMaterial
|
||||||
|
{
|
||||||
|
Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronLeft,
|
||||||
|
Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center
|
||||||
|
});
|
||||||
|
((StackPanel)leftBtn.Content).Children.Add(new TextBlock { Text = "Move", FontSize = 9, Margin = new Thickness(2, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center });
|
||||||
leftBtn.Click += (s, e) =>
|
leftBtn.Click += (s, e) =>
|
||||||
{
|
{
|
||||||
(_editPhotoPaths[index], _editPhotoPaths[index - 1]) =
|
(_editPhotoPaths[index], _editPhotoPaths[index - 1]) =
|
||||||
@@ -474,21 +563,23 @@ public partial class SavedListingsView : UserControl
|
|||||||
BuildEditPhotoStrip();
|
BuildEditPhotoStrip();
|
||||||
};
|
};
|
||||||
reorderPanel.Children.Add(leftBtn);
|
reorderPanel.Children.Add(leftBtn);
|
||||||
}
|
|
||||||
|
|
||||||
if (i < _editPhotoPaths.Count - 1)
|
|
||||||
{
|
|
||||||
var rightBtn = new Button
|
var rightBtn = new Button
|
||||||
{
|
{
|
||||||
Content = "▶",
|
Width = 52, Height = 24,
|
||||||
Width = 26, Height = 20,
|
FontSize = 10,
|
||||||
FontSize = 9,
|
|
||||||
Padding = new Thickness(0),
|
Padding = new Thickness(0),
|
||||||
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
Style = (Style)FindResource("MahApps.Styles.Button.Square"),
|
||||||
Foreground = Brushes.White,
|
ToolTip = "Move right",
|
||||||
Background = new SolidColorBrush(Color.FromArgb(180, 40, 40, 40)),
|
IsEnabled = i < _editPhotoPaths.Count - 1
|
||||||
ToolTip = "Move right"
|
|
||||||
};
|
};
|
||||||
|
rightBtn.Content = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
|
||||||
|
((StackPanel)rightBtn.Content).Children.Add(new TextBlock { Text = "Move", FontSize = 9, Margin = new Thickness(0, 0, 2, 0), VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
((StackPanel)rightBtn.Content).Children.Add(new MahApps.Metro.IconPacks.PackIconMaterial
|
||||||
|
{
|
||||||
|
Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronRight,
|
||||||
|
Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center
|
||||||
|
});
|
||||||
rightBtn.Click += (s, e) =>
|
rightBtn.Click += (s, e) =>
|
||||||
{
|
{
|
||||||
(_editPhotoPaths[index], _editPhotoPaths[index + 1]) =
|
(_editPhotoPaths[index], _editPhotoPaths[index + 1]) =
|
||||||
@@ -496,12 +587,9 @@ public partial class SavedListingsView : UserControl
|
|||||||
BuildEditPhotoStrip();
|
BuildEditPhotoStrip();
|
||||||
};
|
};
|
||||||
reorderPanel.Children.Add(rightBtn);
|
reorderPanel.Children.Add(rightBtn);
|
||||||
}
|
|
||||||
|
|
||||||
if (reorderPanel.Children.Count > 0)
|
outer.Children.Add(reorderPanel);
|
||||||
container.Children.Add(reorderPanel);
|
EditPhotosPanel.Children.Add(outer);
|
||||||
|
|
||||||
EditPhotosPanel.Children.Add(container);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Add photos" button at the end of the strip
|
// "Add photos" button at the end of the strip
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public partial class SingleItemView : UserControl
|
|||||||
|
|
||||||
private ListingDraft _draft = new();
|
private ListingDraft _draft = new();
|
||||||
private System.Threading.CancellationTokenSource? _categoryCts;
|
private System.Threading.CancellationTokenSource? _categoryCts;
|
||||||
|
private bool _suppressCategoryLookup;
|
||||||
private string _suggestedPriceValue = "";
|
private string _suggestedPriceValue = "";
|
||||||
|
|
||||||
// Photo drag-reorder
|
// Photo drag-reorder
|
||||||
@@ -50,7 +51,7 @@ public partial class SingleItemView : UserControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Pre-fills the form from a Photo Analysis result.</summary>
|
/// <summary>Pre-fills the form from a Photo Analysis result.</summary>
|
||||||
public void PopulateFromAnalysis(PhotoAnalysisResult result, IReadOnlyList<string> imagePaths, decimal price)
|
public async void PopulateFromAnalysis(PhotoAnalysisResult result, IReadOnlyList<string> imagePaths, decimal price)
|
||||||
{
|
{
|
||||||
// Q6: reset form directly — calling NewListing_Click shows a confirmation dialog which
|
// Q6: reset form directly — calling NewListing_Click shows a confirmation dialog which
|
||||||
// is unexpected when arriving here automatically from the Photo Analysis tab.
|
// is unexpected when arriving here automatically from the Photo Analysis tab.
|
||||||
@@ -71,9 +72,9 @@ public partial class SingleItemView : UserControl
|
|||||||
TitleBox.Text = result.Title;
|
TitleBox.Text = result.Title;
|
||||||
DescriptionBox.Text = result.Description;
|
DescriptionBox.Text = result.Description;
|
||||||
PriceBox.Value = (double)price;
|
PriceBox.Value = (double)price;
|
||||||
CategoryBox.Text = result.CategoryKeyword;
|
|
||||||
|
|
||||||
_draft.CategoryName = result.CategoryKeyword;
|
// Auto-fill the top eBay category from the analysis keyword; user can override
|
||||||
|
await AutoFillCategoryAsync(result.CategoryKeyword);
|
||||||
|
|
||||||
// Q1: load all photos from analysis
|
// Q1: load all photos from analysis
|
||||||
var validPaths = imagePaths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToArray();
|
var validPaths = imagePaths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToArray();
|
||||||
@@ -117,6 +118,10 @@ public partial class SingleItemView : UserControl
|
|||||||
{
|
{
|
||||||
var title = await _aiService.GenerateTitleAsync(current, condition);
|
var title = await _aiService.GenerateTitleAsync(current, condition);
|
||||||
TitleBox.Text = title.Trim().TrimEnd('.').Trim('"');
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -129,6 +134,8 @@ public partial class SingleItemView : UserControl
|
|||||||
|
|
||||||
private async void CategoryBox_TextChanged(object sender, TextChangedEventArgs e)
|
private async void CategoryBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (_suppressCategoryLookup) return;
|
||||||
|
|
||||||
_categoryCts?.Cancel();
|
_categoryCts?.Cancel();
|
||||||
_categoryCts?.Dispose();
|
_categoryCts?.Dispose();
|
||||||
_categoryCts = new System.Threading.CancellationTokenSource();
|
_categoryCts = new System.Threading.CancellationTokenSource();
|
||||||
@@ -211,6 +218,38 @@ public partial class SingleItemView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 ----
|
// ---- Condition ----
|
||||||
|
|
||||||
private void ConditionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void ConditionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
|||||||
Reference in New Issue
Block a user