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>
373 lines
12 KiB
C#
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();
|
|
}
|
|
}
|