752 lines
29 KiB
C#
752 lines
29 KiB
C#
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<string> _photoPaths = new();
|
||
private const int MaxPhotos = 12;
|
||
|
||
// State B — draft being edited
|
||
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 = 120;
|
||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||
bmp.EndInit();
|
||
bmp.Freeze();
|
||
|
||
var img = new Image
|
||
{
|
||
Source = bmp, Width = 96, Height = 96,
|
||
Stretch = System.Windows.Media.Stretch.UniformToFill,
|
||
Margin = new Thickness(4)
|
||
};
|
||
img.Clip = new System.Windows.Media.RectangleGeometry(
|
||
new Rect(0, 0, 96, 96), 6, 6);
|
||
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();
|
||
}
|
||
}
|
||
|
||
// ---- State B: Populate from analysis ----
|
||
|
||
private async Task PopulateStateBAsync(PhotoAnalysisResult result)
|
||
{
|
||
_draft = new ListingDraft { Postcode = _defaultPostcode };
|
||
_draft.PhotoPaths = new List<string>(_photoPaths);
|
||
RebuildBPhotoThumbnails();
|
||
|
||
BTitleBox.Text = result.Title;
|
||
BDescBox.Text = result.Description;
|
||
BPriceBox.Value = (double)Math.Round(result.PriceSuggested, 2);
|
||
BPostcodeBox.Text = _defaultPostcode;
|
||
BConditionBox.SelectedIndex = 3; // Used
|
||
|
||
if (!string.IsNullOrWhiteSpace(result.CategoryKeyword))
|
||
await AutoFillCategoryAsync(result.CategoryKeyword);
|
||
|
||
if (result.PriceMin > 0 && result.PriceMax > 0)
|
||
{
|
||
BPriceHint.Text = $"AI estimate: \u00A3{result.PriceMin:F2} – \u00A3{result.PriceMax:F2}";
|
||
BPriceHint.Visibility = Visibility.Visible;
|
||
}
|
||
}
|
||
|
||
// ---- Title ----
|
||
|
||
private void TitleBox_TextChanged(object sender, TextChangedEventArgs e)
|
||
{
|
||
_draft.Title = BTitleBox.Text;
|
||
var len = BTitleBox.Text.Length;
|
||
BTitleCount.Text = $"{len} / 80";
|
||
var over = len > 75;
|
||
var trackBorder = BTitleBar.Parent as Border;
|
||
double trackWidth = trackBorder?.ActualWidth ?? 0;
|
||
if (trackWidth > 0) BTitleBar.Width = trackWidth * (len / 80.0);
|
||
BTitleBar.Background = over
|
||
? System.Windows.Media.Brushes.OrangeRed
|
||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent");
|
||
BTitleCount.Foreground = over
|
||
? System.Windows.Media.Brushes.OrangeRed
|
||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
||
}
|
||
|
||
private async void AiTitle_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_aiService == null) return;
|
||
SetTitleBusy(true);
|
||
try
|
||
{
|
||
var title = await _aiService.GenerateTitleAsync(BTitleBox.Text, GetSelectedCondition().ToString());
|
||
BTitleBox.Text = title.Trim().TrimEnd('.').Trim('"');
|
||
if (string.IsNullOrWhiteSpace(_draft.CategoryId))
|
||
await AutoFillCategoryAsync(BTitleBox.Text);
|
||
}
|
||
catch (Exception ex) { ShowError("AI Title", ex.Message); }
|
||
finally { SetTitleBusy(false); }
|
||
}
|
||
|
||
private void SetTitleBusy(bool busy)
|
||
{
|
||
AiTitleBtn.IsEnabled = !busy;
|
||
TitleSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||
TitleAiIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||
}
|
||
|
||
// ---- Description ----
|
||
|
||
private void DescBox_TextChanged(object sender, TextChangedEventArgs e)
|
||
{
|
||
_draft.Description = BDescBox.Text;
|
||
var len = BDescBox.Text.Length;
|
||
const int softCap = 2000;
|
||
BDescCount.Text = $"{len} / {softCap}";
|
||
var over = len > softCap;
|
||
var trackBorder = BDescBar.Parent as Border;
|
||
double trackWidth = trackBorder?.ActualWidth ?? 0;
|
||
if (trackWidth > 0) BDescBar.Width = Math.Min(trackWidth, trackWidth * (len / (double)softCap));
|
||
BDescBar.Background = over
|
||
? System.Windows.Media.Brushes.OrangeRed
|
||
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xF5, 0x9E, 0x0B));
|
||
BDescCount.Foreground = over
|
||
? System.Windows.Media.Brushes.OrangeRed
|
||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
||
}
|
||
|
||
private async void AiDesc_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_aiService == null) return;
|
||
SetDescBusy(true);
|
||
try
|
||
{
|
||
var desc = await _aiService.WriteDescriptionAsync(
|
||
BTitleBox.Text, GetSelectedCondition().ToString(), BDescBox.Text);
|
||
BDescBox.Text = desc;
|
||
}
|
||
catch (Exception ex) { ShowError("AI Description", ex.Message); }
|
||
finally { SetDescBusy(false); }
|
||
}
|
||
|
||
private void SetDescBusy(bool busy)
|
||
{
|
||
AiDescBtn.IsEnabled = !busy;
|
||
DescSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||
DescAiIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||
}
|
||
|
||
// ---- Category ----
|
||
|
||
private void CategoryBox_TextChanged(object sender, TextChangedEventArgs e)
|
||
{
|
||
if (_suppressCategoryLookup) return;
|
||
_categoryCts?.Cancel();
|
||
_categoryCts?.Dispose();
|
||
_categoryCts = new System.Threading.CancellationTokenSource();
|
||
var cts = _categoryCts;
|
||
if (BCategoryBox.Text.Length < 3) { BCategoryList.Visibility = Visibility.Collapsed; return; }
|
||
_ = SearchCategoryAsync(BCategoryBox.Text, cts);
|
||
}
|
||
|
||
private async Task SearchCategoryAsync(string text, System.Threading.CancellationTokenSource cts)
|
||
{
|
||
try
|
||
{
|
||
await Task.Delay(350, cts.Token);
|
||
if (cts.IsCancellationRequested) return;
|
||
var suggestions = await _categoryService!.GetCategorySuggestionsAsync(text);
|
||
if (cts.IsCancellationRequested) return;
|
||
BCategoryList.ItemsSource = suggestions.Select(s => s.CategoryName).ToList();
|
||
BCategoryList.Tag = suggestions;
|
||
BCategoryList.Visibility = suggestions.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||
}
|
||
catch (OperationCanceledException) { }
|
||
catch { }
|
||
}
|
||
|
||
private void CategoryBox_KeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
if (e.Key == Key.Escape) { BCategoryList.Visibility = Visibility.Collapsed; e.Handled = true; }
|
||
}
|
||
|
||
private void CategoryList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||
{
|
||
if (BCategoryList.SelectedIndex < 0) return;
|
||
var suggestions = BCategoryList.Tag as List<CategorySuggestion>;
|
||
if (suggestions == null || BCategoryList.SelectedIndex >= suggestions.Count) return;
|
||
var cat = suggestions[BCategoryList.SelectedIndex];
|
||
_suppressCategoryLookup = true;
|
||
_draft.CategoryId = cat.CategoryId;
|
||
_draft.CategoryName = cat.CategoryName;
|
||
BCategoryBox.Text = cat.CategoryName;
|
||
BCategoryIdLabel.Text = $"ID: {cat.CategoryId}";
|
||
BCategoryList.Visibility = Visibility.Collapsed;
|
||
_suppressCategoryLookup = false;
|
||
}
|
||
|
||
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;
|
||
_draft.CategoryId = top.CategoryId;
|
||
_draft.CategoryName = top.CategoryName;
|
||
BCategoryBox.Text = top.CategoryName;
|
||
BCategoryIdLabel.Text = $"ID: {top.CategoryId}";
|
||
_suppressCategoryLookup = false;
|
||
BCategoryList.ItemsSource = suggestions.Select(s => s.CategoryName).ToList();
|
||
BCategoryList.Tag = suggestions;
|
||
BCategoryList.Visibility = suggestions.Count > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
// ---- Condition ----
|
||
|
||
private void ConditionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||
{
|
||
_draft.Condition = GetSelectedCondition();
|
||
}
|
||
|
||
private ItemCondition GetSelectedCondition()
|
||
{
|
||
var tag = (BConditionBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Used";
|
||
return tag switch
|
||
{
|
||
"New" => ItemCondition.New,
|
||
"OpenBox" => ItemCondition.OpenBox,
|
||
"Refurbished" => ItemCondition.Refurbished,
|
||
"ForParts" => ItemCondition.ForPartsOrNotWorking,
|
||
_ => ItemCondition.Used
|
||
};
|
||
}
|
||
|
||
// ---- Price ----
|
||
|
||
private async void AiPrice_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_aiService == null) return;
|
||
SetPriceBusy(true);
|
||
try
|
||
{
|
||
var result = await _aiService.SuggestPriceAsync(BTitleBox.Text, GetSelectedCondition().ToString());
|
||
var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||
var priceLine = lines.FirstOrDefault(l => l.StartsWith("PRICE:", StringComparison.OrdinalIgnoreCase));
|
||
_suggestedPriceValue = priceLine?.Replace("PRICE:", "", StringComparison.OrdinalIgnoreCase).Trim() ?? "";
|
||
BPriceHint.Text = lines.FirstOrDefault() ?? result;
|
||
BPriceHint.Visibility = Visibility.Visible;
|
||
if (decimal.TryParse(_suggestedPriceValue, out var price))
|
||
BPriceBox.Value = (double)price;
|
||
}
|
||
catch (Exception ex) { ShowError("AI Price", ex.Message); }
|
||
finally { SetPriceBusy(false); }
|
||
}
|
||
|
||
private void SetPriceBusy(bool busy)
|
||
{
|
||
AiPriceBtn.IsEnabled = !busy;
|
||
PriceSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||
PriceAiIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||
}
|
||
|
||
// ---- Photos (State B) ----
|
||
|
||
private void RebuildBPhotoThumbnails()
|
||
{
|
||
BPhotosPanel.Children.Clear();
|
||
for (int i = 0; i < _draft.PhotoPaths.Count; i++)
|
||
AddBPhotoThumbnail(_draft.PhotoPaths[i], i);
|
||
BPhotoCount.Text = $"{_draft.PhotoPaths.Count} / {MaxPhotos}";
|
||
BPhotoCount.Foreground = _draft.PhotoPaths.Count >= MaxPhotos
|
||
? System.Windows.Media.Brushes.OrangeRed
|
||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
||
}
|
||
|
||
private void AddBPhotoThumbnail(string path, int index)
|
||
{
|
||
try
|
||
{
|
||
var bmp = new BitmapImage();
|
||
bmp.BeginInit();
|
||
bmp.UriSource = new Uri(path, UriKind.Absolute);
|
||
bmp.DecodePixelWidth = 160;
|
||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||
bmp.EndInit();
|
||
bmp.Freeze();
|
||
|
||
var img = new Image
|
||
{
|
||
Width = 100, Height = 100,
|
||
Stretch = System.Windows.Media.Stretch.UniformToFill,
|
||
Source = bmp, ToolTip = System.IO.Path.GetFileName(path)
|
||
};
|
||
img.Clip = new System.Windows.Media.RectangleGeometry(new Rect(0, 0, 100, 100), 6, 6);
|
||
|
||
var removeBtn = new Button
|
||
{
|
||
Width = 18, Height = 18, Content = "\u2715",
|
||
FontSize = 11, FontWeight = FontWeights.Bold,
|
||
Cursor = Cursors.Hand, ToolTip = "Remove",
|
||
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), Opacity = 0
|
||
};
|
||
removeBtn.Click += (s, ev) =>
|
||
{
|
||
ev.Handled = true;
|
||
_draft.PhotoPaths.Remove(path);
|
||
RebuildBPhotoThumbnails();
|
||
};
|
||
|
||
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),
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
VerticalAlignment = VerticalAlignment.Top,
|
||
Margin = new Thickness(2, 2, 0, 0),
|
||
IsHitTestVisible = false,
|
||
Child = new TextBlock
|
||
{
|
||
Text = "Cover", FontSize = 8, FontWeight = FontWeights.SemiBold,
|
||
Foreground = System.Windows.Media.Brushes.White
|
||
}
|
||
};
|
||
}
|
||
|
||
var container = new Grid
|
||
{
|
||
Width = 100, Height = 100, Margin = new Thickness(4),
|
||
Cursor = Cursors.SizeAll, AllowDrop = true, Tag = path
|
||
};
|
||
container.Children.Add(img);
|
||
if (coverBadge != null) container.Children.Add(coverBadge);
|
||
container.Children.Add(removeBtn);
|
||
|
||
container.MouseEnter += (s, ev) => removeBtn.Opacity = 1;
|
||
container.MouseLeave += (s, ev) => removeBtn.Opacity = 0;
|
||
|
||
Point dragStart = default;
|
||
bool isDragging = false;
|
||
container.MouseLeftButtonDown += (s, ev) => dragStart = ev.GetPosition(null);
|
||
container.MouseMove += (s, ev) =>
|
||
{
|
||
if (ev.LeftButton != MouseButtonState.Pressed || isDragging) return;
|
||
var pos = ev.GetPosition(null);
|
||
if (Math.Abs(pos.X - dragStart.X) > SystemParameters.MinimumHorizontalDragDistance ||
|
||
Math.Abs(pos.Y - dragStart.Y) > SystemParameters.MinimumVerticalDragDistance)
|
||
{
|
||
isDragging = true;
|
||
DragDrop.DoDragDrop(container, path, DragDropEffects.Move);
|
||
isDragging = false;
|
||
}
|
||
};
|
||
container.DragOver += (s, ev) =>
|
||
{
|
||
if (ev.Data.GetDataPresent(typeof(string)) &&
|
||
(string)ev.Data.GetData(typeof(string)) != path)
|
||
{ ev.Effects = DragDropEffects.Move; container.Opacity = 0.45; }
|
||
else ev.Effects = DragDropEffects.None;
|
||
ev.Handled = true;
|
||
};
|
||
container.DragLeave += (s, ev) => container.Opacity = 1.0;
|
||
container.Drop += (s, ev) =>
|
||
{
|
||
container.Opacity = 1.0;
|
||
if (!ev.Data.GetDataPresent(typeof(string))) return;
|
||
var src = (string)ev.Data.GetData(typeof(string));
|
||
var tgt = (string)container.Tag;
|
||
if (src == tgt) return;
|
||
var si = _draft.PhotoPaths.IndexOf(src);
|
||
var ti = _draft.PhotoPaths.IndexOf(tgt);
|
||
if (si < 0 || ti < 0) return;
|
||
_draft.PhotoPaths.RemoveAt(si);
|
||
_draft.PhotoPaths.Insert(ti, src);
|
||
RebuildBPhotoThumbnails();
|
||
ev.Handled = true;
|
||
};
|
||
|
||
BPhotosPanel.Children.Add(container);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void AddMorePhotos_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var dlg = new OpenFileDialog
|
||
{
|
||
Title = "Add more photos",
|
||
Filter = "Images|*.jpg;*.jpeg;*.png;*.gif;*.webp;*.bmp|All files|*.*",
|
||
Multiselect = true
|
||
};
|
||
if (dlg.ShowDialog() == true)
|
||
{
|
||
foreach (var path in dlg.FileNames)
|
||
{
|
||
if (_draft.PhotoPaths.Count >= MaxPhotos) break;
|
||
if (!_draft.PhotoPaths.Contains(path)) _draft.PhotoPaths.Add(path);
|
||
}
|
||
RebuildBPhotoThumbnails();
|
||
}
|
||
}
|
||
|
||
// ---- Footer actions ----
|
||
|
||
private void StartOver_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var isDirty = !string.IsNullOrWhiteSpace(BTitleBox.Text) ||
|
||
!string.IsNullOrWhiteSpace(BDescBox.Text);
|
||
if (isDirty)
|
||
{
|
||
var result = MessageBox.Show("Start over? Any edits will be lost.",
|
||
"Start Over", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||
if (result != MessageBoxResult.OK) return;
|
||
}
|
||
ResetToStateA();
|
||
}
|
||
|
||
public void ResetToStateA()
|
||
{
|
||
_photoPaths.Clear();
|
||
_draft = new ListingDraft { Postcode = _defaultPostcode };
|
||
_lastAnalysis = null;
|
||
UpdateThumbStrip();
|
||
UpdateAnalyseButton();
|
||
if (BPhotosPanel != null) BPhotosPanel.Children.Clear();
|
||
if (BTitleBox != null) BTitleBox.Text = "";
|
||
if (BDescBox != null) BDescBox.Text = "";
|
||
if (BCategoryBox != null) { BCategoryBox.Text = ""; BCategoryList.Visibility = Visibility.Collapsed; }
|
||
if (BCategoryIdLabel != null) BCategoryIdLabel.Text = "(no category selected)";
|
||
if (BPriceBox != null) BPriceBox.Value = 0;
|
||
if (BPriceHint != null) BPriceHint.Visibility = Visibility.Collapsed;
|
||
if (BConditionBox != null) BConditionBox.SelectedIndex = 3;
|
||
if (BFormatBox != null) BFormatBox.SelectedIndex = 0;
|
||
if (BPostcodeBox != null) BPostcodeBox.Text = _defaultPostcode;
|
||
SetState(ListingState.DropZone);
|
||
}
|
||
|
||
private void SaveDraft_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_savedService == null) return;
|
||
if (!ValidateDraft()) return;
|
||
CollectDraftFromFields();
|
||
try
|
||
{
|
||
_savedService.Save(
|
||
_draft.Title, _draft.Description, _draft.Price,
|
||
_draft.CategoryName, "",
|
||
_draft.PhotoPaths,
|
||
_draft.CategoryId, _draft.Condition, _draft.Format,
|
||
BPostcodeBox.Text);
|
||
GetWindow()?.RefreshSavedListings();
|
||
GetWindow()?.SetStatus($"Draft saved: {_draft.Title}");
|
||
ResetToStateA();
|
||
}
|
||
catch (Exception ex) { ShowError("Save Failed", ex.Message); }
|
||
}
|
||
|
||
private async void Post_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_listingService == null) return;
|
||
if (!ValidateDraft()) return;
|
||
CollectDraftFromFields();
|
||
SetPostBusy(true);
|
||
try
|
||
{
|
||
var url = await _listingService.PostListingAsync(_draft);
|
||
_draft.EbayListingUrl = url;
|
||
|
||
// Persist a record of the posting
|
||
if (_savedService != null)
|
||
{
|
||
try
|
||
{
|
||
_savedService.Save(
|
||
_draft.Title, _draft.Description, _draft.Price,
|
||
_draft.CategoryName, $"Posted: {url}",
|
||
_draft.PhotoPaths,
|
||
_draft.CategoryId, _draft.Condition, _draft.Format,
|
||
_draft.Postcode);
|
||
GetWindow()?.RefreshSavedListings();
|
||
}
|
||
catch { /* non-critical — posting succeeded, history save is best-effort */ }
|
||
}
|
||
|
||
BSuccessUrl.Text = url;
|
||
SetState(ListingState.Success);
|
||
GetWindow()?.SetStatus($"Listed: {_draft.Title}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Log full stack trace to help diagnose crashes
|
||
try
|
||
{
|
||
var logPath = System.IO.Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||
"EbayListingTool", "crash_log.txt");
|
||
var msg = $"{DateTime.Now:HH:mm:ss} [Post_Click] {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}\n";
|
||
if (ex.InnerException != null)
|
||
msg += $" Inner: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}\n";
|
||
System.IO.File.AppendAllText(logPath, msg + "\n");
|
||
}
|
||
catch { }
|
||
ShowError("Post Failed", ex.Message);
|
||
}
|
||
finally { SetPostBusy(false); }
|
||
}
|
||
|
||
private void CollectDraftFromFields()
|
||
{
|
||
_draft.Title = BTitleBox.Text.Trim();
|
||
_draft.Description = BDescBox.Text.Trim();
|
||
_draft.Price = (decimal)(BPriceBox.Value ?? 0);
|
||
_draft.Condition = GetSelectedCondition();
|
||
_draft.Format = BFormatBox.SelectedIndex == 0 ? ListingFormat.FixedPrice : ListingFormat.Auction;
|
||
_draft.Postcode = BPostcodeBox.Text;
|
||
_draft.Quantity = 1;
|
||
}
|
||
|
||
private bool ValidateDraft()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(BTitleBox?.Text))
|
||
{ ShowError("Validation", "Please enter a title."); return false; }
|
||
if (BTitleBox.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 ((BPriceBox?.Value ?? 0) <= 0)
|
||
{ ShowError("Validation", "Please enter a price greater than zero."); return false; }
|
||
return true;
|
||
}
|
||
|
||
private void SetPostBusy(bool busy)
|
||
{
|
||
PostBtn.IsEnabled = !busy;
|
||
PostSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||
PostIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||
IsEnabled = !busy;
|
||
}
|
||
|
||
private void ShowError(string title, string msg)
|
||
=> MessageBox.Show(msg, title, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||
|
||
// ---- State C handlers ----
|
||
|
||
private void SuccessUrl_Click(object sender, MouseButtonEventArgs e)
|
||
{
|
||
var url = BSuccessUrl.Text;
|
||
if (!string.IsNullOrEmpty(url))
|
||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url)
|
||
{ UseShellExecute = true });
|
||
}
|
||
|
||
private void CopyUrl_Click(object sender, RoutedEventArgs e)
|
||
=> System.Windows.Clipboard.SetText(BSuccessUrl.Text);
|
||
|
||
private void ListAnother_Click(object sender, RoutedEventArgs e)
|
||
=> ResetToStateA();
|
||
|
||
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;
|
||
}
|