Compare commits
3 Commits
feature/dy
...
7507030f72
| Author | SHA1 | Date | |
|---|---|---|---|
| 7507030f72 | |||
| e9c5464df0 | |||
| e3827d97e8 |
@@ -1,79 +0,0 @@
|
|||||||
namespace EbayListingTool.Helpers;
|
|
||||||
|
|
||||||
public static class NumberWords
|
|
||||||
{
|
|
||||||
private static readonly string[] Ones =
|
|
||||||
[
|
|
||||||
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
|
||||||
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
|
|
||||||
"seventeen", "eighteen", "nineteen"
|
|
||||||
];
|
|
||||||
|
|
||||||
private static readonly string[] Tens =
|
|
||||||
[
|
|
||||||
"", "", "twenty", "thirty", "forty", "fifty",
|
|
||||||
"sixty", "seventy", "eighty", "ninety"
|
|
||||||
];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a price to a friendly verbal string.
|
|
||||||
/// £17.49 → "about seventeen pounds"
|
|
||||||
/// £17.50 → "about seventeen pounds fifty"
|
|
||||||
/// £0.50 → "fifty pence"
|
|
||||||
/// </summary>
|
|
||||||
public static string ToVerbalPrice(decimal price)
|
|
||||||
{
|
|
||||||
if (price <= 0) return "no price set";
|
|
||||||
|
|
||||||
// Snap to nearest 50p
|
|
||||||
var rounded = Math.Round(price * 2) / 2m;
|
|
||||||
int pounds = (int)rounded;
|
|
||||||
bool hasFifty = (rounded - pounds) >= 0.5m;
|
|
||||||
|
|
||||||
if (pounds == 0)
|
|
||||||
return "fifty pence";
|
|
||||||
|
|
||||||
var poundsWord = IntToWords(pounds);
|
|
||||||
var poundsLabel = pounds == 1 ? "pound" : "pounds";
|
|
||||||
var suffix = hasFifty ? " fifty" : "";
|
|
||||||
|
|
||||||
return $"about {poundsWord} {poundsLabel}{suffix}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a UTC DateTime to a human-friendly relative string.
|
|
||||||
/// </summary>
|
|
||||||
public static string ToRelativeDate(DateTime utcTime)
|
|
||||||
{
|
|
||||||
var diff = DateTime.UtcNow - utcTime;
|
|
||||||
|
|
||||||
if (diff.TotalSeconds < 60) return "just now";
|
|
||||||
if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} minutes ago";
|
|
||||||
if (diff.TotalHours < 2) return "about an hour ago";
|
|
||||||
if (diff.TotalHours < 24) return $"{(int)diff.TotalHours} hours ago";
|
|
||||||
if (diff.TotalDays < 2) return "yesterday";
|
|
||||||
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays} days ago";
|
|
||||||
if (diff.TotalDays < 14) return "last week";
|
|
||||||
if (diff.TotalDays < 30) return $"{(int)(diff.TotalDays / 7)} weeks ago";
|
|
||||||
if (diff.TotalDays < 60) return "last month";
|
|
||||||
return $"{(int)(diff.TotalDays / 30)} months ago";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string IntToWords(int n)
|
|
||||||
{
|
|
||||||
if (n < 20) return Ones[n];
|
|
||||||
if (n < 100)
|
|
||||||
{
|
|
||||||
var t = Tens[n / 10];
|
|
||||||
var o = n % 10;
|
|
||||||
return o == 0 ? t : $"{t}-{Ones[o]}";
|
|
||||||
}
|
|
||||||
if (n < 1000)
|
|
||||||
{
|
|
||||||
var h = Ones[n / 100];
|
|
||||||
var rest = n % 100;
|
|
||||||
return rest == 0 ? $"{h} hundred" : $"{h} hundred and {IntToWords(rest)}";
|
|
||||||
}
|
|
||||||
return n.ToString(); // fallback for very large prices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using EbayListingTool.Helpers;
|
|
||||||
|
|
||||||
namespace EbayListingTool.Models;
|
namespace EbayListingTool.Models;
|
||||||
|
|
||||||
public class SavedListing
|
public class SavedListing
|
||||||
@@ -13,6 +11,18 @@ public class SavedListing
|
|||||||
public string ConditionNotes { get; set; } = "";
|
public string ConditionNotes { get; set; } = "";
|
||||||
public string ExportFolder { get; set; } = "";
|
public string ExportFolder { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>eBay category ID — stored at save time so we can post without re-looking it up.</summary>
|
||||||
|
public string CategoryId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Item condition — defaults to Used for existing records without this field.</summary>
|
||||||
|
public ItemCondition Condition { get; set; } = ItemCondition.Used;
|
||||||
|
|
||||||
|
/// <summary>Listing format — defaults to FixedPrice for existing records.</summary>
|
||||||
|
public ListingFormat Format { get; set; } = ListingFormat.FixedPrice;
|
||||||
|
|
||||||
|
/// <summary>Seller postcode — populated from appsettings default at save time.</summary>
|
||||||
|
public string Postcode { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>Absolute paths to photos inside ExportFolder.</summary>
|
/// <summary>Absolute paths to photos inside ExportFolder.</summary>
|
||||||
public List<string> PhotoPaths { get; set; } = new();
|
public List<string> PhotoPaths { get; set; } = new();
|
||||||
|
|
||||||
@@ -20,9 +30,22 @@ public class SavedListing
|
|||||||
|
|
||||||
public string PriceDisplay => Price > 0 ? $"£{Price:F2}" : "—";
|
public string PriceDisplay => Price > 0 ? $"£{Price:F2}" : "—";
|
||||||
|
|
||||||
public string PriceWords => NumberWords.ToVerbalPrice(Price);
|
|
||||||
|
|
||||||
public string SavedAtDisplay => SavedAt.ToLocalTime().ToString("d MMM yyyy, HH:mm");
|
public string SavedAtDisplay => SavedAt.ToLocalTime().ToString("d MMM yyyy, HH:mm");
|
||||||
|
|
||||||
public string SavedAtRelative => NumberWords.ToRelativeDate(SavedAt);
|
/// <summary>
|
||||||
|
/// Converts this saved draft back into a ListingDraft suitable for PostListingAsync.
|
||||||
|
/// </summary>
|
||||||
|
public ListingDraft ToListingDraft() => new ListingDraft
|
||||||
|
{
|
||||||
|
Title = Title,
|
||||||
|
Description = Description,
|
||||||
|
Price = Price,
|
||||||
|
CategoryId = CategoryId,
|
||||||
|
CategoryName = Category,
|
||||||
|
Condition = Condition,
|
||||||
|
Format = Format,
|
||||||
|
Postcode = Postcode,
|
||||||
|
PhotoPaths = new List<string>(PhotoPaths),
|
||||||
|
Quantity = 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
115
EbayListingTool/Views/NewListingView.xaml
Normal file
115
EbayListingTool/Views/NewListingView.xaml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<UserControl x:Class="EbayListingTool.Views.NewListingView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
|
||||||
|
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||||
|
Loaded="UserControl_Loaded">
|
||||||
|
|
||||||
|
<!-- Root grid hosts all three states; Visibility toggled in code-behind -->
|
||||||
|
<Grid>
|
||||||
|
<!-- ══════════════════════════════════════ STATE A: Drop Zone -->
|
||||||
|
<Grid x:Name="StateA" Visibility="Visible">
|
||||||
|
<DockPanel LastChildFill="True">
|
||||||
|
|
||||||
|
<!-- Loading panel — shown while AI runs -->
|
||||||
|
<Border x:Name="LoadingPanel" DockPanel.Dock="Top"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Margin="60,30,60,0" Padding="30,40"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Gray9}"
|
||||||
|
CornerRadius="10">
|
||||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<mah:ProgressRing Width="36" Height="36"
|
||||||
|
HorizontalAlignment="Center" Margin="0,0,0,16"/>
|
||||||
|
<TextBlock x:Name="LoadingStepText"
|
||||||
|
Text="Examining the photo…"
|
||||||
|
FontSize="14" FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray1}"/>
|
||||||
|
<TextBlock Text="This usually takes 10–20 seconds"
|
||||||
|
FontSize="11" HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Margin="0,6,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Drop zone -->
|
||||||
|
<Border x:Name="DropZoneBorder" DockPanel.Dock="Top"
|
||||||
|
Margin="60,30,60,0"
|
||||||
|
AllowDrop="True"
|
||||||
|
MouseLeftButtonUp="DropZone_Click"
|
||||||
|
DragOver="DropZone_DragOver"
|
||||||
|
DragEnter="DropZone_DragEnter"
|
||||||
|
DragLeave="DropZone_DragLeave"
|
||||||
|
Drop="DropZone_Drop"
|
||||||
|
Cursor="Hand"
|
||||||
|
MinHeight="180">
|
||||||
|
<Grid>
|
||||||
|
<!-- Dashed border via Rectangle -->
|
||||||
|
<Rectangle x:Name="DropBorderRect"
|
||||||
|
StrokeThickness="2"
|
||||||
|
StrokeDashArray="6,4"
|
||||||
|
RadiusX="10" RadiusY="10"
|
||||||
|
Stroke="{DynamicResource MahApps.Brushes.Gray6}"/>
|
||||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Margin="0,40">
|
||||||
|
<iconPacks:PackIconMaterial Kind="CameraOutline"
|
||||||
|
Width="52" Height="52"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
<TextBlock Text="Drop photos here"
|
||||||
|
FontSize="18" FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray2}"/>
|
||||||
|
<TextBlock Text="or click to browse — up to 12 photos"
|
||||||
|
FontSize="12" HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Margin="0,6,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Thumbnail strip -->
|
||||||
|
<ScrollViewer x:Name="ThumbScroller" DockPanel.Dock="Top"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Disabled"
|
||||||
|
Margin="60,12,60,0" Visibility="Collapsed">
|
||||||
|
<StackPanel x:Name="ThumbStrip" Orientation="Horizontal"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Analyse button -->
|
||||||
|
<StackPanel DockPanel.Dock="Top" HorizontalAlignment="Center" Margin="0,20,0,0">
|
||||||
|
<Button x:Name="AnalyseBtn"
|
||||||
|
Click="Analyse_Click"
|
||||||
|
IsEnabled="False"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square.Accent}"
|
||||||
|
Padding="28,12" FontSize="14" FontWeight="SemiBold">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="AnalyseIcon"
|
||||||
|
Kind="MagnifyScan" Width="18" Height="18"
|
||||||
|
Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<mah:ProgressRing x:Name="AnalyseSpinner"
|
||||||
|
Width="18" Height="18" Margin="0,0,8,0"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
<TextBlock x:Name="AnalyseBtnText"
|
||||||
|
Text="Identify & Price with AI"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<TextBlock x:Name="PhotoCountLabel"
|
||||||
|
HorizontalAlignment="Center" Margin="0,8,0,0"
|
||||||
|
FontSize="11" Visibility="Collapsed"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid/> <!-- fill remaining space -->
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════ STATE B: Review & Edit (stub for now) -->
|
||||||
|
<Grid x:Name="StateB" Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════ STATE C: Success (stub for now) -->
|
||||||
|
<Grid x:Name="StateC" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
235
EbayListingTool/Views/NewListingView.xaml.cs
Normal file
235
EbayListingTool/Views/NewListingView.xaml.cs
Normal file
@@ -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<string> _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;
|
||||||
|
}
|
||||||
@@ -325,166 +325,6 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Card Preview — dyscalculia-friendly quick-approve flow
|
|
||||||
============================================================ -->
|
|
||||||
<StackPanel x:Name="CardPreviewPanel" Visibility="Collapsed">
|
|
||||||
|
|
||||||
<!-- Item card -->
|
|
||||||
<Border CornerRadius="10" Padding="16" Margin="0,0,0,12"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray10}"
|
|
||||||
BorderBrush="{DynamicResource MahApps.Brushes.Gray8}"
|
|
||||||
BorderThickness="1">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="110"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Cover photo -->
|
|
||||||
<Border Grid.Column="0" CornerRadius="7" ClipToBounds="True"
|
|
||||||
Width="110" Height="110">
|
|
||||||
<Image x:Name="CardPhoto" Stretch="UniformToFill"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Details -->
|
|
||||||
<StackPanel Grid.Column="1" Margin="14,0,0,0"
|
|
||||||
VerticalAlignment="Center">
|
|
||||||
|
|
||||||
<TextBlock x:Name="CardItemName"
|
|
||||||
FontSize="17" FontWeight="Bold"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray1}"/>
|
|
||||||
|
|
||||||
<TextBlock x:Name="CardCondition"
|
|
||||||
FontSize="11" Margin="0,3,0,0"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
|
||||||
|
|
||||||
<!-- Photo dots (built in code-behind) -->
|
|
||||||
<StackPanel x:Name="CardPhotoDots"
|
|
||||||
Orientation="Horizontal" Margin="0,8,0,0"/>
|
|
||||||
|
|
||||||
<!-- Verbal price — primary -->
|
|
||||||
<TextBlock x:Name="CardPriceVerbal"
|
|
||||||
FontSize="24" FontWeight="Bold" Margin="0,10,0,2"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
|
|
||||||
<!-- Digit price — secondary/small -->
|
|
||||||
<TextBlock x:Name="CardPriceDigit"
|
|
||||||
FontSize="11" Opacity="0.40"/>
|
|
||||||
|
|
||||||
<!-- Category pill -->
|
|
||||||
<Border CornerRadius="10" Padding="8,3" Margin="0,10,0,0"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Gray9}"
|
|
||||||
HorizontalAlignment="Left">
|
|
||||||
<TextBlock x:Name="CardCategory" FontSize="11"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray3}"/>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Live price note (updated async) -->
|
|
||||||
<TextBlock x:Name="CardLivePriceNote"
|
|
||||||
FontSize="10" Margin="0,0,0,10"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
|
||||||
TextWrapping="Wrap" Visibility="Collapsed"/>
|
|
||||||
|
|
||||||
<!-- Primary action buttons -->
|
|
||||||
<Grid Margin="0,0,0,6">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="8"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Button Grid.Column="0" x:Name="LooksGoodBtn"
|
|
||||||
Click="LooksGood_Click"
|
|
||||||
Height="54" FontSize="15" FontWeight="SemiBold"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Check" Width="17" Height="17"
|
|
||||||
Margin="0,0,8,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Looks good" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button Grid.Column="2" x:Name="ChangeSomethingBtn"
|
|
||||||
Click="ChangeSomething_Click"
|
|
||||||
Height="54" FontSize="13"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial x:Name="ChangeChevron"
|
|
||||||
Kind="ChevronDown" Width="13" Height="13"
|
|
||||||
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Change something" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Change panel (collapsed by default) -->
|
|
||||||
<StackPanel x:Name="CardChangePanel" Visibility="Collapsed" Margin="0,4,0,0">
|
|
||||||
|
|
||||||
<!-- Price slider -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="PRICE" Style="{StaticResource SectionHeading}"
|
|
||||||
Margin="0,0,0,10"/>
|
|
||||||
<TextBlock x:Name="SliderVerbalLabel"
|
|
||||||
FontSize="22" FontWeight="Bold" Margin="0,0,0,2"
|
|
||||||
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
<TextBlock x:Name="SliderDigitLabel"
|
|
||||||
FontSize="11" Opacity="0.40" Margin="0,0,0,12"/>
|
|
||||||
<Slider x:Name="PriceSliderCard"
|
|
||||||
Minimum="0.50" Maximum="200"
|
|
||||||
SmallChange="0.5" LargeChange="5"
|
|
||||||
TickFrequency="0.5" IsSnapToTickEnabled="True"
|
|
||||||
ValueChanged="PriceSliderCard_ValueChanged"/>
|
|
||||||
<Grid Margin="0,3,0,0">
|
|
||||||
<TextBlock Text="cheaper" FontSize="10" Opacity="0.45"
|
|
||||||
HorizontalAlignment="Left"/>
|
|
||||||
<TextBlock Text="pricier" FontSize="10" Opacity="0.45"
|
|
||||||
HorizontalAlignment="Right"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Title edit -->
|
|
||||||
<Border Style="{StaticResource SectionCard}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="TITLE" Style="{StaticResource SectionHeading}"
|
|
||||||
Margin="0,0,0,8"/>
|
|
||||||
<TextBox x:Name="CardTitleBox"
|
|
||||||
TextWrapping="Wrap" AcceptsReturn="False"
|
|
||||||
MaxLength="80" FontSize="13"
|
|
||||||
TextChanged="CardTitleBox_TextChanged"/>
|
|
||||||
<!-- Colour bar — no digit counter -->
|
|
||||||
<Grid Margin="0,6,0,0" Height="4">
|
|
||||||
<Border CornerRadius="2" Background="{DynamicResource MahApps.Brushes.Gray8}"/>
|
|
||||||
<Border x:Name="CardTitleBar" CornerRadius="2"
|
|
||||||
HorizontalAlignment="Left" Width="0"
|
|
||||||
Background="{DynamicResource MahApps.Brushes.Accent}"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Save with changes -->
|
|
||||||
<Button Content="Save with changes"
|
|
||||||
Click="SaveWithChanges_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
|
||||||
Height="46" FontSize="14" FontWeight="SemiBold"
|
|
||||||
HorizontalAlignment="Stretch" Margin="0,4,0,8"/>
|
|
||||||
|
|
||||||
<Button Content="Analyse another item"
|
|
||||||
Click="AnalyseAnother_Click"
|
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
|
||||||
Height="36" FontSize="12"
|
|
||||||
HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Results (hidden until analysis complete) -->
|
<!-- Results (hidden until analysis complete) -->
|
||||||
<StackPanel x:Name="ResultsPanel" Visibility="Collapsed" Opacity="0">
|
<StackPanel x:Name="ResultsPanel" Visibility="Collapsed" Opacity="0">
|
||||||
<StackPanel.RenderTransform>
|
<StackPanel.RenderTransform>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using EbayListingTool.Helpers;
|
|
||||||
using EbayListingTool.Models;
|
using EbayListingTool.Models;
|
||||||
using EbayListingTool.Services;
|
using EbayListingTool.Services;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
@@ -286,65 +285,9 @@ public partial class PhotoAnalysisView : UserControl
|
|||||||
|
|
||||||
private void ShowResults(PhotoAnalysisResult r)
|
private void ShowResults(PhotoAnalysisResult r)
|
||||||
{
|
{
|
||||||
IdlePanel.Visibility = Visibility.Collapsed;
|
IdlePanel.Visibility = Visibility.Collapsed;
|
||||||
LoadingPanel.Visibility = Visibility.Collapsed;
|
LoadingPanel.Visibility = Visibility.Collapsed;
|
||||||
ResultsPanel.Visibility = Visibility.Collapsed; // hidden behind card preview
|
ResultsPanel.Visibility = Visibility.Visible;
|
||||||
CardPreviewPanel.Visibility = Visibility.Visible;
|
|
||||||
CardChangePanel.Visibility = Visibility.Collapsed;
|
|
||||||
ChangeChevron.Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronDown;
|
|
||||||
|
|
||||||
// --- Populate card preview ---
|
|
||||||
CardItemName.Text = r.ItemName;
|
|
||||||
CardCondition.Text = r.ConditionNotes;
|
|
||||||
CardCategory.Text = r.CategoryKeyword;
|
|
||||||
CardPriceVerbal.Text = NumberWords.ToVerbalPrice(r.PriceSuggested);
|
|
||||||
CardPriceDigit.Text = r.PriceSuggested > 0 ? $"£{r.PriceSuggested:F2}" : "";
|
|
||||||
CardLivePriceNote.Visibility = Visibility.Collapsed;
|
|
||||||
|
|
||||||
// Cover photo
|
|
||||||
if (_currentImagePaths.Count > 0)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bmp = new BitmapImage();
|
|
||||||
bmp.BeginInit();
|
|
||||||
bmp.UriSource = new Uri(_currentImagePaths[0], UriKind.Absolute);
|
|
||||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
|
||||||
bmp.DecodePixelWidth = 220;
|
|
||||||
bmp.EndInit();
|
|
||||||
bmp.Freeze();
|
|
||||||
CardPhoto.Source = bmp;
|
|
||||||
}
|
|
||||||
catch { CardPhoto.Source = null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Photo dots — one dot per photo, filled accent for first
|
|
||||||
CardPhotoDots.Children.Clear();
|
|
||||||
for (int i = 0; i < _currentImagePaths.Count; i++)
|
|
||||||
{
|
|
||||||
CardPhotoDots.Children.Add(new System.Windows.Shapes.Ellipse
|
|
||||||
{
|
|
||||||
Width = 7, Height = 7,
|
|
||||||
Margin = new Thickness(2, 0, 2, 0),
|
|
||||||
Fill = i == 0
|
|
||||||
? (Brush)FindResource("MahApps.Brushes.Accent")
|
|
||||||
: (Brush)FindResource("MahApps.Brushes.Gray7")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
CardPhotoDots.Visibility = _currentImagePaths.Count > 1
|
|
||||||
? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
|
|
||||||
// Price slider — centre on suggested, range ±60% clamped to sensible bounds
|
|
||||||
var suggested = (double)(r.PriceSuggested > 0 ? r.PriceSuggested : 10m);
|
|
||||||
PriceSliderCard.Minimum = Math.Max(0.50, Math.Round(suggested * 0.4 * 2) / 2);
|
|
||||||
PriceSliderCard.Maximum = Math.Round(suggested * 1.8 * 2) / 2;
|
|
||||||
PriceSliderCard.Value = Math.Round(suggested * 2) / 2; // snap to 50p
|
|
||||||
SliderVerbalLabel.Text = NumberWords.ToVerbalPrice(r.PriceSuggested);
|
|
||||||
SliderDigitLabel.Text = $"£{r.PriceSuggested:F2}";
|
|
||||||
|
|
||||||
// Card title box
|
|
||||||
CardTitleBox.Text = r.Title;
|
|
||||||
UpdateCardTitleBar(r.Title.Length);
|
|
||||||
|
|
||||||
// Item identification
|
// Item identification
|
||||||
ItemNameText.Text = r.ItemName;
|
ItemNameText.Text = r.ItemName;
|
||||||
@@ -408,6 +351,10 @@ public partial class PhotoAnalysisView : UserControl
|
|||||||
|
|
||||||
// Reset live price row until lookup completes
|
// Reset live price row until lookup completes
|
||||||
LivePriceRow.Visibility = Visibility.Collapsed;
|
LivePriceRow.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
// Animate results in
|
||||||
|
var sb = (Storyboard)FindResource("ResultsReveal");
|
||||||
|
sb.Begin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateLivePricesAsync(string query)
|
private async Task UpdateLivePricesAsync(string query)
|
||||||
@@ -445,23 +392,9 @@ public partial class PhotoAnalysisView : UserControl
|
|||||||
// Update suggested price to 40th percentile (competitive but not cheapest)
|
// Update suggested price to 40th percentile (competitive but not cheapest)
|
||||||
var suggested = live.Suggested;
|
var suggested = live.Suggested;
|
||||||
PriceSuggestedText.Text = $"£{suggested:F2}";
|
PriceSuggestedText.Text = $"£{suggested:F2}";
|
||||||
PriceOverride.Value = (double)Math.Round(suggested, 2);
|
PriceOverride.Value = (double)Math.Round(suggested, 2); // Issue 6: avoid decimal→double drift
|
||||||
if (_lastResult != null) _lastResult.PriceSuggested = suggested;
|
if (_lastResult != null) _lastResult.PriceSuggested = suggested;
|
||||||
|
|
||||||
// Update card preview price
|
|
||||||
CardPriceVerbal.Text = NumberWords.ToVerbalPrice(suggested);
|
|
||||||
CardPriceDigit.Text = $"£{suggested:F2}";
|
|
||||||
var snapped = Math.Round((double)suggested * 2) / 2;
|
|
||||||
if (snapped >= PriceSliderCard.Minimum && snapped <= PriceSliderCard.Maximum)
|
|
||||||
PriceSliderCard.Value = snapped;
|
|
||||||
SliderVerbalLabel.Text = NumberWords.ToVerbalPrice(suggested);
|
|
||||||
SliderDigitLabel.Text = $"£{suggested:F2}";
|
|
||||||
|
|
||||||
// Show note on card
|
|
||||||
var noteText = $"eBay: {live.Count} similar listing{(live.Count == 1 ? "" : "s")}, range £{live.Min:F2}–£{live.Max:F2}";
|
|
||||||
CardLivePriceNote.Text = noteText;
|
|
||||||
CardLivePriceNote.Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
// Update status label
|
// Update status label
|
||||||
LivePriceSpinner.Visibility = Visibility.Collapsed;
|
LivePriceSpinner.Visibility = Visibility.Collapsed;
|
||||||
LivePriceStatus.Text =
|
LivePriceStatus.Text =
|
||||||
@@ -733,63 +666,6 @@ public partial class PhotoAnalysisView : UserControl
|
|||||||
Clipboard.SetText(DescriptionBox.Text);
|
Clipboard.SetText(DescriptionBox.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Card preview handlers ----
|
|
||||||
|
|
||||||
private void LooksGood_Click(object sender, RoutedEventArgs e)
|
|
||||||
=> SaveListing_Click(sender, e);
|
|
||||||
|
|
||||||
private void ChangeSomething_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var expanding = CardChangePanel.Visibility != Visibility.Visible;
|
|
||||||
CardChangePanel.Visibility = expanding ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
ChangeChevron.Kind = expanding
|
|
||||||
? MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronUp
|
|
||||||
: MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PriceSliderCard_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
||||||
{
|
|
||||||
if (!IsLoaded) return;
|
|
||||||
var price = (decimal)e.NewValue;
|
|
||||||
SliderVerbalLabel.Text = NumberWords.ToVerbalPrice(price);
|
|
||||||
SliderDigitLabel.Text = $"£{price:F2}";
|
|
||||||
// Keep card price display in sync
|
|
||||||
CardPriceVerbal.Text = NumberWords.ToVerbalPrice(price);
|
|
||||||
CardPriceDigit.Text = $"£{price:F2}";
|
|
||||||
// Keep hidden ResultsPanel in sync so SaveListing_Click gets the right value
|
|
||||||
PriceOverride.Value = e.NewValue;
|
|
||||||
if (_lastResult != null) _lastResult.PriceSuggested = price;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CardTitleBox_TextChanged(object sender, TextChangedEventArgs e)
|
|
||||||
{
|
|
||||||
var len = CardTitleBox.Text.Length;
|
|
||||||
UpdateCardTitleBar(len);
|
|
||||||
// Keep hidden ResultsPanel in sync
|
|
||||||
TitleBox.Text = CardTitleBox.Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCardTitleBar(int len)
|
|
||||||
{
|
|
||||||
if (!IsLoaded) return;
|
|
||||||
var trackWidth = CardTitleBar.Parent is Grid g ? g.ActualWidth : 0;
|
|
||||||
if (trackWidth <= 0) return;
|
|
||||||
CardTitleBar.Width = trackWidth * (len / 80.0);
|
|
||||||
CardTitleBar.Background = len > 75
|
|
||||||
? System.Windows.Media.Brushes.OrangeRed
|
|
||||||
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveWithChanges_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_lastResult != null)
|
|
||||||
{
|
|
||||||
_lastResult.Title = CardTitleBox.Text.Trim();
|
|
||||||
TitleBox.Text = _lastResult.Title;
|
|
||||||
}
|
|
||||||
SaveListing_Click(sender, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Loading step cycling ----
|
// ---- Loading step cycling ----
|
||||||
|
|
||||||
private void LoadingTimer_Tick(object? sender, EventArgs e)
|
private void LoadingTimer_Tick(object? sender, EventArgs e)
|
||||||
|
|||||||
@@ -318,6 +318,21 @@
|
|||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<WrapPanel Orientation="Horizontal">
|
<WrapPanel Orientation="Horizontal">
|
||||||
|
<Button x:Name="PostDraftBtn"
|
||||||
|
Click="PostDraft_Click"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square.Accent}"
|
||||||
|
Height="34" Padding="14,0" Margin="0,0,8,6"
|
||||||
|
ToolTip="Post this draft to eBay">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="PostDraftIcon"
|
||||||
|
Kind="CartArrowRight" Width="14" Height="14"
|
||||||
|
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||||||
|
<mah:ProgressRing x:Name="PostDraftSpinner"
|
||||||
|
Width="14" Height="14" Margin="0,0,6,0"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
<TextBlock Text="Post to eBay" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
<Button Click="EditListing_Click"
|
<Button Click="EditListing_Click"
|
||||||
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
|
||||||
Height="34" Padding="14,0" Margin="0,0,8,6">
|
Height="34" Padding="14,0" Margin="0,0,8,6">
|
||||||
@@ -443,5 +458,40 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Draft posted toast -->
|
||||||
|
<Border x:Name="DraftPostedToast"
|
||||||
|
Grid.Column="2"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Margin="12" Padding="14,10"
|
||||||
|
CornerRadius="6"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Gray8}"
|
||||||
|
BorderThickness="0,0,0,3"
|
||||||
|
BorderBrush="{DynamicResource MahApps.Brushes.Accent}"
|
||||||
|
Panel.ZIndex="10">
|
||||||
|
<Border.RenderTransform>
|
||||||
|
<TranslateTransform x:Name="ToastTranslate" Y="60"/>
|
||||||
|
</Border.RenderTransform>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<iconPacks:PackIconMaterial Kind="CheckCircleOutline"
|
||||||
|
Width="16" Height="16" Margin="0,0,10,0"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Accent}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="ToastUrlText" Grid.Column="1"
|
||||||
|
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray1}" FontSize="12"/>
|
||||||
|
<Button Grid.Column="2" Content="✕" Width="20" Height="20"
|
||||||
|
BorderThickness="0" Background="Transparent"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Click="DismissToast_Click" Margin="8,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Windows.Input;
|
|||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Threading;
|
||||||
using EbayListingTool.Models;
|
using EbayListingTool.Models;
|
||||||
using EbayListingTool.Services;
|
using EbayListingTool.Services;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
@@ -14,6 +15,8 @@ public partial class SavedListingsView : UserControl
|
|||||||
{
|
{
|
||||||
private SavedListingsService? _service;
|
private SavedListingsService? _service;
|
||||||
private PriceLookupService? _priceLookup;
|
private PriceLookupService? _priceLookup;
|
||||||
|
private EbayListingService? _ebayListing;
|
||||||
|
private EbayAuthService? _ebayAuth;
|
||||||
private SavedListing? _selected;
|
private SavedListing? _selected;
|
||||||
|
|
||||||
// Edit mode working state
|
// Edit mode working state
|
||||||
@@ -34,10 +37,15 @@ public partial class SavedListingsView : UserControl
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialise(SavedListingsService service, PriceLookupService? priceLookup = null)
|
public void Initialise(SavedListingsService service,
|
||||||
|
PriceLookupService? priceLookup = null,
|
||||||
|
EbayListingService? ebayListing = null,
|
||||||
|
EbayAuthService? ebayAuth = null)
|
||||||
{
|
{
|
||||||
_service = service;
|
_service = service;
|
||||||
_priceLookup = priceLookup;
|
_priceLookup = priceLookup;
|
||||||
|
_ebayListing = ebayListing;
|
||||||
|
_ebayAuth = ebayAuth;
|
||||||
RefreshList();
|
RefreshList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,32 +197,23 @@ public partial class SavedListingsView : UserControl
|
|||||||
Margin = new Thickness(0, 0, 0, 3)
|
Margin = new Thickness(0, 0, 0, 3)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verbal price — primary; digit price small beneath
|
var priceRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
var priceVerbal = new TextBlock
|
priceRow.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = listing.PriceWords,
|
Text = listing.PriceDisplay,
|
||||||
FontSize = 13,
|
FontSize = 13,
|
||||||
FontWeight = FontWeights.Bold,
|
FontWeight = FontWeights.Bold,
|
||||||
Foreground = (Brush)FindResource("MahApps.Brushes.Accent"),
|
Foreground = (Brush)FindResource("MahApps.Brushes.Accent"),
|
||||||
TextTrimming = TextTrimming.CharacterEllipsis
|
Margin = new Thickness(0, 0, 8, 0)
|
||||||
};
|
|
||||||
textStack.Children.Add(priceVerbal);
|
|
||||||
|
|
||||||
textStack.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = listing.PriceDisplay,
|
|
||||||
FontSize = 9,
|
|
||||||
Opacity = 0.40,
|
|
||||||
Margin = new Thickness(0, 1, 0, 0)
|
|
||||||
});
|
});
|
||||||
|
textStack.Children.Add(priceRow);
|
||||||
|
|
||||||
// Relative date
|
|
||||||
textStack.Children.Add(new TextBlock
|
textStack.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = listing.SavedAtRelative,
|
Text = listing.SavedAtDisplay,
|
||||||
FontSize = 10,
|
FontSize = 10,
|
||||||
Foreground = (Brush)FindResource("MahApps.Brushes.Gray5"),
|
Foreground = (Brush)FindResource("MahApps.Brushes.Gray5"),
|
||||||
Margin = new Thickness(0, 3, 0, 0)
|
Margin = new Thickness(0, 3, 0, 0)
|
||||||
});
|
});
|
||||||
|
|
||||||
grid.Children.Add(textStack);
|
grid.Children.Add(textStack);
|
||||||
@@ -273,7 +272,7 @@ public partial class SavedListingsView : UserControl
|
|||||||
DetailTitle.Text = listing.Title;
|
DetailTitle.Text = listing.Title;
|
||||||
DetailPrice.Text = listing.PriceDisplay;
|
DetailPrice.Text = listing.PriceDisplay;
|
||||||
DetailCategory.Text = listing.Category;
|
DetailCategory.Text = listing.Category;
|
||||||
DetailDate.Text = listing.SavedAtRelative;
|
DetailDate.Text = listing.SavedAtDisplay;
|
||||||
DetailDescription.Text = listing.Description;
|
DetailDescription.Text = listing.Description;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(listing.ConditionNotes))
|
if (!string.IsNullOrWhiteSpace(listing.ConditionNotes))
|
||||||
@@ -762,4 +761,87 @@ public partial class SavedListingsView : UserControl
|
|||||||
ClearDetail();
|
ClearDetail();
|
||||||
RefreshList();
|
RefreshList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void PostDraft_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null || _ebayListing == null || _ebayAuth == null) return;
|
||||||
|
if (!_ebayAuth.IsConnected)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Please connect to eBay first.", "Not Connected",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetPostDraftBusy(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var draft = _selected.ToListingDraft();
|
||||||
|
var url = await _ebayListing.PostListingAsync(draft);
|
||||||
|
|
||||||
|
ToastUrlText.Text = url;
|
||||||
|
ShowDraftPostedToast();
|
||||||
|
|
||||||
|
var posted = _selected;
|
||||||
|
_selected = null;
|
||||||
|
_service?.Delete(posted);
|
||||||
|
ClearDetail();
|
||||||
|
RefreshList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Failed to post listing:\n\n{ex.Message}", "Post Failed",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetPostDraftBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPostDraftBusy(bool busy)
|
||||||
|
{
|
||||||
|
PostDraftBtn.IsEnabled = !busy;
|
||||||
|
PostDraftIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
PostDraftSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
IsEnabled = !busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DispatcherTimer? _toastTimer;
|
||||||
|
|
||||||
|
private void ShowDraftPostedToast()
|
||||||
|
{
|
||||||
|
_toastTimer?.Stop();
|
||||||
|
ToastTranslate.BeginAnimation(System.Windows.Media.TranslateTransform.YProperty, null);
|
||||||
|
DraftPostedToast.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
var slideIn = new System.Windows.Media.Animation.DoubleAnimation(
|
||||||
|
60, 0, new Duration(TimeSpan.FromMilliseconds(220)))
|
||||||
|
{
|
||||||
|
EasingFunction = new System.Windows.Media.Animation.CubicEase
|
||||||
|
{ EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut }
|
||||||
|
};
|
||||||
|
slideIn.Completed += (_, _) =>
|
||||||
|
{
|
||||||
|
_toastTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(8) };
|
||||||
|
_toastTimer.Tick += (_, _) => DismissToastAnimated();
|
||||||
|
_toastTimer.Start();
|
||||||
|
};
|
||||||
|
ToastTranslate.BeginAnimation(System.Windows.Media.TranslateTransform.YProperty, slideIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DismissToast_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> DismissToastAnimated();
|
||||||
|
|
||||||
|
private void DismissToastAnimated()
|
||||||
|
{
|
||||||
|
_toastTimer?.Stop();
|
||||||
|
var slideOut = new System.Windows.Media.Animation.DoubleAnimation(
|
||||||
|
0, 60, new Duration(TimeSpan.FromMilliseconds(180)))
|
||||||
|
{
|
||||||
|
EasingFunction = new System.Windows.Media.Animation.CubicEase
|
||||||
|
{ EasingMode = System.Windows.Media.Animation.EasingMode.EaseIn }
|
||||||
|
};
|
||||||
|
slideOut.Completed += (_, _) => DraftPostedToast.Visibility = Visibility.Collapsed;
|
||||||
|
ToastTranslate.BeginAnimation(System.Windows.Media.TranslateTransform.YProperty, slideOut);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user