diff --git a/EbayListingTool/Views/PhotoAnalysisView.xaml b/EbayListingTool/Views/PhotoAnalysisView.xaml
deleted file mode 100644
index 7da77e3..0000000
--- a/EbayListingTool/Views/PhotoAnalysisView.xaml
+++ /dev/null
@@ -1,682 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/EbayListingTool/Views/PhotoAnalysisView.xaml.cs b/EbayListingTool/Views/PhotoAnalysisView.xaml.cs
deleted file mode 100644
index 224c041..0000000
--- a/EbayListingTool/Views/PhotoAnalysisView.xaml.cs
+++ /dev/null
@@ -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 _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, 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;
- }
-
- ///
- /// Adds to the photo list (up to MaxPhotos).
- /// The preview always shows the most recently added image.
- ///
- 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 ----
-
- ///
- /// Rebuilds the thumbnail strip from .
- /// Shows/hides the strip based on whether 2+ photos are loaded.
- ///
- 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);
- }
- }
-
- ///
- /// Updates only the border highlights on the existing thumb strip children
- /// after the active index changes, avoiding a full thumbnail reload.
- ///
- 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;
-}
diff --git a/EbayListingTool/Views/SingleItemView.xaml b/EbayListingTool/Views/SingleItemView.xaml
deleted file mode 100644
index c122f03..0000000
--- a/EbayListingTool/Views/SingleItemView.xaml
+++ /dev/null
@@ -1,566 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/EbayListingTool/Views/SingleItemView.xaml.cs b/EbayListingTool/Views/SingleItemView.xaml.cs
deleted file mode 100644
index bce2b3d..0000000
--- a/EbayListingTool/Views/SingleItemView.xaml.cs
+++ /dev/null
@@ -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"] ?? "";
- }
-
- /// Pre-fills the form from a Photo Analysis result.
- public async void PopulateFromAnalysis(PhotoAnalysisResult result, IReadOnlyList 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;
- }
- }
-
- ///
- /// Fetches the top eBay category suggestion for and auto-fills
- /// the category fields. The suggestions list is shown so the user can override.
- ///
- 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(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();
- }
-
- ///
- /// Clears and recreates all photo thumbnails from .
- /// Called after any add, remove, or reorder operation so the panel always matches the list.
- ///
- 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;
-}