diff --git a/EbayListingTool/Views/NewListingView.xaml b/EbayListingTool/Views/NewListingView.xaml new file mode 100644 index 0000000..eb90c80 --- /dev/null +++ b/EbayListingTool/Views/NewListingView.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EbayListingTool/Views/NewListingView.xaml.cs b/EbayListingTool/Views/NewListingView.xaml.cs new file mode 100644 index 0000000..8e9f211 --- /dev/null +++ b/EbayListingTool/Views/NewListingView.xaml.cs @@ -0,0 +1,235 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using EbayListingTool.Models; +using EbayListingTool.Services; +using Microsoft.Win32; + +namespace EbayListingTool.Views; + +public partial class NewListingView : UserControl +{ + // Services (injected via Initialise) + private EbayListingService? _listingService; + private EbayCategoryService? _categoryService; + private AiAssistantService? _aiService; + private EbayAuthService? _auth; + private SavedListingsService? _savedService; + private string _defaultPostcode = ""; + + // State A — photos + private readonly List _photoPaths = new(); + private const int MaxPhotos = 12; + + // State B — draft being edited (stub, populated in Task 4) + private ListingDraft _draft = new(); + private PhotoAnalysisResult? _lastAnalysis; + private bool _suppressCategoryLookup; + private System.Threading.CancellationTokenSource? _categoryCts; + private string _suggestedPriceValue = ""; + + // 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" + ]; + + public NewListingView() + { + InitializeComponent(); + _loadingTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2.5) }; + _loadingTimer.Tick += (_, _) => + { + _loadingStep = (_loadingStep + 1) % LoadingSteps.Length; + LoadingStepText.Text = LoadingSteps[_loadingStep]; + }; + } + + private void UserControl_Loaded(object sender, RoutedEventArgs e) { } + + public void Initialise(EbayListingService listingService, EbayCategoryService categoryService, + AiAssistantService aiService, EbayAuthService auth, + SavedListingsService savedService, string defaultPostcode) + { + _listingService = listingService; + _categoryService = categoryService; + _aiService = aiService; + _auth = auth; + _savedService = savedService; + _defaultPostcode = defaultPostcode; + } + + // ---- State machine ---- + + private enum ListingState { DropZone, ReviewEdit, Success } + + private void SetState(ListingState state) + { + StateA.Visibility = state == ListingState.DropZone ? Visibility.Visible : Visibility.Collapsed; + StateB.Visibility = state == ListingState.ReviewEdit ? Visibility.Visible : Visibility.Collapsed; + StateC.Visibility = state == ListingState.Success ? Visibility.Visible : Visibility.Collapsed; + } + + // ---- State A: 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) + { + DropBorderRect.Stroke = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent"); + e.Handled = true; + } + + private void DropZone_DragLeave(object sender, DragEventArgs e) + { + DropBorderRect.Stroke = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray6"); + } + + 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); + AddPhotos(files.Where(IsImageFile).ToArray()); + } + + private void DropZone_Click(object sender, MouseButtonEventArgs e) + { + var dlg = new OpenFileDialog + { + Title = "Select photos of the item", + Filter = "Images|*.jpg;*.jpeg;*.png;*.gif;*.webp;*.bmp|All files|*.*", + Multiselect = true + }; + if (dlg.ShowDialog() == true) + AddPhotos(dlg.FileNames); + } + + private void AddPhotos(string[] paths) + { + foreach (var path in paths) + { + if (_photoPaths.Count >= MaxPhotos) break; + if (_photoPaths.Contains(path)) continue; + if (!IsImageFile(path)) continue; + _photoPaths.Add(path); + } + UpdateThumbStrip(); + UpdateAnalyseButton(); + } + + private void UpdateThumbStrip() + { + ThumbStrip.Children.Clear(); + ThumbScroller.Visibility = _photoPaths.Count > 0 ? Visibility.Visible : Visibility.Collapsed; + + foreach (var path in _photoPaths) + { + try + { + var bmp = new BitmapImage(); + bmp.BeginInit(); + bmp.UriSource = new Uri(path, UriKind.Absolute); + bmp.DecodePixelWidth = 80; + bmp.CacheOption = BitmapCacheOption.OnLoad; + bmp.EndInit(); + bmp.Freeze(); + + var img = new Image + { + Source = bmp, Width = 60, Height = 60, + Stretch = System.Windows.Media.Stretch.UniformToFill, + Margin = new Thickness(3) + }; + img.Clip = new System.Windows.Media.RectangleGeometry( + new Rect(0, 0, 60, 60), 4, 4); + ThumbStrip.Children.Add(img); + } + catch { /* skip bad files */ } + } + + PhotoCountLabel.Text = $"{_photoPaths.Count} / {MaxPhotos} photos"; + PhotoCountLabel.Visibility = _photoPaths.Count > 0 ? Visibility.Visible : Visibility.Collapsed; + } + + private void UpdateAnalyseButton() + { + AnalyseBtn.IsEnabled = _photoPaths.Count > 0; + } + + private async void Analyse_Click(object sender, RoutedEventArgs e) + { + if (_aiService == null || _photoPaths.Count == 0) return; + SetAnalysing(true); + try + { + var result = await _aiService.AnalyseItemFromPhotosAsync(_photoPaths); + _lastAnalysis = result; + await PopulateStateBAsync(result); + SetState(ListingState.ReviewEdit); + } + catch (Exception ex) + { + MessageBox.Show($"Analysis failed:\n\n{ex.Message}", "AI Error", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + finally + { + SetAnalysing(false); + } + } + + private void SetAnalysing(bool busy) + { + AnalyseBtn.IsEnabled = !busy; + AnalyseSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; + AnalyseIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible; + AnalyseBtnText.Text = busy ? "Analysing\u2026" : "Identify & Price with AI"; + LoadingPanel.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; + DropZoneBorder.Visibility = busy ? Visibility.Collapsed : Visibility.Visible; + if (busy) + { + _loadingStep = 0; + LoadingStepText.Text = LoadingSteps[0]; + _loadingTimer.Start(); + } + else + { + _loadingTimer.Stop(); + } + } + + // Stub for State B — implemented in Task 4 + private Task PopulateStateBAsync(PhotoAnalysisResult result) => Task.CompletedTask; + + // Stub for ResetToStateA — implemented in Task 4 + public void ResetToStateA() + { + _photoPaths.Clear(); + _draft = new ListingDraft { Postcode = _defaultPostcode }; + _lastAnalysis = null; + UpdateThumbStrip(); + UpdateAnalyseButton(); + SetState(ListingState.DropZone); + } + + private static bool IsImageFile(string path) + { + var ext = System.IO.Path.GetExtension(path).ToLowerInvariant(); + return ext is ".jpg" or ".jpeg" or ".png" or ".gif" or ".webp" or ".bmp"; + } + + private MainWindow? GetWindow() => Window.GetWindow(this) as MainWindow; +}