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:
@@ -20,6 +20,10 @@ public partial class SingleItemView : UserControl
|
||||
private System.Threading.CancellationTokenSource? _categoryCts;
|
||||
private string _suggestedPriceValue = "";
|
||||
|
||||
// Photo drag-reorder
|
||||
private Point _dragStartPoint;
|
||||
private bool _isDragging;
|
||||
|
||||
public SingleItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -329,25 +333,35 @@ public partial class SingleItemView : UserControl
|
||||
if (!imageExts.Contains(Path.GetExtension(path))) continue;
|
||||
if (_draft.PhotoPaths.Count >= 12) break;
|
||||
if (_draft.PhotoPaths.Contains(path)) continue;
|
||||
|
||||
_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();
|
||||
}
|
||||
|
||||
private void AddPhotoThumbnail(string path)
|
||||
private void AddPhotoThumbnail(string path, int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.UriSource = new Uri(path, UriKind.Absolute); // W1
|
||||
bmp.UriSource = new Uri(path, UriKind.Absolute);
|
||||
bmp.DecodePixelWidth = 128;
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.EndInit();
|
||||
bmp.Freeze(); // M2
|
||||
bmp.Freeze();
|
||||
|
||||
var img = new System.Windows.Controls.Image
|
||||
{
|
||||
@@ -356,12 +370,9 @@ public partial class SingleItemView : UserControl
|
||||
Source = bmp,
|
||||
ToolTip = Path.GetFileName(path)
|
||||
};
|
||||
img.Clip = new System.Windows.Media.RectangleGeometry(new Rect(0, 0, 72, 72), 4, 4);
|
||||
|
||||
// Rounded clip on the image
|
||||
img.Clip = new System.Windows.Media.RectangleGeometry(
|
||||
new Rect(0, 0, 72, 72), 4, 4);
|
||||
|
||||
// Remove button — shown on hover via opacity triggers
|
||||
// Remove button
|
||||
var removeBtn = new Button
|
||||
{
|
||||
Width = 18, Height = 18,
|
||||
@@ -375,38 +386,116 @@ public partial class SingleItemView : UserControl
|
||||
System.Windows.Media.Color.FromArgb(200, 30, 30, 30)),
|
||||
Foreground = System.Windows.Media.Brushes.White,
|
||||
BorderThickness = new Thickness(0),
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.Bold,
|
||||
FontSize = 11, FontWeight = FontWeights.Bold,
|
||||
Content = "✕",
|
||||
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
|
||||
{
|
||||
Width = 72, Height = 72,
|
||||
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);
|
||||
if (coverBadge != null) container.Children.Add(coverBadge);
|
||||
container.Children.Add(removeBtn);
|
||||
|
||||
// Hover: reveal remove button
|
||||
container.MouseEnter += (s, e) => removeBtn.Opacity = 1;
|
||||
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);
|
||||
}
|
||||
catch { /* skip unreadable files */ }
|
||||
}
|
||||
|
||||
private void RemovePhoto(string path, UIElement thumb)
|
||||
{
|
||||
_draft.PhotoPaths.Remove(path);
|
||||
PhotosPanel.Children.Remove(thumb);
|
||||
UpdatePhotoPanel();
|
||||
}
|
||||
|
||||
private void UpdatePhotoPanel()
|
||||
{
|
||||
var count = _draft.PhotoPaths.Count;
|
||||
@@ -421,8 +510,7 @@ public partial class SingleItemView : UserControl
|
||||
private void ClearPhotos_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_draft.PhotoPaths.Clear();
|
||||
PhotosPanel.Children.Clear();
|
||||
UpdatePhotoPanel();
|
||||
RebuildPhotoThumbnails();
|
||||
}
|
||||
|
||||
// ---- Post / Save ----
|
||||
|
||||
Reference in New Issue
Block a user