Add drag-to-reorder photo ordering in New Listing tab

Photos can now be dragged to any position — cursor changes to a move
cursor to signal draggability, the drop target dims to show the
insertion point, and the list rebuilds immediately after the drop.

First photo gets a 'Cover' badge since eBay uses it as the gallery
hero image. All add/remove/clear operations now go through
RebuildPhotoThumbnails() so the panel always reflects the true order
in PhotoPaths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Foster
2026-04-13 19:52:37 +01:00
parent 426089fb3e
commit 6efa5df2c6

View File

@@ -20,6 +20,10 @@ public partial class SingleItemView : UserControl
private System.Threading.CancellationTokenSource? _categoryCts; private System.Threading.CancellationTokenSource? _categoryCts;
private string _suggestedPriceValue = ""; private string _suggestedPriceValue = "";
// Photo drag-reorder
private Point _dragStartPoint;
private bool _isDragging;
public SingleItemView() public SingleItemView()
{ {
InitializeComponent(); InitializeComponent();
@@ -329,25 +333,35 @@ public partial class SingleItemView : UserControl
if (!imageExts.Contains(Path.GetExtension(path))) continue; if (!imageExts.Contains(Path.GetExtension(path))) continue;
if (_draft.PhotoPaths.Count >= 12) break; if (_draft.PhotoPaths.Count >= 12) break;
if (_draft.PhotoPaths.Contains(path)) continue; if (_draft.PhotoPaths.Contains(path)) continue;
_draft.PhotoPaths.Add(path); _draft.PhotoPaths.Add(path);
AddPhotoThumbnail(path);
} }
RebuildPhotoThumbnails();
}
/// <summary>
/// Clears and recreates all photo thumbnails from <see cref="ListingDraft.PhotoPaths"/>.
/// Called after any add, remove, or reorder operation so the panel always matches the list.
/// </summary>
private void RebuildPhotoThumbnails()
{
PhotosPanel.Children.Clear();
for (int i = 0; i < _draft.PhotoPaths.Count; i++)
AddPhotoThumbnail(_draft.PhotoPaths[i], i);
UpdatePhotoPanel(); UpdatePhotoPanel();
} }
private void AddPhotoThumbnail(string path) private void AddPhotoThumbnail(string path, int index)
{ {
try try
{ {
var bmp = new BitmapImage(); var bmp = new BitmapImage();
bmp.BeginInit(); bmp.BeginInit();
bmp.UriSource = new Uri(path, UriKind.Absolute); // W1 bmp.UriSource = new Uri(path, UriKind.Absolute);
bmp.DecodePixelWidth = 128; bmp.DecodePixelWidth = 128;
bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit(); bmp.EndInit();
bmp.Freeze(); // M2 bmp.Freeze();
var img = new System.Windows.Controls.Image var img = new System.Windows.Controls.Image
{ {
@@ -356,12 +370,9 @@ public partial class SingleItemView : UserControl
Source = bmp, Source = bmp,
ToolTip = Path.GetFileName(path) ToolTip = Path.GetFileName(path)
}; };
img.Clip = new System.Windows.Media.RectangleGeometry(new Rect(0, 0, 72, 72), 4, 4);
// Rounded clip on the image // Remove button
img.Clip = new System.Windows.Media.RectangleGeometry(
new Rect(0, 0, 72, 72), 4, 4);
// Remove button — shown on hover via opacity triggers
var removeBtn = new Button var removeBtn = new Button
{ {
Width = 18, Height = 18, Width = 18, Height = 18,
@@ -375,38 +386,116 @@ public partial class SingleItemView : UserControl
System.Windows.Media.Color.FromArgb(200, 30, 30, 30)), System.Windows.Media.Color.FromArgb(200, 30, 30, 30)),
Foreground = System.Windows.Media.Brushes.White, Foreground = System.Windows.Media.Brushes.White,
BorderThickness = new Thickness(0), BorderThickness = new Thickness(0),
FontSize = 11, FontSize = 11, FontWeight = FontWeights.Bold,
FontWeight = FontWeights.Bold,
Content = "✕", Content = "✕",
Opacity = 0 Opacity = 0
}; };
removeBtn.Click += (s, e) =>
{
e.Handled = true; // don't bubble and trigger drag
_draft.PhotoPaths.Remove(path);
RebuildPhotoThumbnails();
};
// "Cover" badge on the first photo — it becomes the eBay gallery hero image
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),
Margin = new Thickness(2, 2, 0, 0),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
IsHitTestVisible = false, // don't block drag
Child = new TextBlock
{
Text = "Cover",
FontSize = 8,
FontWeight = FontWeights.SemiBold,
Foreground = System.Windows.Media.Brushes.White
}
};
}
// Container grid — shows remove button on mouse over
var container = new Grid var container = new Grid
{ {
Width = 72, Height = 72, Width = 72, Height = 72,
Margin = new Thickness(4), Margin = new Thickness(4),
Cursor = Cursors.Hand Cursor = Cursors.SizeAll, // signal draggability
AllowDrop = true,
Tag = path // stable identifier used by drop handler
}; };
container.Children.Add(img); container.Children.Add(img);
if (coverBadge != null) container.Children.Add(coverBadge);
container.Children.Add(removeBtn); container.Children.Add(removeBtn);
// Hover: reveal remove button
container.MouseEnter += (s, e) => removeBtn.Opacity = 1; container.MouseEnter += (s, e) => removeBtn.Opacity = 1;
container.MouseLeave += (s, e) => removeBtn.Opacity = 0; container.MouseLeave += (s, e) => removeBtn.Opacity = 0;
removeBtn.Click += (s, e) => RemovePhoto(path, container);
// Drag initiation
container.MouseLeftButtonDown += (s, e) =>
{
_dragStartPoint = e.GetPosition(null);
};
container.MouseMove += (s, e) =>
{
if (e.LeftButton != MouseButtonState.Pressed || _isDragging) return;
var pos = e.GetPosition(null);
if (Math.Abs(pos.X - _dragStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(pos.Y - _dragStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
{
_isDragging = true;
DragDrop.DoDragDrop(container, path, DragDropEffects.Move);
_isDragging = false;
}
};
// Drop target
container.DragOver += (s, e) =>
{
if (e.Data.GetDataPresent(typeof(string)) &&
(string)e.Data.GetData(typeof(string)) != path)
{
e.Effects = DragDropEffects.Move;
container.Opacity = 0.45; // dim to signal insertion point
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true;
};
container.DragLeave += (s, e) => container.Opacity = 1.0;
container.Drop += (s, e) =>
{
container.Opacity = 1.0;
if (!e.Data.GetDataPresent(typeof(string))) return;
var sourcePath = (string)e.Data.GetData(typeof(string));
var targetPath = (string)container.Tag;
if (sourcePath == targetPath) return;
var sourceIdx = _draft.PhotoPaths.IndexOf(sourcePath);
var targetIdx = _draft.PhotoPaths.IndexOf(targetPath);
if (sourceIdx < 0 || targetIdx < 0) return;
_draft.PhotoPaths.RemoveAt(sourceIdx);
_draft.PhotoPaths.Insert(targetIdx, sourcePath);
RebuildPhotoThumbnails();
e.Handled = true;
};
PhotosPanel.Children.Add(container); PhotosPanel.Children.Add(container);
} }
catch { /* skip unreadable files */ } catch { /* skip unreadable files */ }
} }
private void RemovePhoto(string path, UIElement thumb)
{
_draft.PhotoPaths.Remove(path);
PhotosPanel.Children.Remove(thumb);
UpdatePhotoPanel();
}
private void UpdatePhotoPanel() private void UpdatePhotoPanel()
{ {
var count = _draft.PhotoPaths.Count; var count = _draft.PhotoPaths.Count;
@@ -421,8 +510,7 @@ public partial class SingleItemView : UserControl
private void ClearPhotos_Click(object sender, RoutedEventArgs e) private void ClearPhotos_Click(object sender, RoutedEventArgs e)
{ {
_draft.PhotoPaths.Clear(); _draft.PhotoPaths.Clear();
PhotosPanel.Children.Clear(); RebuildPhotoThumbnails();
UpdatePhotoPanel();
} }
// ---- Post / Save ---- // ---- Post / Save ----