Files
EbayListingTool/EbayListingTool/Views/SavedListingsView.xaml.cs
Peter Foster f4e7854297 Revert to Light.Indigo theme; restore original grey values for light bg
Dark theme experiment abandoned due to persistent contrast issues.
All Gray3/Gray4 muted text reverted to Gray5/Gray6 (darker = readable
on light background). Gray2 headings reverted to Gray4/Gray5.
App.xaml switched back to Light.Indigo.xaml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 03:21:10 +01:00

373 lines
12 KiB
C#

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using EbayListingTool.Models;
using EbayListingTool.Services;
namespace EbayListingTool.Views;
public partial class SavedListingsView : UserControl
{
private SavedListingsService? _service;
private SavedListing? _selected;
// Normal card background — resolved once after load so we can restore it on mouse-leave
private Brush? _cardNormalBg;
private Brush? _cardHoverBg;
public SavedListingsView()
{
InitializeComponent();
Loaded += (_, _) =>
{
_cardNormalBg = (Brush)FindResource("MahApps.Brushes.Gray10");
_cardHoverBg = (Brush)FindResource("MahApps.Brushes.Gray9");
};
}
public void Initialise(SavedListingsService service)
{
_service = service;
RefreshList();
}
// ---- Public refresh (called after a new save) ----
public void RefreshList()
{
if (_service == null) return;
var listings = _service.Listings;
ListingCountText.Text = listings.Count == 1 ? "1 saved listing" : $"{listings.Count} saved listings";
ApplyFilter(SearchBox.Text, listings);
// Re-select if we had one selected
if (_selected != null)
{
var stillExists = listings.FirstOrDefault(l => l.Id == _selected.Id);
if (stillExists != null) ShowDetail(stillExists, animate: false);
else ClearDetail();
}
}
// ---- Search filter ----
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (_service == null) return;
ApplyFilter(SearchBox.Text, _service.Listings);
}
private void ApplyFilter(string query, IReadOnlyList<SavedListing> listings)
{
CardPanel.Children.Clear();
var filtered = string.IsNullOrWhiteSpace(query)
? listings
: listings.Where(l => l.Title.Contains(query, StringComparison.OrdinalIgnoreCase)).ToList();
// Empty states
if (listings.Count == 0)
{
EmptyCardState.Visibility = Visibility.Visible;
EmptyFilterState.Visibility = Visibility.Collapsed;
}
else if (filtered.Count == 0)
{
EmptyCardState.Visibility = Visibility.Collapsed;
EmptyFilterState.Visibility = Visibility.Visible;
}
else
{
EmptyCardState.Visibility = Visibility.Collapsed;
EmptyFilterState.Visibility = Visibility.Collapsed;
}
foreach (var listing in filtered)
CardPanel.Children.Add(BuildCard(listing));
}
// ---- Card builder ----
private Border BuildCard(SavedListing listing)
{
var isSelected = _selected?.Id == listing.Id;
var card = new Border
{
Style = (Style)FindResource(isSelected ? "SelectedCardBorder" : "CardBorder")
};
var grid = new Grid { Margin = new Thickness(10, 10, 10, 10) };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(64) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// Thumbnail container — relative panel so we can overlay the badge
var thumbContainer = new Grid { Width = 60, Height = 60, Margin = new Thickness(0, 0, 10, 0) };
var thumb = new Border
{
Width = 60, Height = 60,
CornerRadius = new CornerRadius(5),
Background = (Brush)FindResource("MahApps.Brushes.Gray8"),
ClipToBounds = true
};
if (!string.IsNullOrEmpty(listing.FirstPhotoPath) && File.Exists(listing.FirstPhotoPath))
{
try
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = new Uri(listing.FirstPhotoPath, UriKind.Absolute); // W1
bmp.DecodePixelWidth = 120;
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
bmp.Freeze(); // M2
thumb.Child = new Image
{
Source = bmp,
Stretch = Stretch.UniformToFill
};
}
catch { AddPhotoIcon(thumb); }
}
else
{
AddPhotoIcon(thumb);
}
thumbContainer.Children.Add(thumb);
// Photo count badge — shown only when there are 2+ photos
if (listing.PhotoPaths.Count >= 2)
{
var badge = new Border
{
CornerRadius = new CornerRadius(3),
Background = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)),
Padding = new Thickness(4, 1, 4, 1),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Bottom,
Margin = new Thickness(0, 0, 2, 2)
};
badge.Child = new TextBlock
{
Text = $"{listing.PhotoPaths.Count} photos",
FontSize = 9,
Foreground = Brushes.White
};
thumbContainer.Children.Add(badge);
}
Grid.SetColumn(thumbContainer, 0);
grid.Children.Add(thumbContainer);
// Text block
var textStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
Grid.SetColumn(textStack, 1);
textStack.Children.Add(new TextBlock
{
Text = listing.Title,
FontSize = 12,
FontWeight = FontWeights.SemiBold,
TextTrimming = TextTrimming.CharacterEllipsis,
Foreground = (Brush)FindResource("MahApps.Brushes.Gray1"),
Margin = new Thickness(0, 0, 0, 3)
});
var priceRow = new StackPanel { Orientation = Orientation.Horizontal };
priceRow.Children.Add(new TextBlock
{
Text = listing.PriceDisplay,
FontSize = 13,
FontWeight = FontWeights.Bold,
Foreground = (Brush)FindResource("MahApps.Brushes.Accent"),
Margin = new Thickness(0, 0, 8, 0)
});
textStack.Children.Add(priceRow);
textStack.Children.Add(new TextBlock
{
Text = listing.SavedAtDisplay,
FontSize = 10,
Foreground = (Brush)FindResource("MahApps.Brushes.Gray5"),
Margin = new Thickness(0, 3, 0, 0)
});
grid.Children.Add(textStack);
card.Child = grid;
// Hover effect — only for non-selected cards
card.MouseEnter += (s, e) =>
{
if (_selected?.Id != listing.Id && _cardHoverBg != null)
card.Background = _cardHoverBg;
};
card.MouseLeave += (s, e) =>
{
if (_selected?.Id != listing.Id && _cardNormalBg != null)
card.Background = _cardNormalBg;
};
card.MouseLeftButtonUp += (s, e) =>
{
_selected = listing;
ShowDetail(listing, animate: true);
// Rebuild cards to update selection styling without re-filtering
if (_service != null)
ApplyFilter(SearchBox.Text, _service.Listings);
};
return card;
}
private static void AddPhotoIcon(Border thumb)
{
// placeholder icon when no photo
thumb.Child = new MahApps.Metro.IconPacks.PackIconMaterial
{
Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.ImageOutline,
Width = 28, Height = 28,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Foreground = SystemColors.GrayTextBrush
};
}
// ---- Detail panel ----
private void ShowDetail(SavedListing listing, bool animate = true)
{
_selected = listing;
EmptyDetail.Visibility = Visibility.Collapsed;
DetailPanel.Visibility = Visibility.Visible;
DetailTitle.Text = listing.Title;
DetailPrice.Text = listing.PriceDisplay;
DetailCategory.Text = listing.Category;
DetailDate.Text = listing.SavedAtDisplay;
DetailDescription.Text = listing.Description;
if (!string.IsNullOrWhiteSpace(listing.ConditionNotes))
{
DetailCondition.Text = listing.ConditionNotes;
DetailConditionRow.Visibility = Visibility.Visible;
}
else
{
DetailConditionRow.Visibility = Visibility.Collapsed;
}
// Photos strip
DetailPhotosPanel.Children.Clear();
foreach (var path in listing.PhotoPaths)
{
if (!File.Exists(path)) continue;
try
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = new Uri(path, UriKind.Absolute); // W1
bmp.DecodePixelWidth = 200;
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
bmp.Freeze(); // M2
var img = new Image
{
Source = bmp,
Width = 120, Height = 120,
Stretch = Stretch.UniformToFill,
Margin = new Thickness(0, 0, 8, 0),
Cursor = Cursors.Hand,
ToolTip = Path.GetFileName(path)
};
img.Clip = new RectangleGeometry(new Rect(0, 0, 120, 120), 5, 5);
img.MouseLeftButtonUp += (s, e) => OpenImage(path);
DetailPhotosPanel.Children.Add(img);
}
catch { /* skip broken images */ }
}
// Fade-in animation
if (animate)
{
DetailPanel.Opacity = 0;
var fadeIn = new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(200)))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
DetailPanel.BeginAnimation(OpacityProperty, fadeIn);
}
else
{
DetailPanel.Opacity = 1;
}
}
private void ClearDetail()
{
_selected = null;
EmptyDetail.Visibility = Visibility.Visible;
DetailPanel.Visibility = Visibility.Collapsed;
DetailPanel.Opacity = 0;
}
private static void OpenImage(string path)
{
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); }
catch { }
}
// ---- Button handlers ----
private void OpenExportsDir_Click(object sender, RoutedEventArgs e)
{
var exportsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"EbayListingTool", "Exports");
if (!Directory.Exists(exportsDir)) Directory.CreateDirectory(exportsDir);
System.Diagnostics.Process.Start("explorer.exe", exportsDir);
}
private void OpenFolderDetail_Click(object sender, RoutedEventArgs e)
{
if (_selected != null) _service?.OpenExportFolder(_selected);
}
private void CopyTitle_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(_selected?.Title))
Clipboard.SetText(_selected.Title);
}
private void CopyDescription_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(_selected?.Description))
Clipboard.SetText(_selected.Description);
}
private void DeleteListing_Click(object sender, RoutedEventArgs e)
{
if (_selected == null || _service == null) return;
var result = MessageBox.Show(
$"Delete \"{_selected.Title}\"?\n\nThis will also remove the export folder from disk.",
"Delete Listing", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) return;
_service.Delete(_selected);
ClearDetail();
RefreshList();
}
}