- 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>
695 lines
25 KiB
C#
695 lines
25 KiB
C#
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;
|
|
}
|