Files
EbayListingTool/EbayListingTool/Views/NewListingView.xaml.cs

752 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}