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 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 ----
|
||||||
|
|||||||
Reference in New Issue
Block a user